Python命名空间,范​​围解析和LEGB规则的初学者指南

原文链接: A Beginner’s Guide to Python’s Namespaces, Scope Resolution, and the LEGB Rule

May 12, 2014 by Sebastian Raschka

这是有关使用LEGB规则、Python命名空间和变量名称作用域解析的简短教程。以下各节将提供简短的示例代码块,其中会描述问题,然后进行简短说明。您可以简单地从头到尾阅读本教程,但是我鼓励您执行这些代码片段-您可以复制并粘贴它们,或者为了方便起见,只需下载此IPython Notebook

目标

命名空间和作用域-Python在哪里寻找变量名?

我们可以同时为多个对象定义/重用变量名吗?

Python按照什么顺序在不同的名称空间中搜索变量名?

命名空间和范围介绍

命名空间(namespace)

粗略地说,名称命名空间(名称空间)只是用于将名称映射到对象的容器。您可能已经听说过,Python中的所有内容(文字,列表,字典,函数,类等)都是一个对象。

这种“名称到对象”的映射使我们可以通过分配给它的名称来访问对象。例如,如果我们通过进行简单的字符串分配,a_string = “Hello string”,则会创建对该"Hello string"对象的引用,此后我们就可以通过其变量名a_string访问对象。

我们可以将命名空间描述为Python字典结构,其中字典键代表名称,而字典值代表对象本身(这也是当前在Python中实现命名空间的方式),例如,

a_namespace = {'name_a':object_1,'name_b':object_2,...}

现在,棘手的部分是我们在Python中拥有多个独立的名称空间,并且名称可以用于不同的名称空间(仅对象是唯一的,例如:

a_namespace = {'name_a':object_1,'name_b':object_2,...}
b_namespace = {'name_a':object_3,'name_b':object_4,...}

例如,每次调用一个for-loop或定义一个函数时,它将创建自己的名称空间。命名空间也具有不同的层次结构级别(所谓的“作用域”),我们将在下一部分中对其进行详细讨论。

作用域(scope)

在上一节中,我们了解到名称空间可以彼此独立存在,并且它们被构建为一定的层级结构,这让我们了解了“作用域”的概念。Python中的“作用域”定义了“层次结构级别”,在该层次结构中,我们在命名空间中搜索某些“名称到对象”的映射。

例如,让我们考虑以下代码:

i = 1

def foo():
    i = 5
    print(i, 'in foo()')

print(i, 'global')

foo()
1 global
5 in foo()

在这里,我们定义了两次变量名i,其中一次是在foo函数内。

foo_namespace = {‘i’:object_3, …}

global_namespace = {‘i’:object_1, ‘name_b’:object_2, …}

那么,如果我们要打印变量的值,Python如何知道它搜索哪个名称空间中的i?这就是Python的LEGB规则发挥作用的地方,我们将在下一部分中进行讨论。

提示(Tip):

如果要打印出全局变量和局部变量的字典映射,可以使用函数global()和local()

#print(globals()) # prints global namespace
#print(locals()) # prints local namespace

glob = 1

def foo():
    loc = 5
    print('loc in foo():', 'loc' in locals())

foo()
print('loc in global:', 'loc' in globals())    
print('glob in global:', 'foo' in globals())
loc in foo(): True
loc in global: False
glob in global: True

通过LEGB规则解析变量名称的作用范围。

我们已经看到,多个名称空间可以彼此独立存在,并且它们可以在不同的层次结构级别上包含相同的变量名称。“作用域”(scope)定义Python在哪个层次级别上为其关联对象搜索特定的“变量名”。现在,下一个问题是:“ Python在找到名称到对象的映射之前,按什么顺序搜索不同级别的名称空间?”

要回答的是:它使用LEGB规则,表示为

Local->Enclosed->Global->Built-in,

箭头应表示名称空间层次结构搜索顺序的方向。

例如,Local可以在函数或类方法中(即局部作用域)。Enclosed可以是它的enclosing函数,比如一个函数被包裹在另一个函数的内部(即闭包)。Global是指执行脚本本身的最高级别(即全局),Built-in是Python为其保留的特殊名称(即内建对象)。

因此,如果在local局部命名空间中找不到特定的name:object映射,则接下来将搜索Enclosed闭包范围的命名空间。如果在闭包范围内搜索也不成功,Python将移至Global全局命名空间,最终它将搜索Built-in内置名称空间(注意:如果在任何名称空间中都找不到名称,则会引发一个NameError)。

注意事项(Note):

命名空间也可以进一步嵌套,例如,如果我们导入模块,或者正在定义新类。在这些情况下,我们必须使用前缀来访问这些嵌套的名称空间。让我在以下代码块中说明此概念:

import numpy
import math
import scipy

print(math.pi, 'from the math module')
print(numpy.pi, 'from the numpy package')
print(scipy.pi, 'from the scipy package')
3.141592653589793 from the math module
3.141592653589793 from the numpy package
3.141592653589793 from the scipy package

(这也是为什么我们必须通过“ from a_module import *” 导入模块时要小心的原因,因为它将变量名加载到全局命名空间中,并可能覆盖已经存在的变量名)

LEGB图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEAaWwEH-1570699136782)(/static/imgupload/scope_resolution_1-0x18f4140.png)]

