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

时间:2016-06-24 07:27:55

标签: python python-2.x

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)不会导致更改的对象,这是正确的。)

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

编辑:

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

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

我如何知道代码已更改?(是否需要再次执行代码或添加简单空间的更改。)

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

3 个答案:

答案 0 :(得分:3)

不要这样做作为优化,它不会带来极快的加速

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

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

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

使用字节代码:

一般来说,我要提出的方法只会返回&#34;等于&#34; (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)时会生成相同的确切输出 ,唯一的区别在于加载code对象fact 1}}:

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的代码对象)如果我们不比较这些函数,我们就会得到误报。

那么函数code的{​​{1}}对象在哪里呢?它们分别位于fact对象co_constscode内的c字段中,d是包含所有常量的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 的{​​{1}}用于检查CodeType个实例。

我认为只使用types生成的code个对象,这是最好的。

答案 1 :(得分:2)

有许多方法可以做到这一点比你正在尝试的方法简单得多:

  1. 使用diff-w(忽略空格)标记。如果产生差异,您将很可能知道它是代码更改。

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

答案 2 :(得分:1)

这么多问题......

  

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

是的,有点儿。但是,由于代码对象包含其局部变量的名称,因此将x重命名为y将使您的比较失败,即使没有功能更改。

  

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

值得一提的是,“简单的空间添加”可能需要在Python中重新编译和重新执行,而不是在许多其他语言中。

  

我认为他们是通过使用散列值或类似的东西实现我想要实现的目标的另一种方式,但我想这样做,因为它似乎更像Pythonic并且比重新发明轮子更好。

我不确定这个特殊选项的Pythonic是什么 - 也许它会让你的代码变得更简单?这是选择它的一个很好的理由。

否则,字符串比较可能更快(对变量重命名具有相同的敏感性),并且完整的AST比较更复杂但可能更智能。

最后:

  

每次用户执行他/她的代码时,都会在服务器上执行。甚至添加空格/括号等也会导致新的执行。我试图通过存储已编译的代码来优化此步骤,如果新请求中的代码相同,则不执行它。

但是当你明确要求时,你可能会 执行用户的代码。

如果你每次键入一个字符时都这样做,那显然是没有意义的,但是考虑使用随机数的代码:即使没有代码更改,用户也会合理地期望看到输出在执行时发生变化。