为什么我可以在函数中使用变量之前在 Python 中定义它?

Chr*_*ean 8 python class function

在下面的代码中,我定义了两个函数。maincube。我想main成为我的程序的开始,所以我cube在里面调用了main

>>> def main():
    number = int(input('Enter a number: '))
    cubed_number =  cube(number)
    print("The number cubed is: ", cubed_number)


>>> def cube(number):
    return number * number * number

>>> 
Run Code Online (Sandbox Code Playgroud)

但是我在定义cube之后main,所以我认为我的代码会引发一个NameError. 然而,Python 并没有引发异常,而是完美地执行了我的代码:

>>> main()
Enter a number: 5
The number cubed is:  125
>>> 
Run Code Online (Sandbox Code Playgroud)

发生了什么?为什么 Python 能够运行我的代码,而不知道它cube是如何定义的?NameError怎么没有升职?

更奇怪的是,当我尝试对类做同样的事情时,Python 确实引发了一个NameError

>>> class Main:
        cubed_number()


Traceback (most recent call last):
  File "<pyshell#27>", line 1, in <module>
    class Main:
  File "<pyshell#27>", line 2, in Main
    cubed_number()
NameError: name 'cubed_number' is not defined
>>> 
Run Code Online (Sandbox Code Playgroud)

这里发生了什么事?


注意:这不是为什么我可以在定义一个函数之前调用它,只有一个警告?,因为那里的答案并没有真正解释这种行为在 Python 中为什么或如何工作。我创建了这个问答是因为当前对此类问题的回答分散在各种问题中。我也觉得展示幕后正在发生的事情会有所帮助。随意编辑和改进问答。

Chr*_*ean 8

要了解正在发生的事情,必须了解 Python 在定义函数和执行函数之间的区别。

定义与执行

当 Python 遇到函数定义时,它会将函数编译为代码对象。

代码对象是 Python 用来保存与特定可执行代码块相关联的字节码的内部结构。它还包含 Python 执行字节码所需的其他信息,例如常量和局部变量名称。该文档对什么是代码对象进行了更广泛的概述

然后使用代码对象来构造函数对象。然后,函数对象的代码对象用于在稍后调用函数时执行该函数。Python 不会执行该函数,它只会将该函数编译为一个可以在以后执行的对象。Python 执行函数的唯一时间是调用函数时

这是文档中提到的相关部分:

函数定义是一个可执行语句。它的执行将当前本地命名空间中的函数名称绑定到一个函数对象(函数的可执行代码的包装器)。该函数对象包含对当前全局命名空间的引用,作为调用该函数时要使用的全局命名空间。

函数定义不执行函数体;this 仅在调用函数时执行。

由于这种区别,在调用函数之前,Python 无法验证名称是否实际定义。因此,您可以在函数体中使用当前不存在的名称。只要在调用函数时定义了名称,Python 就不会引发错误。

这是一个例子。我们定义了一个func将两个变量相加的函数;ab

>>> def func():
...     return a + b
Run Code Online (Sandbox Code Playgroud)

如您所见,Python 没有引发任何错误。这是因为它只是简单地编译func。它没有尝试执行该函数,因此它没有看到a并且b没有定义。

我们可以反汇编func的代码对象,并使用该dis模块查看字节码的样子。这将告诉我们更多关于 Python 正在做什么的信息:

>>> from dis import dis
>>> dis(func)
  2           0 LOAD_GLOBAL              0 (a)
              2 LOAD_GLOBAL              1 (b)
              4 BINARY_ADD
              6 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

PythonLOAD_GLOBAL在字节码中编码了两条指令。指令的参数分别是变量名ab

这表明 Python 确实看到我们在编译函数时试图引用两个变量,并为此创建了字节码指令。但它不会尝试实际执行指令,直到函数被调用

让我们看看当我们尝试func通过调用它来执行字节码时会发生什么:

>>> func()
Traceback (most recent call last):
  File "<pyshell#15>", line 1, in <module>
    func()
  File "<pyshell#14>", line 2, in func
    return a + b
NameError: name 'a' is not defined
Run Code Online (Sandbox Code Playgroud)

如您所见,Python 引发了一个NameError. 这是因为它试图执行这两LOAD_GLOBAL条指令,但发现名称在全局范围内未定义。

现在让我们看看如果我们定义了两个变量a并且b在调用之前会发生什么func

>>> a = 1
>>> b = 2
>>> 
>>> func()
3
Run Code Online (Sandbox Code Playgroud)

上述工作的原因,是因为当 Python 执行func的字节码时,它能够找到全局变量ab,并使用它们来执行函数。

这个例子同样适用于问题。当main被编译,Python的“锯”我们试图调用名为变量cube并生成指令来获取的价值cube。但是cube在执行指令之前,它并没有尝试找到命名的可调用对象。并且当main的字节码被执行(例如main被调用)时,一个名为的函数cube被定义,所以 Python 没有引发错误。

但是,如果我们尝试定义多维数据集之前调用 main ,由于与上述示例相同的原因,我们将收到名称错误:

>>> def main():
...     number = int(input('Enter a number: '))
...     cubed_number =  cube(number)
...     print("The number cubed is: ", cubed_number)
... 
>>> main()
Enter a number: 23
Traceback (most recent call last):
  File "<pyshell#23>", line 1, in <module>
    main()
  File "<pyshell#22>", line 3, in main
    cubed_number =  cube(number)
NameError: name 'cube' is not defined
Run Code Online (Sandbox Code Playgroud)

班级呢?

Python 处理类定义与函数定义有点不同。

当 Python 遇到类定义时,它会为该类创建一个代码对象,就像函数一样。但是,Python 还允许类具有在类定义期间执行的命名空间。Python 不会等待执行类命名空间,因为定义的任何变量都应该属于该类。因此,在类名称空间内使用的任何名称都必须定义为在类定义期间使用。

类定义文档涉及这一点

然后使用新创建的本地命名空间和原始全局命名空间在新的执行框架中执行该类的套件(请参阅命名和绑定)。(通常,套件主要包含函数定义。)当类的套件完成执行时,它的执行帧将被丢弃,但它的本地命名空间会被保存。

但是,这不适用于方法。Python 将方法中的未定义名称视为函数,并允许您在定义方法时使用它们:

>>> class Class:
...     def method(self):
...         return var
...
>>> var = 10
>>> cls = Class()
>>> cls.method()
10
Run Code Online (Sandbox Code Playgroud)

  • 您显然反汇编了一个 **不同的** 函数,其中 `a` 和 `b` 是 *locals*,因为使用了 `LOAD_FAST`。但是然后你谈到全局变量:*Python 在字节码中编码了两个 `LOAD_GLOBAL` 指令*。请做校对。:-) 另外,`dis(func)` 可以工作,这里不需要访问 `__code__`。 (2认同)