【问题标题】:How is a python function's name reference found inside it's declaration?如何在它的声明中找到 python 函数的名称引用?
【发布时间】:2015-03-23 14:56:39
【问题描述】:

foo 会在函数 def 中为 foo 知道 foo,这完全出乎意料(至少对我而言)。这到底是怎么回事?

>>> def foo(x):
...   print "wow"
...   print globals().get('foo', 'sorry')
...   return foo
... 
>>> f = foo(3)
wow
<function foo at 0x10135f8c0>
>>> f
<function foo at 0x10135f8c0>

这是python惰性求值的某种效果吗?它首先构建函数代码并将其放入globals,但实际上稍后在调用它时构建函数?哇……这是什么形式的蟒蛇魔法?

当然,这便于递归……这可能是语言中出现这种情况的原因……

>>> def bar(x):
...   return bar(x)
... 
>>> bar(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in bar
  File "<stdin>", line 2, in bar
  File "<stdin>", line 2, in bar
  ...snip...
  File "<stdin>", line 2, in bar
  File "<stdin>", line 2, in bar
RuntimeError: maximum recursion depth exceeded

【问题讨论】:

  • “但后来实际构建函数”是什么意思?该功能已经“构建”(定义)。我没有看到任何魔法。
  • 你习惯什么语言?因为我没有看到任何魔法,递归也没有。你把这和什么比较?
  • 我不明白你的问题。你会怎么做递归?为什么这使得无限递归比任何其他支持递归的语言更容易?
  • @IsmailBadawi:呃……?谁说过其他语言?
  • 不,我在询问使名称引用在函数定义内部有效的过程......以便递归成为可能。正如我的问题所述,我意识到引擎盖下有字节码。

标签: python


【解决方案1】:

没有什么真正神秘的。

有,我相信这有两个通过方面,你正在对函数的主体进行后期评估。

更新:我对定义步骤之后发生的情况的解释进行了限定 - 看起来函数代码被调用之前确实发生了一些事情。在这方面,Wally 的回答比我的要好。

Pass #1 - def foo(x) 被找到并设置在模块的命名空间中。可能还会添加函数参数。

就是这样,你不用深入代码。

更新:实际上,我不太确定代码是否在调用之前以某种方式处理。查看 foo.func_code 内部结构显示调用前后没有太大差异。

此外,如果您在调用函数之前确实出现了诸如括号嵌套或空格问题之类的语法错误,那么在调用之前就会出现一些问题。最后看我的代码。

我会猜测代码已被解析,但任何变量解析都会延迟到实际执行。

通过 #2 - 你调用 foo(x),它被找到并被调用。

函数的代码第一次被执行。

当您点击 globals()["foo"] 时,它会获取存储在 #1 中的现有引用。

当您运行coverage.py 或类似文件时,您还可以看到此行为的一些提示。导入时,模块中的所有外部定义都标记为已覆盖。

但实际代码只有在您调用它时才会被覆盖。

另一种思考方式是,您需要先通过命名空间传递来设置引用,然后再继续进行。否则,在我下面的代码中,foo 将找不到 bar。

这是我用来区分执行错误和语法错误的一些代码...

def foo(x):
    """comment/uncomment to see behavior"""
    pass
    # return bar(x)   #this works

    return bar2(x)  #call time error

    #   return bar(x) bad whitespace  #IndentationError, nothing runs

print "foo defined"

def bar(x):
    return x*2


print "calling foo#1"

try:
    print foo(3)
except Exception, e:
    print e

#let's make it so there is a bar2...
bar2 = bar

print "calling foo#2"

try:
    print foo(6)
except Exception, e:
    print e

【讨论】:

    【解决方案2】:

    也许dis 可以帮助...

    >>> def foo(x):
    ...   print "wow"
    ...   print globals().get('foo', 'sorry')
    ...   return foo
    ... 
    >>> import dis
    >>> dis.dis(foo)
      2           0 LOAD_CONST               1 ('wow')
                  3 PRINT_ITEM          
                  4 PRINT_NEWLINE       
    
      3           5 LOAD_GLOBAL              0 (globals)
                  8 CALL_FUNCTION            0
                 11 LOAD_ATTR                1 (get)
                 14 LOAD_CONST               2 ('foo')
                 17 LOAD_CONST               3 ('sorry')
                 20 CALL_FUNCTION            2
                 23 PRINT_ITEM          
                 24 PRINT_NEWLINE       
    
      4          25 LOAD_GLOBAL              2 (foo)
                 28 RETURN_VALUE        
    >>> 
    

    嗯。复杂的。所以让我们变得更简单......

    >>> def zap(x):
    ...   return zap
    ... 
    >>> dis.dis(zap)
      2           0 LOAD_GLOBAL              0 (zap)
                  3 RETURN_VALUE        
    >>> 
    

    是的,看起来字节码已构建,并保存从globals 加载zap 的指令。因此,两步过程使 zap 内部的 zap 一点也不特别。

    让我们看看我们是否可以更好地深入研究这个过程并澄清......

    >>> def blah(x):
    ...   def hlab(y):
    ...     return blah(x) 
    ...   return hlab
    ... 
    >>> blah.func_code.co_consts
    (None, <code object hlab at 0x10fcdfd30, file "<stdin>", line 2>)
    >>> b = blah(4)
    >>> b 
    <function hlab at 0x10fce9c80>
    >>> dis.dis(blah.func_code.co_consts[-1])
      3           0 LOAD_GLOBAL              0 (blah)
                  3 LOAD_DEREF               0 (x)
                  6 CALL_FUNCTION            1
                  9 RETURN_VALUE        
    >>> dis.dis(b)
      3           0 LOAD_GLOBAL              0 (blah)
                  3 LOAD_DEREF               0 (x)
                  6 CALL_FUNCTION            1
                  9 RETURN_VALUE        
    >>> b_ = blah.func_code.co_consts[-1]   
    >>> b.func_code
    <code object hlab at 0x10fcdfd30, file "<stdin>", line 2>
    >>> b_
    <code object hlab at 0x10fcdfd30, file "<stdin>", line 2>
    >>> 
    

    所以看起来字节码是先构建的,然后函数是从那个构建的......然后指向原始字节码。我仍然看不到它是如何连接的,但我认为这是在“堆栈”上以某种方式完成的。至少,这个过程会使函数 def 中使用的任何名称引用完全不特殊(即,如果 foo 使用 fooblahwhatever,则无关紧要)。

    好吧,我明白了。没什么特别的。

    虽然这有点奇怪……

    >>> b(2)
    <function hlab at 0x10faf9410>
    >>> b(2)(2)
    <function hlab at 0x10faf9578>
    >>> b(2)(2)(2)
    <function hlab at 0x10faf9410>
    >>> _ is b(2)
    False
    

    ...但我认为它只是循环可用的内存地址,或类似的东西。

    【讨论】:

    • 哇,你最初问了这个问题,现在你已经开始对内部结构了如指掌了?印象深刻。尝试查看 func_code 内部结构,但只使用了 prettyprint 和 dir。我没有看到调用该函数之前和之后的任何区别。但肯定有第一次在命名空间上创建引用并将它们提供给代码的 2 次传递机制。
    • Although this is a bit odd - 不,不是。开头b 引用了hlab,当你用b(2) 调用它时,hlab 被调用,它实际上用参数blah 调用blah 并返回它返回的值。所以我们回到第一方,因为这就是我们首先创建b 的方式,通过使用参数调用blah。所以,b(2)(2)(2)... 会重复同样的事情。
    • 我就是这么想的,但是为什么b(2)(2)(2)不是b(2)?我希望id(b(2)(2)(2)) is id(b(2)) 是这种情况——但最后的声明似乎反驳了这一点。请注意,第一个和第三个内存地址是重复的——对象没有被垃圾回收,因为它们没有分配一个名称?
    猜你喜欢
    • 1970-01-01
    • 2020-12-10
    • 1970-01-01
    • 1970-01-01
    • 2019-04-24
    • 2023-03-20
    • 1970-01-01
    • 2021-12-17
    • 2016-09-08
    相关资源
    最近更新 更多