作用域和命名空间

命名空间是一个名称和对象的映射关系。在现在的 Python 实现中,命名空间大都是通过字典实现的。常见的命名空间例子有模块的顶层命名空间,函数内的命名空间等。不同命名空间内拥有相同名字的对象之间没有任何关联关系。

不同的命名空间,创建时机不同,生命周期也不同。保存内置函数、变量的命名空间在 Python 解析器启动的时候创建,并且在解析器退出之前一直有效。模块的顶层命名空间在模块首次导入的时候被创建。函数内部的命名空间在函数被调用的时候创建,并且多次调用函数会创建不同的命名空间,在函数返回时,命名空间被销毁。

一般有三种命名空间:

  • 内置名称(built-in names), Python 语言内置的名称,比如函数名 abschar 和异常名称 BaseExceptionException 等等。
  • 全局名称(global names),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
  • 局部名称(local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)

作用域是一个 Python 程序可以直接访问命名空间的正文区域。在一个 python 程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到。变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称。作用域物理上指的是一段程序区域,在这个区域里的所有命名构成一个命名空间,在这个区域里,这个命名空间包含的所有命名都可以直接访问。

一般有四种作用域:

  • L(Local):最内层,包含局部变量,比如一个函数/方法内部。
  • E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
  • G(Global):当前脚本的最外层,比如当前模块的全局变量。
  • B(Built-in):包含了内建的变量/关键字等,最后被搜索。

一般来说,在没有 globalnonlocal 关键字的情况下,为一个名称赋值都会在最内层的命名空间中创建对象。赋值并不拷贝具体的值,而是在命名空间中做绑定关系。

如果在内部作用域使用 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)
1
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
1
2
3
4

类的定义

类定义的语法形式如下:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>
1
2
3
4
5
6

在定义类的时候,会创建一个新的命名空间,同时 Python 的 Local 作用域变成了这个类。类中定义的变量、函数都绑定在新的命名空间中。当完成类定义之后,会生成一个类对象,这个类对象中保存者类中命名空间的内容。同时,在进入类定义之前的作用域被恢复为 Local 作用域,类对象被绑定在这个作用域的命名空间中。

类对象

类对象支持两种类型的操作,属性访问和实例化。

比如下面定义的类:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'
1
2
3
4
5
6

我们可以通过 MyClass.iMyClass.f 来访问类对象上的属性,我们设置可以修改类对象上的属性。

通过如下方式可以实现类的实例化。

x = MyClass()
1

实例化后,会创建一个新的对象,实例对象,对象中保存一些初始状态。比如上面的代码会创建一个空对象。我们可以在类定义中通过 __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)
1
2
3
4
5
6
7
8

实例对象

实例对象,顾名思义是有类实例化而来。实例对象上通常又两种属性,一种是变量,一种是方法。我们可以给实例对象添加任意属性,即使这个属性没有在类定义中定义。

实例对象上的变量

实例对象上的变量为实例单独所有,类对象上的变量为所有实例对象共有。如果类对象上的变量是一个可变对象,比如列表,那么变更后的值为所有实例对象共有。

class A:
    a = 3 # 所有实例共有
    list = []
    def f(self):
        print('class a ' + str(self.a))
1
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]
1
2
3
4
5
6
7
8
9
10
11
12

注意上面的 m.a = 6,如果实例对象上的属性名称与类对象上的属性名称相同,则 Python 会优先使用实例对象上的名称。(别忘了,在给一个变量赋值的时候,会在对应命名空间中创建绑定关系。)

实例对象上的方法

在前面的例子中,MyClass.fx.f 是否是一样的呢?答案是否。MyClass.f 是一个函数对象,而 x.f 是一个方法对象。方法对象中包装了类的函数对象和实例对象。当调用这个方法对象的时候,方法对象会在参数列表前面加上实例对象,然后使用新的参数列表调用函数对象。

也就是说 x.f()MyClass.f(MyClass) 意义相同。

继承

继承的语法形式为:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>
1
2
3
4
5
6

继承场景下的类对象与单一类对象类似,子类对象中会保存一个基类对象。在查找属性的时候,会优先查找子类上的属性,必要情况下会查找基类对象上的属性。

子类可以复写基类的方法。如果子类复写了基类的方法,那么当基类上的其他方法在调用这个方法的时候,实际上是调用的子类上的方法。我们可以通过 BaseClassName.methodname(self, arguments) 来调用基类的方法。

在 Python 中,我们可以通过 isinstance()issubclass() 来判断继承关系。

在多继承场景下,属性搜索遵循深度优先,从左到右的原则。即先查找 DerivedClassName,之后查找 Base1 以及 Base1 的基类,如果还未找到,再从 Base2Base2 的基类中找,以此类推。

私有变量

在 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

即使我们在 MappingSubclass 中定义一个 __update 变量,也不会影响 Mapping 类中的 __update 工作。

关注微信公众号,获取最新推送~