1. LG-局部(Local)和全局(Global)作用域

例1.1

作为一个热身练习,让我们首先忘记LEGB规则中的Enclosed(E)和Built-in(B)作用域,只看LG-Local和Global作用域。

以下代码显示什么?

a_var = 'global variable'

def a_func():
    print(a_var, '[ a_var inside a_func() ]')

a_func()
print(a_var, '[ a_var outside a_func() ]')

a)

raises an error

b)

global value [ a_var outside a_func() ]

C)

global value [ a_var inside a_func() ]  
global value [ a_var outside a_func() ]

原因如下:

我们先调用了a_func(),它应该打印a_var的值。根据LEGB规则,如果在该函数中定义了a_var,则该函数将首先查看其自身的局部作用域(L)。由于函数a_func()没有在其内部定义a_var,因此它将搜索更高一级的全局作用域(在全局作用域,a_var被定义了)

例1.2

现在,让我们在全局和局部范围内定义变量a_var。

您能猜出下面的代码会输出什么吗?

a_var = 'global value'

def a_func():
    a_var = 'local value'
    print(a_var, '[ a_var inside a_func() ]')

a_func()
print(a_var, '[ a_var outside a_func() ]')

a)

raises an error

b)

local value [ a_var inside a_func() ]
global value [ a_var outside a_func() ]

C)

global value [ a_var inside a_func() ]  
global value [ a_var outside a_func() ]

原因如下:

当我们调用时a_func(),它将首先在其局部范围(L)中查找a_var,因为a_var是在a_func的局部范围中定义的,因此会打印其分配的值local variable。请注意,这不会影响位于不同作用域(scope)的全局变量。

但是,如果我们使用global关键字,也可以通过为全局值重新分配一个值来修改全局值,如以下示例所示:

a_var = 'global value'

def a_func():
    global a_var
    a_var = 'local value'
    print(a_var, '[ a_var inside a_func() ]')

print(a_var, '[ a_var outside a_func() ]')
a_func()
print(a_var, '[ a_var outside a_func() ]')
global value [ a_var outside a_func() ]
local value [ a_var inside a_func() ]
local value [ a_var outside a_func() ]

但是我们必须注意顺序:如果不明确告诉Python我们要使用全局范围并尝试修改变量的值,则很容易引发一个UnboundLocalError(记住,赋值操作的右边是首先执行):

a_var = 1

def a_func():
    a_var = a_var + 1
    print(a_var, '[ a_var inside a_func() ]')

print(a_var, '[ a_var outside a_func() ]')
a_func()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)

<ipython-input-4-a6cdd0ee9a55> in <module>()
      6
      7 print(a_var, '[ a_var outside a_func() ]')
----> 8 a_func()


<ipython-input-4-a6cdd0ee9a55> in a_func()
      2
      3 def a_func():
----> 4     a_var = a_var + 1
      5     print(a_var, '[ a_var inside a_func() ]')
      6


UnboundLocalError: local variable 'a_var' referenced before assignment


1 [ a_var outside a_func() ]

2. LEG-局部(Local),闭包(Enclosed)和全局(Global)作用域

现在,让我们介绍闭包(enclosed)(E)作用域的概念。按照“Local->Enclosed->Global”的顺序,您能猜出下面的代码将输出什么?

范例2.1

a_var = 'global value'

def outer():
    a_var = 'enclosed value'

    def inner():
        a_var = 'local value'
        print(a_var)

    inner()

outer()

a)

global value

b)

enclosed value

C)

local value

原因如下:

让我们快速地概括一下我们刚刚做的事情:我们调用了outer()函数,该函数在本地定义了a_var变量(全局范围内也存在a_var变量)。接下来,outer()调用了函数inner(),而该函数又定义了一个名称为a_var的变量。在继续往上一层次查找变量前,该print()函数会先查找在局部范围内的变量(L->E),因此,会先打印在局部范围内分配的值。

与在上一节中看到的关键字global的概念类似,我们可以在inner函数中使用关键字nonlocal显式访问outer(闭包)范围的变量,以修改其值。

请注意,该nonlocal关键字是在Python 3.x中添加的,尚未在Python 2.x中实现。

a_var = 'global value'

def outer():
       a_var = 'local value'
       print('outer before:', a_var)
       def inner():
           nonlocal a_var
           a_var = 'inner value'
           print('in inner():', a_var)
       inner()
       print("outer after:", a_var)
outer()
outer before: local value
in inner(): inner value
outer after: inner value

