美文网首页Python学习分享
Python 的作用域的相关规则

Python 的作用域的相关规则

作者: 烟雨丿丶蓝 | 来源:发表于2019-06-09 14:18 被阅读21次

    我并不喜欢 Python 的作用域的设计,但这门语言是如此流行,以至于很多时候你不得不去了解它. 本文试图比较全面地总结 Python 的作用域的相关规则. 若本文有错误之处,欢迎纠正.

    本文基于 Python 3.6.8.

    1、作用域划分

    Python 中作用域的划分大致以“块”为单位. 什么是“块”呢?主要是模块、函数体、类定义(还有一些其他情况,例如函数 eval() 和 exec() 的字符串参数等). 所以,在 if / while / for 等语句中引入的变量会污染整个“块”:

    def f():
    ... for i in range(10):
    ... pass
    ... print(i)
    ...
    f()
    9
    2、名字查找

    Python 的名字查找规则是符合直觉的:按照 local -> enclosing functions -> global -> built-in 的顺序由内而外查找,并选择最近的一个. 有一种说法叫 LEGB 规则(Local,Enclosing,Global,Built-in),可能是为了便于记忆,但我个人觉得直接按照直觉记忆即可.

    在 Python 中,名字通过“绑定操作”引入. 以下这些结构执行“绑定操作”:函数参数、import 语句、类定义、函数定义,以及赋值、for 循环、with 或 except 中引入的新变量. 另外,del 语句可以取消一个名字的绑定.

    如果在当前块中绑定一个名字,这个名字会被默认为是当前块中的,除非被声明为 nonlocal 或 global.

    然而,Python 不区分“通过赋值操作引入新变量”和“给已有的变量赋值”. 当我们试图在内层作用域中修改外层变量时,这个特性就会将问题搞复杂. 比如说:

    def f():
    ... a = 0
    ... def g():
    ... a = 1
    ... g()
    ... print(a)
    ...
    f()
    0
    在这里,g 中的语句 a = 1 实际上是在 g 中引入了一个新变量,所以外层的 a 并没有被改变. 要想改变外层的 a, 就需要使用 nonlocal 语句,它会在 enclosing functions 中由内而外查找相应的变量:

    def f():
    ... a = 0
    ... def g():
    ... nonlocal a
    ... a = 1
    ... g()
    ... print(a)
    ...
    f()
    1
    相应地,要想在内层作用域中改变全局变量,就要使用 global 语句,它会按照 global -> built-in 的顺序由内而外查找相应的变量.

    然而,在内层作用域中可以直接读取最近的外层 / 全局变量(只要当前作用域内没有重名的变量),而不需要 nonlocal / global 之类的语句:

    def f():
    ... a = 100
    ... def g():
    ... print(a)
    ... g()
    ...
    f()
    100
    注意,类定义也是一个名字空间,但它的影响范围不会延伸到方法中。所以,一个方法如果想要使用类变量或类中的其它方法,必须通过 self 参数:

    class A:
    ... a = 11
    ... def f(self):
    ... print(20)
    ... def g(self):
    ... print(self.a)
    ... self.f()

    def f():
    ... for i in range(10):
    ... pass
    ... print(i)
    ...
    >>> f()
    9
    2、名字查找

    Python 的名字查找规则是符合直觉的:按照 local -> enclosing functions -> global -> built-in 的顺序由内而外查找,并选择最近的一个. 有一种说法叫 LEGB 规则(Local,Enclosing,Global,Built-in),可能是为了便于记忆,但我个人觉得直接按照直觉记忆即可.

    在 Python 中,名字通过“绑定操作”引入. 以下这些结构执行“绑定操作”:函数参数、import 语句、类定义、函数定义,以及赋值、for 循环、with 或 except 中引入的新变量. 另外,del 语句可以取消一个名字的绑定.

    如果在当前块中绑定一个名字,这个名字会被默认为是当前块中的,除非被声明为 nonlocal 或 global.

    然而,Python 不区分“通过赋值操作引入新变量”和“给已有的变量赋值”. 当我们试图在内层作用域中修改外层变量时,这个特性就会将问题搞复杂. 比如说:

    >>> def f():
    ... a = 0
    ... def g():
    ... a = 1
    ... g()
    ... print(a)
    ...
    >>> f()
    0
    在这里,g 中的语句 a = 1 实际上是在 g 中引入了一个新变量,所以外层的 a 并没有被改变. 要想改变外层的 a, 就需要使用 nonlocal 语句,它会在 enclosing functions 中由内而外查找相应的变量:

    >>> def f():
    ... a = 0
    ... def g():
    ... nonlocal a
    ... a = 1
    ... g()
    ... print(a)
    ...
    >>> f()
    1
    相应地,要想在内层作用域中改变全局变量,就要使用 global 语句,它会按照 global -> built-in 的顺序由内而外查找相应的变量.

    然而,在内层作用域中可以直接读取最近的外层 / 全局变量(只要当前作用域内没有重名的变量),而不需要 nonlocal / global 之类的语句:

    >>> def f():
    ... a = 100
    ... def g():
    ... print(a)
    ... g()
    ...
    >>> f()
    100
    注意,类定义也是一个名字空间,但它的影响范围不会延伸到方法中。所以,一个方法如果想要使用类变量或类中的其它方法,必须通过 self 参数:

    >>> class A:
    ... a = 11
    ... def f(self):
    ... print(20)
    ... def g(self):
    ... print(self.a)
    ... self.f()
    ...
    x = A()
    x.g()
    11
    20
    3、名字绑定的影响是“前后双向”的

    如果你在一个“块”中的任何地方绑定一个名字,该“块”中所有对该名字的使用都会被当成是指向当前“块”的. 这意味着以下的代码会报“赋值前使用”的错,因为这里 print(a) 中的 a 被认为是之后的 for 循环中的 a,即使外层有一个 a 也无济于事:

    def f():
    ... a = 100
    ... def g():
    ... print(a)
    ... for a in range(10):
    ... pass
    ... g()
    ...
    f()
    Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "<stdin>", line 7, in f
    File "<stdin>", line 4, in g
    UnboundLocalError: local variable 'a' referenced before assignment
    这时候,只要将 for 循环中的变量改个名字,print(a) 中的 a 就会被正常的名字查找过程在外层找到:

    def f():
    ... a = 100
    ... def g():
    ... print(a)
    ... for b in range(10):
    ... pass
    ... g()
    ...
    f()
    100

    1. Lexical Scoping or Dynamic Scoping

    Python 是 lexical scoping. 比如说:

    def f():
    ... x = 1
    ... def g():
    ... print(x)
    ... def h():
    ... x = 2
    ... g()
    ... h()
    ...
    f()
    1
    虽然作用域是被静态决定的,但它们是被“动态使用”的。考虑下面的例子:

    def f():
    ... def g():
    ... print(x)
    ... x = 1
    ... g()
    ...
    f()
    1
    在这里,g 被定义时尚未有 x 的存在,但当 g 被调用时却可以使用 g 定义后引入的 x. 不过,g 中使用的 x 仍然属于 g 被定义时的外层作用域(即 f 的内部),从这个意义上说,此处的作用域规则仍然是 lexical scoping.

    要注意的是,根据 Python 官方 Tutorial 的说法,Python 正在向“静态名字解析”方向演化,所以请勿依赖这种动态名字解析!

    Python学习交流群:835017344,这里是python学习者聚集地,有大牛答疑,有资源共享!有想学习python编程的,或是转行,或是大学生,还有工作中想提升自己能力的,正在学习的小伙伴欢迎加入学习。

    相关文章

      网友评论

        本文标题:Python 的作用域的相关规则

        本文链接:https://www.haomeiwen.com/subject/bdfnxctx.html