Python中的面向对象编程设计
本文是Python Tutorial官方教程第九章的学习笔记,主要记录了Python中的作用域规则、类设计以及迭代器和生成器的相关知识,对中文教程中的翻译错误之处做了订正,部分章节根据个人理解做了顺序调整。本笔记与原教程第九章几无差别,可作为Python类设计的学习和回顾之用。
Python的OOP特性
如同C++与Java,Python同样采用类设计实现用户定义类型,绑定数据与函数。创建新类(class)就是创建新的对象类型(type of object),从而允许创建该类型的实例(instance)。实例拥有标识其状态的属性(attributes)以及改变其状态(修改属性)的方法(methods)
Python的类有点类似于C++和Modula-3中类的结合体,Modula-3的面向对象语义比C++更接近Python。Python的类设计拥有以下特性:
- 类成员通常为公有的(public)
- 私有成员变量约定以
_
为前缀并采用名称改写规则避免冲突 - 所有成员函数都是虚函数(virtual)
- 类本身也是对象从而为导入和重命名提供了语义支持
- 内置类型可以用作基类供用户扩展
- 算术运算符、下标等具有特殊语法的内置运算符都可为类实例重新定义(支持重载运算符)
- 支持多重继承
- 派生类能覆盖(override)基类的方法
- 派生类的方法能调用基类中的同名方法
- 采用类似模块的动态类设计:运行时创建、创建后可修改
别名机制
对象具有独立性(相对于名称),多个名称(在多个作用域内)可以被绑定至同一对象,这被称为别名(alias)。别名的存在类似指针,传递对象时只需要传递指针(别名),代价很小;而且不同于复制传值,对于传递对象的修改可以直接被调用者所知晓。
Python的作用域规则
命名空间与属性引用
命名空间(namespace)是名称到对象的映射集,一般使用字典实现。不同命名空间的名称互不可见,没有干扰,因而同一名称在不同命名空间中可以映射不同对象。用户使用函数时必须要在函数名前面附加上模块名。命名空间的实例包括:
- 内置名称(函数名以及内置异常名)的集合
- 模块内部的所有全局名称
- 函数调用中的所有局部名称
- 对象属性的集合
对属性的访问是通过点号运算符.
实现的。严格来说,对模块中名称的引用是属性引用。引用模块内部名称的表达式modname.funcname
中,modname
指向模块对象,funcname
既是模块内部的函数名称,也是模块的属性。模块属性和模块中定义的全局名称之间共享相同的命名空间,全局名称直接映射至相应的模块属性。
属性可以是只读或者可写的。如果可写,则可对属性赋值。模块属性是可写时,可以对模块属性赋值。del语句
可以删除可写属性。
>>> modname.the_answer = 42
>>> del modname.the_answer
命名空间是在不同时刻创建的,且拥有不同的生命周期。
- 内置名称实际上位于
builtins
模块里,其命名空间是在Python解释器启动时创建的,永远不会被删除 - 模块的全局命名空间在读取模块定义时创建,通常也会持续到解释器退出
- (从脚本文件读取或交互式读取的,由解释器顶层调用执行的)语句是
__main__
模块调用的一部分,也拥有自己的全局命名空间 - 函数的本地命名空间在调用该函数时创建,并在函数返回或抛出错误不在函数内部处理时被删除(准确来说,被遗忘)
- 函数的每次递归调用都会有自己的本地命名空间
作用域与名称解析
作用域(scope)是命名空间可直接访问的Python程序的文本区域,所谓“可直接访问”指对名称的非限定引用会在命名空间中查找名称。作用域虽然是静态确定的,但会被动态使用。执行期间任何时刻,都会有3-4个嵌套作用域:
- 最内层作用域(innermost scope),包含局部名称,最先被搜索
- 所在高层函数(enclosing function)的作用域,包含非局部非全局的名称,在最近的嵌套作用域后被搜索
- 次外层作用域(next-to-last scope),包含当前模块的全局名称
- 最外层作用域(outermost scope),包含内置名称的命名空间,最后搜索
以上被称作Python作用域的LEGB
原则。Python会按照本地作用域(Local)→ 当前作用域被嵌入的本地作用域(Enclosing locals)→ 全局/模块作用域(Global)→ 内置作用域(Built-in)的顺序搜索名称所对应的对象。
若名称被声明为全局名称,所有的引用和赋值操作都会直接搜索包含模块全局名称的次外层作用域。可使用nonlocal
语句重新绑定最内层作用域外的变量,若未声明为nonlocal
,这些变量是只读的(任何尝试写变量的操作都会在最内层作用域中创建新的局部变量而不改变外层的同名变量)。
函数内部,本地作用域将(按字面文本)引用当前函数的局部名称。函数之外,本地作用域引用与全局作用域一致的命名空间:模块的命名空间。类定义会在本地作用域内生成额外的命名空间。
作用域是通过程序文本确定的,模块内定义的函数的全局作用域即为该模块的命名空间,无论该函数从哪里以哪种别名被调用。然而,名称的实际搜索是运行时动态进行的。当然,也不应完全依赖动态名称解析(name resolution),语言定义也有静态名称解析或编译时名称解析的参与。
nonlocal与global语句
如果没有生效的nonlocal
或global
语句,对名称的赋值永远发生在最内层作用域。赋值并不会复制数据,仅仅是将名称绑定至对象。同样地,删除语句del name
也只会解除本地作用域指向的命名空间的name
的绑定。实际上,所有引入新名称的操作都发生于本地作用域。作为范例,import
语句和函数定义将模块名或函数名绑定至本地作用域。
global
语句被用于特别指明那存在于全局作用域的变量名应在此重新绑定;nonlocal
语句特别指明那存在于Enclosing scope的变量名应在此重新绑定。以下是二者如何影响变量名绑定的示例:
def scope_test():
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
# name spam is bound to enclosing scope
# that is, name spam is bound to scope_test()
spam = "nonlocal spam"
def do_global():
global spam
# name spam is bound to global scope
# that is, name spam is bound to "__main__" module
spam = "global spam"
spam = "test spam"
do_local()
# spam inside do_local() is destroyed
# next spam is applied to local spam "test spam"
print("After local assignment:", spam)
do_nonlocal()
# spam is mapped to string "nonlocal spam"
print("After nonlocal assignment:", spam)
do_global()
# global spam(outside function) is mapped to string "global spam"
# next spam is searched in local scope first
print("After global assignment:", spam)
scope_test()
# spam is just mapped to string "global spam"
# even though there is no assignment in global scope
print("In global scope:", spam)
以上程序清单的运行结果为:
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
Python类基础
本篇介绍类的新语法(syntax)、三种对象类型(object types)以及部分新的语义(semantics)。篇末也将探讨Python类中的私有变量的表示。
类定义语法
最简单的类定义形式为:
class ClassName:
<statement-1>
.
.
.
<statement-N>
类定义内的语句通常都是函数定义,但也可以是其他语句。类里的函数定义一般是特殊的参数列表,这是由方法调用的约定规范所指明的。
当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域。因此,所有对局部变量的赋值都是在这个新命名空间之内。特别的,函数定义会绑定到这里的新函数名称。
与函数定义def
语句一样,类定义必须先执行才能生效。当从结尾处正常离开类定义时,将创建一个类对象。原始的(在进入类定义之前起作用的)局部作用域将重新生效,类对象将在这里被绑定到类定义头所给出的类名称。
新的对象类型
类设计引入三种新的对象类型:
- 类对象
- 实例对象
- 方法对象
类对象
类对象支持两种操作:属性引用和实例化。属性引用(attribute references)使用标准语法 obj.name
。有效的属性名称是类对象被创建时存在于类命名空间中的所有名称。
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
MyClass.i
和MyClass.f
就是有效的属性引用,将分别返回一个整数和一个函数对象。类属性也可以被赋值,因此可以通过赋值来更改MyClass.i
的值。__doc__
也是一个有效的属性,将返回所属类的文档字符串:“A simple example class”。
类的实例化(instantiation)使用函数表示法。可以把类对象视为是一个不带参数的函数,返回该类的一个新实例。
x = MyClass()
该行代码创建类的新实例并将此对象赋值给局部变量x。
实例化操作会创建一个空对象。许多类喜欢创建带有特定初始状态(参数已初始化)的自定义实例。为此类定义可能包含一个名为__init__()
的特殊方法。若类定义了该方法,实例化时会为新创建的类实例自动触发__init__()
方法。
def