作用域和命名空间
命名空间是一个名称和对象的映射关系。在现在的 Python 实现中,命名空间大都是通过字典实现的。常见的命名空间例子有模块的顶层命名空间,函数内的命名空间等。不同命名空间内拥有相同名字的对象之间没有任何关联关系。
不同的命名空间,创建时机不同,生命周期也不同。保存内置函数、变量的命名空间在 Python 解析器启动的时候创建,并且在解析器退出之前一直有效。模块的顶层命名空间在模块首次导入的时候被创建。函数内部的命名空间在函数被调用的时候创建,并且多次调用函数会创建不同的命名空间,在函数返回时,命名空间被销毁。
一般有三种命名空间:
- 内置名称(built-in names), Python 语言内置的名称,比如函数名
abs
、char
和异常名称BaseException
、Exception
等等。 - 全局名称(global names),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
- 局部名称(local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)
作用域是一个 Python 程序可以直接访问命名空间的正文区域。在一个 python 程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到。变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。作用域物理上指的是一段程序区域,在这个区域里的所有命名构成一个命名空间,在这个区域里,这个命名空间包含的所有命名都可以直接访问。
一般有四种作用域:
- L(Local):最内层,包含局部变量,比如一个函数/方法内部。
- E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
- G(Global):当前脚本的最外层,比如当前模块的全局变量。
- B(Built-in):包含了内建的变量/关键字等,最后被搜索。
一般来说,在没有 global
和 nonlocal
关键字的情况下,为一个名称赋值都会在最内层的命名空间中创建对象。赋值并不拷贝具体的值,而是在命名空间中做绑定关系。
如果在内部作用域使用 global
关键字,则会将名称重新绑定到模块顶层的全局命名空间(Global)中。使用 nonlocal
关键字,则标明这个名称在中间层次(Enclosing)的命名空间中。
下面的例子可以说明这两个关键字的作用。
def scope_test():
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
spam = "test spam"
do_local()
print("After local assignment:", spam)
do_nonlocal()
print("After nonlocal assignment:", spam)
do_global()
print("After global assignment:", spam)
scope_test()
print("In global scope:", spam)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输出为
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
2
3
4
类的定义
类定义的语法形式如下:
class ClassName:
<statement-1>
.
.
.
<statement-N>
2
3
4
5
6
在定义类的时候,会创建一个新的命名空间,同时 Python 的 Local 作用域变成了这个类。类中定义的变量、函数都绑定在新的命名空间中。当完成类定义之后,会生成一个类对象,这个类对象中保存者类中命名空间的内容。同时,在进入类定义之前的作用域被恢复为 Local 作用域,类对象被绑定在这个作用域的命名空间中。
类对象
类对象支持两种类型的操作,属性访问和实例化。
比如下面定义的类:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
2
3
4
5
6
我们可以通过 MyClass.i
和 MyClass.f
来访问类对象上的属性,我们设置可以修改类对象上的属性。
通过如下方式可以实现类的实例化。
x = MyClass()
实例化后,会创建一个新的对象,实例对象,对象中保存一些初始状态。比如上面的代码会创建一个空对象。我们可以在类定义中通过 __init__()
函数来指定在实例化时需要的参数,在实例化时会自动调用这个函数。
>>> class Complex:
... def __init__(self, realpart, imagpart):
... self.r = realpart
... self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)
2
3
4
5
6
7
8
实例对象
实例对象,顾名思义是有类实例化而来。实例对象上通常又两种属性,一种是变量,一种是方法。我们可以给实例对象添加任意属性,即使这个属性没有在类定义中定义。
实例对象上的变量
实例对象上的变量为实例单独所有,类对象上的变量为所有实例对象共有。如果类对象上的变量是一个可变对象,比如列表,那么变更后的值为所有实例对象共有。
class A:
a = 3 # 所有实例共有
list = []
def f(self):
print('class a ' + str(self.a))
2
3
4
5
>>> m = A()
>>> n = A()
>>> m.a
3
>>> m.a = 6
>>> m.a
6
>>> n.a
3
>>> m.list.append(1)
>>> n.list
[1]
2
3
4
5
6
7
8
9
10
11
12
注意上面的 m.a = 6
,如果实例对象上的属性名称与类对象上的属性名称相同,则 Python 会优先使用实例对象上的名称。(别忘了,在给一个变量赋值的时候,会在对应命名空间中创建绑定关系。)
实例对象上的方法
在前面的例子中,MyClass.f
和 x.f
是否是一样的呢?答案是否。MyClass.f
是一个函数对象,而 x.f
是一个方法对象。方法对象中包装了类的函数对象和实例对象。当调用这个方法对象的时候,方法对象会在参数列表前面加上实例对象,然后使用新的参数列表调用函数对象。
也就是说 x.f()
与 MyClass.f(MyClass)
意义相同。
继承
继承的语法形式为:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
2
3
4
5
6
继承场景下的类对象与单一类对象类似,子类对象中会保存一个基类对象。在查找属性的时候,会优先查找子类上的属性,必要情况下会查找基类对象上的属性。
子类可以复写基类的方法。如果子类复写了基类的方法,那么当基类上的其他方法在调用这个方法的时候,实际上是调用的子类上的方法。我们可以通过 BaseClassName.methodname(self, arguments)
来调用基类的方法。
在 Python 中,我们可以通过 isinstance()
和 issubclass()
来判断继承关系。
在多继承场景下,属性搜索遵循深度优先,从左到右的原则。即先查找 DerivedClassName
,之后查找 Base1
以及 Base1
的基类,如果还未找到,再从 Base2
及 Base2
的基类中找,以此类推。
私有变量
在 Python 中有个约定,以下划线开头的变量会被认为是私有变量。为了防止在类继承的时候私有变量被修改,Python 会对以至少两个下划线 __
开头、至多一个下划线结尾的变量名做内部处理,比如 __spam
会被处理为 _classname__spam
,这样就可以有效避免不必要的修改了。这种处理对所有定义在类定义范围内的名称有效,不考虑名称出现的具体位置。
class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # 对 update() 的私有拷贝
class MappingSubclass(Mapping):
def update(self, keys, values):
for item in zip(keys, values):
self.items_list.append(item)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
即使我们在 MappingSubclass
中定义一个 __update
变量,也不会影响 Mapping
类中的 __update
工作。
关注微信公众号,获取最新推送~
加微信,深入交流~