python如何实现相互递归函数调用的查找



假设我在同一个python文件中一个接一个地有两个函数:

def A(n):
    B(n-1)
# if I add A(1) here, it gives me an error
def B(n):
    if n <= 0:
        return
    else:
        A(n-1)

当解释器读取A时,B还没有定义,但是这段代码没有给我一个错误。这让我很困惑,因为我认为Python程序是逐行解释的。为什么在A中调用B的尝试不会立即给出错误,在调用任何东西之前?

我的理解是,当def被解释时,Python用{"function name": function address}向一些本地名称空间locals()添加一个条目,但对于函数体,它只做语法检查:

def A():
    # this will give an error as it isn't a valid expression
    syntax error
def B():
    # even though x is not defined, this does not give an error
    print(x)
    # same as above, NameError is only detected during runtime
    A()

我写对了吗?

B(n-1)行表示"当执行此语句时,在模块作用域中查找某些函数B,然后带参数n-1调用它"。由于查找是在函数执行时进行的,因此可以稍后定义B

(另外,您可以用不同的函数完全覆盖B,然后A将调用新的B。但是这会导致一些令人困惑的代码。)

如果您担心无法捕捉到对不存在的函数的调用,您可以尝试使用静态分析工具。除此之外,请确保您正在测试您的代码。

SyntaxError将在编译时被捕获,但大多数其他错误(NameError, ValueError等)将仅在运行时被捕获,然后仅当该函数被调用时。

"如果我写了一个函数,如果它在我的测试中没有被调用…"——这就是为什么你应该测试一切。

一些ide会在各种情况下发出警告,但最好的选择仍然是自己进行彻底的测试。这样,您还可以检查由诸如用户输入之类的因素引起的错误,IDE的自动检查不包括这些因素。

当解释器读取A时,B还没有定义,但是这段代码没有给我一个错误

python解释器不给出错误的原因可以从docs中找到,这在技术上称为前向引用:

自由变量的名称解析发生在运行时,而不是编译时。

具体考虑第一个示例代码:

在Python中,def A():是一个可执行语句。通过使用块内的代码来创建一个函数对象,然后将该对象赋值给名称A来求值。这不仅仅是一个"语法检查";它实际编译代码(在参考实现中,这意味着它为Python VM生成字节码)。该过程包括确定B i)是一个名称,ii)将在全局中查找(因为在A中没有赋值给 B)。

我们可以使用标准库中的dis模块看到结果:

>>> def A(n):
...     B(n-1)
... 
>>> import dis
>>> dis.dis(A)
  2           0 LOAD_GLOBAL              0 (B)
              2 LOAD_FAST                0 (n)
              4 LOAD_CONST               1 (1)
              6 BINARY_SUBTRACT
              8 CALL_FUNCTION            1
             10 POP_TOP
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE

此结果是特定于版本和实现的;我已经展示了使用Python 3.8的参考实现所得到的结果。可以推断,LOAD_GLOBAL操作码表示在全局命名空间中查找B的指令(如果查找不到,则在特殊的内置名称中查找)。

但是,在调用函数之前,没有任何代码实际运行。因此,B没有在全局命名空间中定义并不重要;它会在需要的时候出现。 从评论:

但是为什么python在编译时不检查NameError ?

因为没有什么值得检查的名称。Python是一种动态语言;的全部要点是,您可以自由地做任何事情来确保BA使用它之前被定义。例如,包括从互联网上下载另一个Python文件并在当前全局命名空间中动态执行它的极端糟糕的想法。

最新更新