编译函数输出可以用来检测源代码中的更改吗

2024-04-19 19:05:57 发布

您现在位置:Python中文网/ 问答频道 /正文

Python的内置函数compile()

code = """def fact(x):
    if x <= 1:
        return 1
    print (1)
    return x*fact(x-1)
fact(10)"""
c = compile(code, "<string>", "exec")

code = """def fact(y):
    if y <= 1:
        return 1
    print (1)
    return y*fact(y-1)
fact(10)"""
d = compile(code, "<string>", "exec")

这里c==d为假。预期的行为是什么?(在源代码中添加空格或用print 1代替print(1)不会导致对象发生更改,这是正确的。)

我的问题是:compile对象可以用来检测Python源代码中的更改吗?你知道吗

编辑:

为了更清楚地解释这一点,我正在开发一个允许用户执行Python代码的web应用程序。你知道吗

每次用户执行代码时,它都会在服务器上执行。即使添加空格/括号等也会导致新的执行。我试图通过存储编译后的代码来优化这一步,如果新请求中的代码相同,则不执行它。你知道吗

如何知道代码已更改?(这种更改是需要再次执行代码还是需要简单的空格添加。)

我确实认为有其他方法可以通过使用散列值或类似的东西来实现我想要实现的目标,但我想这样做,因为它看起来更像Python,而且比重新发明轮子更好。你知道吗


Tags: 对象代码用户stringreturnif源代码def
3条回答

不要这样做作为一种优化,它不会带来很大的速度提升。你知道吗

compile是一个built-in function,它是在C中实现的,它速度很快,并且不是您应该寻求优化的地方。当用户试图执行代码时,应该允许在没有任何缓存的情况下编译和执行代码。你知道吗

考虑下面的代码,编写代码试图发现用户输入的文本是否有任何差异,很可能会导致比实际编译函数并执行它更多的执行时间。除此之外,您还必须在某个地方存储和检索编译代码。第一个增加了不必要的空间需求,第二个增加了开销,因为您必须进行查找(当然,这取决于您选择如何存储它)。你知道吗


除了我刚才所说的,您可以尝试比较编译Python代码的结果,但是这种方法是有限的,而且很模糊。我假设您输入的代码片段应该比较相等,因为它们之间的唯一区别在于参数名(其名称对代码执行没有影响)。你知道吗

使用字节码:

一般来说,我将要介绍的方法只返回“equal”(True),如果所讨论的代码片段仅在空格和/或参数名称上不同,在所有其他情况下,它返回False(您可以尝试使其更智能,但这可能需要考虑许多边缘情况)。你知道吗

通常应该注意的是compile返回一个code对象。code对象通常包含执行一段代码所需的所有信息Python。您可以使用dis模块查看代码对象中包含的指令:

