代码改变世界

初学者教程之命名空间,范围解析及LEDB规则

2016-11-22 10:31  linkxu  阅读(250)  评论(0编辑  收藏  举报

2014年5月12日

Sebastian Raschka编写

    这是一篇关于采用LEGB规则实现Python变量命名空间及范围解析的简短教程。下面章节将会提供简短的可以说明问题的示例代码块来简要阐述问题。您可以简单的从头至尾阅读本教程,但我鼓励您去执行这些代码段。你可以复制粘贴这些代码段,但是为了方便您也可以下载IPython笔记

章节

• 章节

• 目标

• 命名空间和范围介绍

o 命名空间

o 范围

o 提示:

o 通过LEGB规则解析变量名的范围

•1. LG-本地和全局范围

o 原因:

o 原因:

•2. LEG – 局部、封闭和全局范围

o 原因:

•3. LEGB – 局部、封闭、全局、内置

o 原因:

• 自由练习

• 结论

o 经验法则

o 解决方案

o 警告:对于循环变量“leaking”加入全局命名空间

目标

  •命名空间和范围:Python是从哪里寻找变量名?

  •我们可以在同一时间定义或重用多个对象的变量名吗?

  •Python是通过哪种方式为变量名搜索不同的命名空间的呢?

命名空间和范围介绍

命名空间

    大致来说,命名空间只是将名称映射到对象的容器。正如你可能已经听过,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, ...}

    例如,当我们调用一个循环或定义一个函数时,它将创建自己的命名空间。命名空间也有不同的层次(所谓的“范围”),我们将在下一节中更详细地讨论。

范围

    在上述章节中,我们已经了解到命名空间可以独立存在,彼此和他们的结构在一定的层次,这给我们带来了“范围”。Python 中的“范围”在我们为“命名对象”映射搜索命名空间时定义了“层级”。

    例如,让我们思考一下下面的这段代码:

1.png

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

       •foo_namespace = {'i':object_3, ...}

       •global_namespace = {'i':object_1, 'name_b':object_2, ...}

     所以,如果我们想要打印变量i的值,Python是如何知道应该查找哪个命名空间呢?这个地方就需要Python的LEGB规则来实现了,我们将在下个章节讨论。

提示:

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

2.png

通过LEGB规则解析变量名范围

    我们已经看到,多个命名空间可以彼此独立存在,它们可以在不同的层次水平包含相同的变量名。“范围”定义在哪个层次,Python为其相关对象搜索了一个特定的“变量名”。现在,下一个问题是:Python是采用什么方式在它找到对象名称的映射之前搜索命名空间的不同级别呢?

     答案是:Python使用LEGB规则,即

     局部->封闭->全局->内置

    箭头应该指向命名空间层次结构搜索顺序的方向。

      •局部可以在函数内部或类方法中,例如。

       •封闭可以是它的enclosing方法,例如,一个方法被包含在另一个方法中。

       •全局是指执行脚本的最高级别,以及

       •内置是Python自己保留的特殊名称。

    所以,如果一个特定的名称:对象映射不能在本地命名空间中找到,下一步封闭范围的命名空间将会被搜索。如果在封闭范围的搜索也是不成功的,Python将会到全局命名空间去搜索,最终,它将搜索内置命名空间(附注:如果名称在任何的命名空间都找不到,系统将报告NameError)。

注:

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

3.png

   (这也是为什么我们在通过“from a module import *”时必须注意,因为在加载变量名到全局命名空间时很可能会覆盖已经存在的变量名。)

1111.png

1. LG – 局部和全局范围

例 1.1

作为热身练习,让我们先忘记在LEGB规则中封闭(E)和内置(B)的范围,来看一看LG----局部和全局范围。

    下面的代码将打印什么呢?

4.png