3. LEGB-局部(Local),闭包(Enclosed),全局(Global),内置(Built-in)

为了总结LEGB规则,让我们看看内置范围。在这里,我们将定义“自己的”长度函数,该函数恰好与内置len()函数同名。如果我们执行以下代码,您期望得到什么结果?

例子3

a_var = 'global variable'

def len(in_var):
    print('called my len() function')
    l = 0
    for i in in_var:
        l += 1
    return l

def a_func(in_var):
    len_in_var = len(in_var)
    print('Input variable is of length', len_in_var)

a_func('Hello, World!')

a)

raises an error (conflict with in-built `len()` function)

b)

called my len() function
Input variable is of length 13

C)

Input variable is of length 13

原因如下:

由于可以使用完全相同的名称将名称映射到不同的对象(只要名称在不同的名称空间中),因此重复使用len名称来定义我们自己的长度函数没有任何问题(这只是出于演示目的,而不是建议这样做)。正如在Python的L - >E - > G - > B层次关系上那样,函数a_func()在尝试搜索内置(B)命名空间之前,就已经发现len()已经在全局范围(G)内了。

自我评估练习

现在,在我们进行了几次练习之后,让我们快速检查一下自己的水平。所以,又一次:下面的代码将输出什么?

a = 'global'

def outer():

    def len(in_var):
        print('called my len() function: ', end="")
        l = 0
        for i in in_var:
            l += 1
        return l

    a = 'local'

    def inner():
        global len
        nonlocal a
        a += ' variable'
    inner()
    print('a is', a)
    print(len(a))


outer()

print(len(a))
print('a is', a)

结论

我希望这段简短的教程对了解使用LEGB规则的Python范围解析顺序的基本概念有所帮助。我想鼓励您(作为一项自我评估练习)明天再次查看代码片段,并检查您是否可以正确预测其所有结果。

经验法则

在实践中,在函数范围内修改全局变量通常是一个坏主意,因为它通常是造成混乱和难以调试的奇怪错误的原因。
如果要通过函数修改全局变量,建议将其作为参数传递并重新分配返回值。

例如:

a_var = 2

def a_func(some_var):
    return 2**3

a_var = a_func(a_var)
print(a_var)
8

解决方案

为了防止意外损坏,我以二进制格式编写了解决方案。为了显示字符表示,您只需要执行以下代码行:

print('Example 1.1:', chr(int('01100011',2)))
print('Example 1.2:', chr(int('01100010',2)))
print('Example 2.1:', chr(int('01100011',2)))
print('Example 3.1:', chr(int('01100010',2)))
# Execute to run the self-assessment solution

sol = "000010100110111101110101011101000110010101110010001010"\
"0000101001001110100000101000001010011000010010000001101001011100110"\
"0100000011011000110111101100011011000010110110000100000011101100110"\
"0001011100100110100101100001011000100110110001100101000010100110001"\
"1011000010110110001101100011001010110010000100000011011010111100100"\
"1000000110110001100101011011100010100000101001001000000110011001110"\
"1010110111001100011011101000110100101101111011011100011101000100000"\
"0011000100110100000010100000101001100111011011000110111101100010011"\
"0000101101100001110100000101000001010001101100000101001100001001000"\
"0001101001011100110010000001100111011011000110111101100010011000010"\
"1101100"

sol_str =''.join(chr(int(sol[i:i+8], 2)) for i in range(0, len(sol), 8))
for line in sol_str.split('\n'):
    print(line)

警告:For循环变量“泄漏”到全局名称空间中

与某些其他编程语言相比,for-loops将使用它们存在的范围,并保留其定义的循环变量。

for a in range(5):
    if a == 4:
        print(a, '-> a in for-loop')
print(a, '-> a in global')
4 -> a in for-loop
4 -> a in global

如果我们for-loop之前在全局名称空间中显式定义了变量,这也适用!在这种情况下,它将重新绑定现有变量:

b = 1
for b in range(5):
    if b == 4:
        print(b, '-> b in for-loop')
print(b, '-> b in global')
4 -> b in for-loop
4 -> b in global

但是,在Python 3.x中,我们可以使用闭包来防止for循环变量切入全局名称空间。这是一个示例(在Python 3.4中执行):

i = 1
print([i for i in range(5)])
print(i, '-> i in global')
[0, 1, 2, 3, 4]
1 -> i in global

为什么我提到“ Python 3.x”?好吧,碰巧,在Python 2.x中执行的相同代码将打印:

4->i in global

这可以追溯到在Python 3.x中所做的更改,并在Python 3.0的新增功能中进行了如下描述:

列表理解不再支持语法形式[... for var in item1, item2, ...]。使用[... for var in (item1, item2, ...)]代替。还要注意,列表理解具有不同的语义:对于list()构造函数内部的生成器表达式,它们更接近语法糖,并且尤其是循环控制变量不再泄漏到周围的范围中。”

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值