30,808
社区成员
发帖
与我相关
我的任务
分享在介绍类前,首先要介绍 Python 的作用域规则。类定义对命名空间有一些巧妙的技巧,了解作用域和命名空间的工作机制有利于加强对类的理解。并且,即便对于高级 Python 程序员,这方面的知识也很有用。
接下来,我们先了解一些定义。
namespace (命名空间)是映射到对象的名称。现在,大多数命名空间都使用 Python 字典实现,但除非涉及到优化性能,我们一般不会关注这方面的事情,而且将来也可能会改变这种方式。命名空间的几个常见示例: abs() 函数、内置异常等的内置函数集合;模块中的全局名称;函数调用中的局部名称。对象的属性集合也算是一种命名空间。关于命名空间的一个重要知识点是,不同命名空间中的名称之间绝对没有关系;例如,两个不同的模块都可以定义 maximize 函数,且不会造成混淆。用户使用函数时必须要在函数名前面附加上模块名。
点号之后的名称是 属性。例如,表达式 z.real 中,real 是对象 z 的属性。严格来说,对模块中名称的引用是属性引用:表达式 modname.funcname 中,modname 是模块对象,funcname 是模块的属性。模块属性和模块中定义的全局名称之间存在直接的映射:它们共享相同的命名空间! 1
属性可以是只读或者可写的。如果可写,则可对属性赋值。模块属性是可写时,可以使用 modname.the_answer = 42 。del 语句可以删除可写属性。例如, del modname.the_answer 会删除 modname 对象中的 the_answer 属性。
命名空间是在不同时刻创建的,且拥有不同的生命周期。内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。模块的全局命名空间在读取模块定义时创建;通常,模块的命名空间也会持续到解释器退出。从脚本文件读取或交互式读取的,由解释器顶层调用执行的语句是 __main__ 模块调用的一部分,也拥有自己的全局命名空间。内置名称实际上也在模块里,即 builtins 。
函数的本地命名空间在调用该函数时创建,并在函数返回或抛出不在函数内部处理的错误时被删除。 (实际上,用“遗忘”来描述实际发生的情况会更好一些。) 当然,每次递归调用都会有自己的本地命名空间。
作用域 是命名空间可直接访问的 Python 程序的文本区域。 “可直接访问” 的意思是,对名称的非限定引用会在命名空间中查找名称。
作用域虽然是静态确定的,但会被动态使用。执行期间的任何时刻,都会有 3 或 4 个命名空间可被直接访问的嵌套作用域:
最内层作用域,包含局部名称,并首先在其中进行搜索
封闭函数的作用域,包含非局部名称和非全局名称,从最近的封闭作用域开始搜索
倒数第二个作用域,包含当前模块的全局名称
最外层的作用域,包含内置名称的命名空间,最后搜索
如果把名称声明为全局变量,则所有引用和赋值将直接指向包含该模块的全局名称的中间作用域。重新绑定在最内层作用域以外找到的变量,使用 nonlocal 语句把该变量声明为非局部变量。未声明为非局部变量的变量是只读的,(写入只读变量会在最内层作用域中创建一个 新的 局部变量,而同名的外部变量保持不变。)
通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。在函数之外,局部作用域引用与全局作用域一致的命名空间:模块的命名空间。 类定义在局部命名空间内再放置另一个命名空间。
划重点,作用域是按字面文本确定的:模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。另一方面,实际的名称搜索是在运行时动态完成的。但是,Python 正在朝着“编译时静态名称解析”的方向发展,因此不要过于依赖动态名称解析!(局部变量已经是被静态确定了。)
Python 有一个特殊规定。如果不存在生效的 global 或 nonlocal 语句,则对名称的赋值总是会进入最内层作用域。赋值不会复制数据,只是将名称绑定到对象。删除也是如此:语句 del x 从局部作用域引用的命名空间中移除对 x 的绑定。所有引入新名称的操作都是使用局部作用域:尤其是 import 语句和函数定义会在局部作用域中绑定模块或函数名称。
global 语句用于表明特定变量在全局作用域里,并应在全局作用域中重新绑定;nonlocal 语句表明特定变量在外层作用域中,并应在外层作用域中重新绑定。