如何在Python中解析对变量的引用

该消息有很多示例,但是有点长,但是我希望将帮助我和其他人更好地掌握变量的全部内容和Python 2.7中的属性查找。

我正在使用PEP 227的条款([http://www.python.org/dev/peps/pep-0227/)]用于代码块(例如模块,类定义,函数定义等)和变量绑定(例如赋值,参数声明,类和函数声明,for循环等)

我将术语变量用于可以不使用点和需要用对象限定的名称的属性名称(例如obj.x作为对象obj的属性x)。

Python中所有代码块都有三个作用域,但有以下功能:

  • 本地
  • 全球
  • 内建

Python中有四个仅用于功能的块(根据(PEP 227):

  • 本地
  • 封闭功能
  • 全球
  • 内建

将变量绑定到块中并在其中查找的规则是非常简单:

  • 变量与块中对象的任何绑定都会使该变量局部变量,除非变量声明为全局变量(在该变量中变量属于全局范围)
  • 使用规则LGB(本地,全局的,内置的)所有块,但功能
  • 使用规则LEGB(本地,封闭的,全局的,内置的)。

让我知道一些验证此规则的示例,并展示了许多特别案例。 对于每个示例,我都会给出自己的理解。 请如果我错了,请纠正我。 对于最后一个示例,我不了解结果。

范例1:

x = "x in module"
class A():
    print "A: "  + x                    #x in module
    x = "x in class A"
    print locals()
    class B():
        print "B: " + x                 #x in module
        x = "x in class B"
        print locals()
        def f(self):
            print "f: " + x             #x in module
            self.x = "self.x in f"
            print x, self.x
            print locals()

>>>A.B().f()
A: x in module
{'x': 'x in class A', '__module__': '__main__'}
B: x in module
{'x': 'x in class B', '__module__': '__main__'}
f: x in module
x in module self.x in f
{'self': <__main__.B instance at 0x00000000026FC9C8>}

类(规则LGB)没有嵌套作用域,如果不使用,则类无法访问该类的属性限定名称(在此示例中为self.x)。 这在以下内容中有很好的描述PEP227。

范例2:

z = "z in module"
def f():
    z = "z in f()"
    class C():
        z = "z in C"
        def g(self):
            print z
            print C.z
    C().g()
f()
>>> 
z in f()
z in C

这里使用LEGB规则查找函数中的变量,但是如果路径中有一个类,则跳过类参数。 又是在这里,这就是PEP 227正在解释的内容。

范例3:

var = 0
def func():
    print var
    var = 1
>>> func()

Traceback (most recent call last):
  File "<pyshell#102>", line 1, in <module>
func()
  File "C:/Users/aa/Desktop/test2.py", line 25, in func
print var
UnboundLocalError: local variable 'var' referenced before assignment

我们期望使用诸如python这样的动态语言来处理一切动态解决。 但这不是函数的情况。 本地变量在编译时确定。 PEP 227和[http://docs.python.org/2.7/reference/executionmodel.html]描述了这一点这样的行为

“如果名称绑定操作发生在代码块内的任何地方,块中名称的使用被视为对当前块。”

示例4:

x = "x in module"
class A():
    print "A: " + x
    x = "x in A"
    print "A: " + x
    print locals()
    del x
    print locals()
    print "A: " + x
>>> 
A: x in module
A: x in A
{'x': 'x in A', '__module__': '__main__'}
{'__module__': '__main__'}
A: x in module

但是我们在这里看到,PEP227中的此声明“如果名称绑定该操作在代码块内的任何位置进行,使用该名称的所有方式该块中的内容被视为对当前块的引用。” is当代码块为类时出错。 而且,对于班级,似乎本地名称绑定不是在编译时进行的,而是在使用类名称空间执行。 在这方面,PEP227和Python文档中的执行模型具有误导性,并且有些零件错了。

范例5:

x = 'x in module'
def  f2():
    x = 'x in f2'
    def myfunc():
        x = 'x in myfunc'
        class MyClass(object):
            x = x
            print x
        return MyClass
    myfunc()
f2()
>>> 
x in module

我对这段代码的理解如下。 指令x = x首先查找表达式右手x所指的对象至。 在这种情况下,将在类中本地查找对象,然后遵循LGB规则,它将在全局范围内查找字符串“ x in module”。 然后,MyClass的局部属性x为在类字典中创建并指向字符串对象。

范例6:

现在这是一个我无法解释的例子。它与示例5非常接近,我只是在更改本地MyClass从x到y的属性。

x = 'x in module'
def  f2():
    x = 'x in f2'
    def myfunc():
        x = 'x in myfunc'
        class MyClass(object):
            y = x
            print y
        return MyClass
    myfunc()
f2()
>>>
x in myfunc

在这种情况下,为什么要在MyClass中查找x引用最内在的功能?

3个解决方案
21 votes

在理想的世界中,您将是对的,发现的某些矛盾之处将是错误的。 但是,CPython已经优化了一些方案,特别是局部函数。 这些优化以及编译器和评估循环的交互方式以及历史先例,导致了混乱。

Python将代码翻译成字节码,然后由解释器循环解释。 用于访问名称的“常规”操作码是274692823524101414272,它像在字典中一样查找变量名。 LOAD_NAME首先将查找名称为本地名称,如果失败,则查找全局名称。 当找不到名称时,A.x会引发x异常。

对于嵌套作用域,使用闭包可实现在当前作用域之外查找名称。 如果未分配名称但在嵌套(非全局)范围内可用,则将这些值作为闭包处理。 这是必需的,因为父作用域可以在不同时间为给定名称保存不同的值。 对父函数的两次调用可能导致不同的闭包值。 因此,Python具有针对这种情况的x = xLOAD_NAMEA.x操作码; 前两个操作码用于加载和创建嵌套作用域的闭包,并且x将在嵌套作用域需要时加载关闭值。

现在x = x相对较慢; 它将查询两个字典,这意味着它必须首先对键进行哈希处理并运行一些相等性测试(如果名称不是内部的)。 如果名称不是本地名称,则必须为全局名称再次这样做。 对于函数,可能会被调用成千上万次,这可能会很快变得乏味。 因此,函数本机具有特殊的操作码。 加载本地名称由LOAD_NAME实现,它通过索引在特殊的本地名称数组中查找本地变量。 这要快得多,但是确实需要编译器首先查看名称是否是本地名称而不是全局名称。 为了仍然能够查找全局名称,使用了另一个操作码A.x。 编译器针对这种情况显式优化以生成特殊的操作码。 当名称尚无值时,x将引发x异常。

另一方面,尽管类定义主体被当作函数来对待,但它们并未获得此优化步骤。 类定义并不意味着经常被调用。 大多数模块在导入时都会创建一次类。 嵌套时,类作用域也不计算在内,因此规则更简单。 结果,当您稍微混合一下作用域时,类定义主体就不会像函数那样起作用。

因此,对于非功能范围,x = xLOAD_NAME用于局部变量和全局变量,以及分别用于闭包。 对于功能,请使用A.xxx

请注意,Python执行2746928235241041014272行后即会立即执行类主体! 因此,在示例1中,在执行x(即导入模块)后,立即执行了x中的LOAD_NAME。 在示例2中,直到调用274692823524101414277才执行x

让我们来看一下您的示例:

  1. 您已经在类LOAD_NAME中嵌套了类x = x。类主体不会形成嵌套作用域,因此,即使在执行类x时执行了A.x类主体,编译器仍将使用x查找LOAD_NAMEA.B().f()是一个函数(绑定) (例如B()实例),因此它使用LOAD_GLOBAL加载x。在这里我们将忽略属性访问,这是一个定义非常好的名称模式。

  2. 这里x = x在类范围内,因此函数LOAD_NAME将跳过A.x范围,而是使用x来查看x范围。

  3. 此处,编译器确定x = x是本地变量,因为您在范围内对其进行了分配。 功能已经过优化,因此使用LOAD_NAME查找本地,并引发异常。

  4. 现在事情变得有些奇怪了。 x = x在类范围内执行,因此正在使用LOAD_NAME。 从范围的本地字典中删除了A.x,因此对x的第二次访问将导致找到全局x;而是 LOAD_NAME首先在本地查找,然后在本地找不到,然后回退到全局查找。

    是的,这看起来与文档不一致。 Python(语言)和CPython(实现)在这里有些冲突。 但是,您正在超越动态语言所可能实现的范围。 检查x = x是否应为LOAD_NAME中的本地对象是可以的,但是对于大多数开发人员永远不会遇到的极端情况,它会花费宝贵的执行时间。

  5. 现在您在混淆编译器。 您在类范围中使用了x = x,因此,您正在使用范围之外的名称设置本地语言。 编译器发现x是此处的本地(您为其分配),因此它从不认为它也可以是作用域名称。 编译器在此范围内对LOAD_NAME的所有引用均使用x,因为这不是经过优化的功能体。

    执行类定义时,x首先要求您查找x,因此它使用x进行查找。 没有定义LOAD_NAME,没有找到UnboundLocalError,因此找到了全局LOAD_FAST。 结果值存储为本地值,该值也恰好名为UnboundLocalErrorprint x再次使用LOAD_NAME,现在找到新的本地x值。

  6. 在这里,您没有混淆编译器。 您正在创建本地xx不是本地的,因此编译器将其识别为父函数LOAD_NAME的作用域名称。使用LOAD_FAST从闭合中查找UnboundLocalError,并将其存储在UnboundLocalError中。

您可能会发现5和6之间的混淆是一个错误,尽管我认为这个错误不值得修复。 它的确是这样归档的,请参见Python bug跟踪器中的问题532860,它已经存在了10多年了。

对于示例5中的第一次分配,即使x也是本地的,编译器也可以检查范围内的名称x,或者LOAD_NAME可以检查名称是否真的是本地,如果找不到本地,则抛出UnboundLocalError。 ,以牺牲更多性能为代价。 如果这在功能范围内,将使用LOAD_FAST作为示例5,并立即抛出UnboundLocalError

但是,如所引用的错误所示,由于历史原因,该行为得以保留。 如果修复了此错误,今天可能会有代码破译。

Martijn Pieters answered 2020-02-14T08:04:37Z
18 votes

用两个词来说,示例5和示例6之间的区别在于,在示例5中,变量x = x也被分配给了同一范围,而在示例6中则没有。这触发了可以由历史原因理解的差异。

这引发了UnboundLocalError:

x = "foo"
def f():
    print x
    x = 5
f()

而不是打印“ foo”。 即使一开始看起来很奇怪,这还是有道理的:函数f()在本地定义了变量2746927302981452452800,即使它在打印之后也是如此,因此在同一函数中对x = x的任何引用都必须是对该局部变量的引用 。 至少这是有道理的,如果您错误地在本地重用了全局变量的名称,并试图同时使用全局变量和局部变量,则可以避免意外的意外。 这是一个好主意,因为这意味着我们可以通过查看变量来静态知道它意味着什么。 例如,我们知道x在这里引用局部变量(因此可能引发UnboundLocalError):

x = "foo"
def f():
    if some_condition:
        x = 42
    print x
f()

现在,此规则不适用于类级范围:在那里,我们希望像x = x这样的表达式起作用,将全局变量x = x捕获到类级范围中。 这意味着类级别的作用域不遵循上述基本规则:我们不知道该作用域中的2746927302981452452802是引用某个外部变量还是引用本地定义的x-例如:

class X:
    x = x     # we want to read the global x and assign it locally
    bar = x   # but here we want to read the local x of the previous line

class Y:
    if some_condition:
        x = 42
    print x     # may refer to either the local x, or some global x

class Z:
    for i in range(2):
        print x    # prints the global x the 1st time, and 42 the 2nd time
        x = 42

因此,在类范围内,使用了不同的规则:通常会引发UnboundLocalError的地方---仅在这种情况下---而是在模块全局变量中查找。 仅此而已:它不遵循嵌套作用域链。

为什么不? 实际上,我怀疑有一个更好的解释是“出于历史原因”。 用更多的技术术语,它可以认为变量x = x既是在类范围中本地定义的(因为已被分配给它),又应从父范围中作为词法嵌套的变量传递(因为已被读取)。 可以通过使用不同于x = x的字节码来实现它,该字节码在本地作用域中查找,如果找不到嵌套的作用域引用,则转而使用嵌套的作用域引用。

编辑:谢谢wilberforce对[http://bugs.python.org/issue532860的引用。]如果我们认为毕竟应该将其固定( 错误报告考虑终止对x = x的支持,但由于担心破坏太多现有代码而被关闭;相反,我的建议是在更多情况下使x = x工作)。 否则我可能会错过另一个优点...

EDIT2:似乎CPython在当前的3.4主干中确实做到了这一点:[http://bugs.python.org/issue17853] ... ...还是不? 他们介绍字节码的原因略有不同,因此没有系统地使用它。

Armin Rigo answered 2020-02-14T08:02:46Z
6 votes

长话短说,这是Python范围界定的一个极端案例,它有点不一致,但是必须保留以实现向后兼容性(因为尚不清楚正确的答案应该是什么)。 在实施PEP 227时,您可以在Python邮件列表上看到很多有关此问题的原始讨论,并且在此问题已解决的bug中有一些讨论。

我们可以使用self.模块弄清楚为什么存在差异的原因,该模块使我们可以查看代码对象内部,以查看已将代码片段编译为的字节码。 我使用的是python 2.6,因此其细节可能略有不同-但我看到的是相同的行为,因此我认为它可能已经接近2.7。

初始化每个嵌套self.的代码都存在于一个代码对象中,您可以通过顶级函数的属性来获得该对象。 (我将示例5和示例6的功能分别重命名为LOAD_FASTx。)

该代码对象具有self.元组,其中包含LOAD_FAST代码对象,该对象又具有在创建x时运行的代码:

In [20]: f1.func_code.co_consts
Out[20]: (None,
 'x in f2',
 <code object myfunc at 0x1773e40, file "<ipython-input-3-6d9550a9ea41>", line 4>)
In [21]: myfunc1_code = f1.func_code.co_consts[2]
In [22]: MyClass1_code = myfunc1_code.co_consts[3]
In [23]: myfunc2_code = f2.func_code.co_consts[2]
In [24]: MyClass2_code = myfunc2_code.co_consts[3]

然后您可以使用self.在字节码中看到它们之间的区别:

In [25]: from dis import dis
In [26]: dis(MyClass1_code)
  6           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  7           6 LOAD_NAME                2 (x)
              9 STORE_NAME               2 (x)

  8          12 LOAD_NAME                2 (x)
             15 PRINT_ITEM          
             16 PRINT_NEWLINE       
             17 LOAD_LOCALS         
             18 RETURN_VALUE        

In [27]: dis(MyClass2_code)
  6           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  7           6 LOAD_DEREF               0 (x)
              9 STORE_NAME               2 (y)

  8          12 LOAD_NAME                2 (y)
             15 PRINT_ITEM          
             16 PRINT_NEWLINE       
             17 LOAD_LOCALS         
             18 RETURN_VALUE        

因此,唯一的区别是,在self.中,使用x op加载了LOAD_FAST,而在f2中,则使用MyClass加载了。y = x在封闭范围内查找名称,因此它在myfunc中得到'x'。 f2不遵循嵌套作用域-因为它看不到在f1LOAD_FAST中绑定的x = x名称,它获取模块级绑定。

然后的问题是,为什么将self.的两个版本的代码编译为两个不同的操作码? 在LOAD_FAST中,绑定在类范围中隐藏了x,而在f2中,它绑定了新名称。 如果MyClass范围是嵌套函数而不是类,则f2中的y = x行将被编译为相同,但是f1中的x = x将是LOAD_FAST-这是因为编译器会知道函数中绑定了x,因此应该 使用LOAD_FAST来检索局部变量。 当它被调用时将失败,并显示UnboundLocalError

In [28]:  x = 'x in module'
def  f3():
    x = 'x in f2'
    def myfunc():
        x = 'x in myfunc'
        def MyFunc():
            x = x
            print x
        return MyFunc()
    myfunc()
f3()
---------------------------------------------------------------------------
Traceback (most recent call last)
<ipython-input-29-9f04105d64cc> in <module>()
      9         return MyFunc()
     10     myfunc()
---> 11 f3()

<ipython-input-29-9f04105d64cc> in f3()
      8             print x
      9         return MyFunc()
---> 10     myfunc()
     11 f3()

<ipython-input-29-9f04105d64cc> in myfunc()
      7             x = x
      8             print x
----> 9         return MyFunc()
     10     myfunc()
     11 f3()

<ipython-input-29-9f04105d64cc> in MyFunc()
      5         x = 'x in myfunc'
      6         def MyFunc():
----> 7             x = x
      8             print x
      9         return MyFunc()

UnboundLocalError: local variable 'x' referenced before assignment

之所以失败,是因为self.函数然后使用LOAD_FAST

In [31]: myfunc_code = f3.func_code.co_consts[2]
MyFunc_code = myfunc_code.co_consts[2]
In [33]: dis(MyFunc_code)
  7           0 LOAD_FAST                0 (x)
              3 STORE_FAST               0 (x)

  8           6 LOAD_FAST                0 (x)
              9 PRINT_ITEM          
             10 PRINT_NEWLINE       
             11 LOAD_CONST               0 (None)
             14 RETURN_VALUE        

(顺便说一句,作用域与类主体中的代码以及函数中的代码交互的方式应该有所不同,这并不奇怪,因为在类级别上的绑定在方法中不可用,因此您可以说出这一点- 方法范围不会像嵌套函数那样嵌套在类范围内。您必须通过类或使用self.(如果没有实例级绑定也将退回到类)来显式地访问它们 )。

babbageclunk answered 2020-02-14T08:10:31Z
translate from https://stackoverflow.com:/questions/20246523/how-references-to-variables-are-resolved-in-python