原因:

    我们首先调用a_func(),这应该是打印a_var值。根据LEGB规则,这个方法将会首先在局部范围(L)查看是否a_var是被定义的。因为a_func()没有定义自己a_var,它会向上一级全局范围(G)查找,直到a_var之前定义的范围。

例 1.2

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

你能猜到下面的代码将会产生什么?

5.png

原因:

    当我们调用a_func()时,首先它会在局部范围(L)中查找a_var,因为a_var已经在局部范围a_func中定义,它的赋值local varible就会被打印。注意这不会影响在不同范围的全局变量。

    然后,它也在可能会修改全局的,例如,如果我们在重新赋值时使用全局关键字。下面的例子将会说明:

6.png

    但是我们必须小心这个顺序:如果我们没有明确的告诉Python我们想要使用全局范围来尝试修改变量值就很容易出现UnboundLocalError错误。(记住,赋值操作先执行的是右边的操作):

7.png

2. LEG – 局部, 封闭和全局范围

    现在,让我们来介绍一下封闭范围(E)的概念。按照“局部->封闭->全局”的顺序,你能猜出下面的代码将打印什么吗?

例 2.1

8.png

原因:

    让我们快速概括我们在做什么: 我们调用outer(), 其变量a_var在局部被定义(a_var在全局存在)。接下来, outer()函数调用同样定义了名字为a_var的变量的inner()函数。Inner()中的print()函数在它的范围层级结构之前先在局部范围中查找(L->E),因此它打印了在局部范围内分配的值。

类似于我们已经在前面章节中看到的global关键字的概念,我们可以使用包含在内置函数的关键字nonlocal来明确地访问外部(封闭)范围的变量,以修改它的变量值。

    注意nonlocal关键字已经添加到了Python 3.x, 在Python 2.x中还没有实现。

9.png

 

3. LEGB – 局部,封闭,全局,内置

     总结LEGB规则,让我们进入内置范围。在这里,我们定义了我们“own”的长度函数,这恰好为内建的len()函数具有相同的名称。如果我们执行了下面的代码,结果是什么?

例 3

10.png

原因:

    因为相同的名称可用于映射不同的对象,只要这个名字在不同的名字空间并且没有重用的名字来定义自己的函数len长度的问题(这只是用于演示,不推荐)。当我们回顾Python的L - >E> G > B层次,函数a_func()发现在它尝试搜索内置(B)命名空间之前len()已经在全局范围(G)。

自由练习

    现在,在我们经过几次联系之后,让我们快速检查我们掌握到哪里?因此,多一次的练习:下面的代码将打印出什么呢?

11.png

 

结论

    我希望通过这个简短的教程可以有助于理解Python利用LEGB规则来进行范围解析顺序的基本的概念。我想鼓励你(作一次小的自由练习)明天再去看看代码段,再检查一下你是否能正确的预测出它们的结果。

经验法则

    在实践中,在函数范围修改全局变量通常是一个坏想法,因为它往往会引起一些混乱和奇怪的错误,并且很难调试。

如果你想通过函数修改一个全局变量,建议你通过参数传入该全局变量并将返回值重新指定给全局变量。

例如:

12.png

 

解决方案

    为上防止你无意的破坏,我已经用二进制格式编写了解决方案。 为了显示这些字符表示,你只需要执行下面的几行代码:

13.png

 

警告:对于循环变量“leaking”加入全局命名空间

    和其他的变成语言相反,for-loops将使用他们存在的范围并留下他们已经定义的循环变量。

14.png

这也使用于如果我们明确在全局命名空间之前定义for-loop变量!在这种情况下,它会重新绑定存在的变量。

15.png

    无论如何,在Python 3.x中,我们可以使用闭包来防止循环变量将其分割到全局命名空间中。

16.png

    为什么我提到“Python 3.x”?  因为当问题发生时,相同的代码在Python 2.x的执行结果是这样的:

17.png

英文原文:http://sebastianraschka.com/Articles/2014_python_scope_and_namespaces.html
译者:linkxu1989