【问题标题】:In Python, are function-level assignments evaluated at each function call?在 Python 中,是否在每个函数调用时评估函数级赋值?
【发布时间】:2014-01-29 00:59:38
【问题描述】:

请考虑以下代码:

import re

def qcharToUnicode(s):
    p = re.compile(r"QChar\((0x[a-fA-F0-9]*)\)")
    return p.sub(lambda m: '"' + chr(int(m.group(1),16)) + '"', s)

def fixSurrogatePresence(s) :
    '''Returns the input UTF-16 string with surrogate pairs replaced by the character they represent'''
    # ideas from:
    # http://www.unicode.org/faq/utf_bom.html#utf16-4
    # http://stackoverflow.com/a/6928284/1503120
    def joinSurrogates(match) :
        SURROGATE_OFFSET = 0x10000 - ( 0xD800 << 10 ) - 0xDC00
        return chr ( ( ord(match.group(1)) << 10 ) + ord(match.group(2)) + SURROGATE_OFFSET )
    return re.sub ( '([\uD800-\uDBFF])([\uDC00-\uDFFF])', joinSurrogates, s )

现在我下面的问题可能反映了一种 C/C++ 思维方式(而不是“Pythonic”思维方式),但我还是很好奇:

我想知道编译的 RE 对象 pqcharToUnicodeSURROGATE_OFFSETjoinSurrogates 的评估是否会在每次调用各自的函数时进行,或者只在定义点进行一次?我的意思是在 C/C++ 中可以将值声明为 static const 并且编译将 (IIUC) 使构造只发生一次,但在 Python 中我们没有任何此类声明。

这个问题在编译的 RE 对象的情况下更为相关,因为似乎构造这样一个对象的唯一原因是避免重复编译,正如Python RE HOWTO 所说:

您应该使用这些模块级函数,还是应该获取模式并自己调用它的方法?如果你是 在循环中访问正则表达式,预编译它将节省一些函数调用。

...如果在每次函数调用时都进行编译,这个目的就会失败。我不想将符号p(或SURROGATE_OFFSET)放在模块级别,因为我只想将其可见性限制为相关功能。

那么解释器是否会做一些类似启发式的操作来确定特定符号指向的值是恒定的(并且仅在特定函数中可见),因此不需要在下一个函数中重建?此外,这是由语言定义的还是依赖于实现的? (希望我没有要求太多!)

一个相关的问题是关于qcharToUnicode 中函数对象lambda m 的构造——它是否也像def 声明的其他命名函数对象一样只定义一次?

【问题讨论】:

  • 如果整个 def 块在循环中,即使由 def 定义的命名函数也可以定义多次。一般来说,Python 很少假设在程序过程中会发生什么变化或不会发生什么变化。代码在程序流程中在运行时遇到时执行。

标签: python


【解决方案1】:

简单的答案是,正如所写的那样,代码将在每次函数调用时重复执行。对于您描述的情况,Python 中没有隐式缓存机制。

你应该改掉谈论“声明”的习惯。函数定义实际上也“只是”一个普通语句,所以我可以编写一个循环来重复定义相同的函数:

for i in range(10):
    def f(x):
        return x*2
    y = f(i)

在这里,我们将承担在每次循环运行时创建函数的成本。时序显示,这段代码的运行时间大约是之前代码的 75%:

def f(x):
    return x*2

for i in range(10):
    y = f(i)

优化 RE 案例的标准方法是您已经知道将 p 变量放在模块范围内,即:

p = re.compile(r"QChar\((0x[a-fA-F0-9]*)\)")

def qcharToUnicode(s):
    return p.sub(lambda m: '"' + chr(int(m.group(1),16)) + '"', s)

您可以使用约定,例如在变量前加上“_”来表示它不应该被使用,但如果您没有记录它,通常人们不会使用它。使 RE 函数本地化的一个技巧是使用关于默认参数的结果:它们与函数定义同时执行,因此您可以这样做:

def qcharToUnicode(s, p=re.compile(r"QChar\((0x[a-fA-F0-9]*)\)")):
    return p.sub(lambda m: '"' + chr(int(m.group(1),16)) + '"', s)

这将允许您进行相同的优化,但也可以在匹配函数中增加一点灵活性。

正确思考函数定义还可以让您停止思考 lambdadef 的不同之处。唯一的区别是def 还将函数对象绑定到一个名称 - 创建的底层对象是相同的。

【讨论】:

  • 您的第二个代码 sn-p 不起作用。当任何f 函数被调用时,x*i 中的i 将使用i 的当前值进行评估,而不是使用定义函数时的值。
  • @user2357112 等待验证,但我相信他只会在 javascript 等效项中存在该错误。
  • @stewSquared: Javascript 和 Python 都使用函数作用域而不是块作用域,所以问题都会发生。
  • @user2357112 我们可能在谈论不同的问题。在 javascript 版本中,所有返回的函数都相当于 "function(x) {return x*10}" 而在 python 版本中,它们确实是不同的函数。
  • 这个想法是在循环内使用f,所以它没有任何实际问题。即使您将f 传递给另一个函数,这也会起作用。除非您习惯在循环中更改循环变量,否则这仍然很有用,尽管值得记住的是,如果稍后 i 发生更改,f 的行为可能会发生变化。
【解决方案2】:

Python 是一种脚本/解释语言...所以是的,每次调用该函数时都会进行分配。解释器只会解析你的代码一次,生成 Python 字节码。下次调用这个函数时,它已经编译成 Python VM 字节码,所以函数会被简单地执行。

每次都会调用 re.compile,就像在其他语言中一样。如果要模拟静态初始化,请考虑使用全局变量,这样它只会被调用一次。更好的是,您可以创建一个包含静态方法和静态成员(类而不是实例成员)的类。

您可以使用 Python 中的 dis 模块检查所有这些。所以,我只是将您的代码复制并粘贴到 teste.py 模块中。

>>> import teste
>>> import dis
>>> dis.dis(teste.qcharToUnicode)
  4           0 LOAD_GLOBAL              0 (re)
              3 LOAD_ATTR                1 (compile)
              6 LOAD_CONST               1 ('QChar\\((0x[a-fA-F0-9]*)\\)')
              9 CALL_FUNCTION            1
             12 STORE_FAST               1 (p)

  5          15 LOAD_FAST                1 (p)
             18 LOAD_ATTR                2 (sub)
             21 LOAD_CONST               2 (<code object <lambda> at 0056C140, file "teste.py", line 5>)
             24 MAKE_FUNCTION            0
             27 LOAD_FAST                0 (s)
             30 CALL_FUNCTION            2
             33 RETURN_VALUE

【讨论】:

  • 嗯,它说“不要使用评论来表示感谢”,但我觉得有点缺乏礼仪,不对所有有用的回复说谢谢。我对他们都投了赞成票并接受了一个。特别是这个很有用,因为我不知道dis
【解决方案3】:

是的,他们是。假设 re.compile() 有副作用。每次对 p 进行赋值时,即每次调用包含所述赋值的函数时,都会发生这种副作用。

这可以验证:

def foo():
    print("ahahaha!")
    return bar

def f():
    return foo()
def funcWithSideEffect():
    print("The airspeed velocity of an unladen swallow (european) is...")
    return 25

def funcEnclosingAssignment():
    p = funcWithSideEffect()
    return p;

a = funcEnclosingAssignment()
b = funcEnclosingAssignment()
c = funcEnclosingAssignment()

每次调用封闭函数(类似于您的 qcharToUnicode)时,都会打印语句,表明正在重新计算 p。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多