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

Can compile function output be used to detect changes in the source code

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 是假的。哪个是预期的行为? (在源代码中添加 space 或使 print 1 而不是 print(1) 不会导致更改对象,这是正确的。)

我的问题是:可以使用编译对象来检测 Python 源代码中的更改吗?

编辑:

为了更清楚地解释,我正在开发一个允许用户执行他们的 Python 代码的 Web 应用程序。

每次用户执行代码时,它都会在服务器上执行。即使添加 space/bracket 等也会导致新的执行。我正在尝试通过存储编译后的代码来优化此步骤,如果新请求中的代码相同则不执行它。

我怎么知道代码已经改变了?(是那种需要重新执行代码的改变还是简单的space添加。)

我确实认为还有其他方法可以通过使用散列值或类似的东西来实现我想要实现的目标,但我想这样做是因为它看起来更 Pythonic 并且比重新发明更好车轮。

有很多方法可以做到这一点,比您正在尝试的要简单得多:

  1. 使用 diff,带有 -w(忽略空格)标志。如果存在差异,您将知道很可能是代码更改。

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

这么多问题...

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.)

值得一提的是 "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.

我不确定 Python这个特定选项有什么特别之处 - 也许它可以让您的代码更简单?这将是选择它的一个很好的理由。

否则,字符串比较可能更快(对变量重命名具有相同的敏感性),并且完整的 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.

但是您可能应该在用户明确要求您时执行用户的代码。

如果每次他们键入一个字符时您都这样做,那显然毫无意义,但考虑使用随机数的代码:用户有理由期望在他们点击执行时看到输出变化,即使没有代码变化。

不要这样做作为优化,它不会导致很大的加速

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

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


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

使用字节码:

一般来说,如果所讨论的代码片段仅在 white-space 上有所不同,我将只介绍 returns "equal" (True) 的方法and/or 参数名称,在所有其他情况下,它 returns False(您可以尝试使其更智能,但这可能需要您考虑许多边缘情况)。

您通常应该注意的是 compile return 是一个 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),进行函数调用和 returns。

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

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 对象位于何处?它们分别位于 code 对象 cd 内的一个名为 co_consts 的字段中, 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 中的不同位置,我们将 return 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

其中 types 中的 CodeType 用于检查 code 个实例。

我认为这几乎是您仅使用从 compile.

生成的 code 个对象所能做的最好的事情