from dis import dis
dis(c)
  1           0 LOAD_CONST               0 (<code object fact at 0x7fa7bc30e630, file "<string>", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (fact)

  6           9 LOAD_NAME                0 (fact)
             12 LOAD_CONST               1 (10)
             15 CALL_FUNCTION            1
             18 POP_TOP             
             19 LOAD_CONST               2 (None)
             22 RETURN_VALUE  

这就是您的代码片段所做的。它加载另一个代码对象(表示函数fact),进行函数调用并返回。你知道吗

调用dis(d)时会生成相同的精确输出,唯一的区别在于为fact加载code对象:

dis(d)
  1           0 LOAD_CONST               0 (<code object fact at 0x7fa7bc30e5b0, file "<string>", line 1>)

如你所见,它们是不同的:

code object fact at 0x7fa7bc30e630 != code object fact at 0x7fa7bc30e5b0

但是,它们的区别不在于它们的功能,而在于它们是不同的实例,表示相同的功能单元。你知道吗

所以这里有一个问题:如果它们代表相同的功能,我们是否关心它们是不同的实例?同样,如果两个变量代表相同的值,我们是否关心它们是否有不同的名称?我假设我们没有,这就是为什么我认为如果比较代码对象的原始字节代码,就可以对它们进行有意义的基本比较。你知道吗

比较原始字节码:

原始字节码和我之前描述的差不多,没有名称,没有标识只是Python解释器的一系列命令(纯功能)。我们来看看:

c.co_code
'd\x00\x00\x84\x00\x00Z\x00\x00e\x00\x00d\x01\x00\x83\x01\x00\x01d\x02\x00S'

好吧,太难看了,让我们仔细看看:

dis(c.co_code)
  0 LOAD_CONST          0 (0)
  3 MAKE_FUNCTION       0
  6 STORE_NAME          0 (0)
  9 LOAD_NAME           0 (0)
 12 LOAD_CONST          1 (1)
 15 CALL_FUNCTION       1
 18 POP_TOP        
 19 LOAD_CONST          2 (2)
 22 RETURN_VALUE 

看起来好多了。这与前面的dis(c)命令完全相同,唯一的区别是名称不存在,因为它们在这一切中并不真正起作用。你知道吗

那么,如果我们比较d.co_codec.co_code,我们会得到什么呢?当然,因为执行的命令是相同的,所以相等。但是这里有一个陷阱,为了100%确定d.co_code等于c.co_code,我们还需要比较cd中加载的函数的代码对象(表示函数fact的代码对象),如果我们不比较这些,我们将得到误报。你知道吗

那么函数factcode对象在每种情况下位于何处呢?它们位于一个叫做co_constscode对象cd中,co_consts是一个list包含特定code对象的所有常量。如果你在里面取一个峰值,你就能看到每个峰值的定义:

# located at position 0 in this case.
# the byte code for function 'fact'
dis(c.co_consts[0])  
  2           0 LOAD_FAST                0 (x)
              3 LOAD_CONST               1 (1)
              6 COMPARE_OP               1 (<=)
              9 POP_JUMP_IF_FALSE       16

  3          12 LOAD_CONST               1 (1)
             15 RETURN_VALUE        

  4     >>   16 LOAD_CONST               1 (1)
             19 PRINT_ITEM          
             20 PRINT_NEWLINE       

  5          21 LOAD_FAST                0 (x)
             24 LOAD_GLOBAL              0 (fact)
             27 LOAD_FAST                0 (x)
             30 LOAD_CONST               1 (1)
             33 BINARY_SUBTRACT     
             34 CALL_FUNCTION            1
             37 BINARY_MULTIPLY     
             38 RETURN_VALUE        

那么,我们该怎么办?我们比较它们的原始字节码,看看它们是否代表相同的功能,就像我们以前做的那样。你知道吗

正如您所理解的,这是一个递归过程,我们首先比较输入的原始字节码,然后扫描co_consts以查看是否存在另一个code对象,然后重复,直到找不到code对象,如果code对象位于co_consts中的不同位置,我们将返回False。你知道吗

在代码中,应该是这样的:

from types import CodeType 

def equal_code(c1, c2):
    if c1.co_code != c2.co_code:
        return False
    for i, j in zip(c1.co_consts, c2.co_consts):
        if isinstance(i, CodeType):
            if isinstance(j, CodeType):
                return equal_code(i, j)
            else: # consts in different position, return false
                return False
    return True

其中,来自typesCodeType用于检查code实例。你知道吗

我认为这是您仅使用从compile生成的code对象所能做到的最好的方法。你知道吗

这么多问题。。。你知道吗

Can compile object be used to detect changes in the Python source code?

是的,有点。但是,由于代码对象包含其局部变量的名称,因此将x重命名为y将导致比较失败,即使没有函数更改。你知道吗


How do I know that code has changed? (Is it the kind of change that requires executing the code again or a simple space addition.)

值得一提的是,“简单的空间添加”可能需要在Python中重新编译和执行,比在许多其他语言中更需要这样做。你知道吗

I do think their are other way to achieve what I a trying to achieve by using hashed value or something like this but I want to do it this way because it seems more Pythonic and better than reinventing the wheel.

我不知道这个选项有什么Pythonic的地方-也许它让你的代码更简单?这将是一个很好的选择它的理由。你知道吗

否则,字符串比较可能更快(对变量重命名的敏感度相同),完整的AST比较更复杂,但可能更聪明。你知道吗


最后:

Every time an user executes his/her code its executed on the server. Even adding an space/bracket etc results into a new execution. I am trying to optimize this step by storing the compiled code and if the code is same in new request then not executing it.

但是当用户明确要求您执行时,您可能应该执行用户的代码。你知道吗

如果每次用户键入字符时都这样做,显然毫无意义,但请考虑使用随机数的代码:用户在点击execute时会合理地期望看到输出更改,即使没有代码更改。你知道吗

有许多方法比你正在尝试的要简单得多:

  1. 使用带有-w(忽略空白)标志的diff。如果产生了差异,您将知道这很可能是代码更改。

  2. 使用git或其他源代码存储库。然后在决定执行之前,找出文件之间是否有更改。然而,在这方面,您只是在使用git的不同功能,所以不妨使用第一个选项。

相关问题 更多 >