类Class
Learn from python2.7 official documentation
Python的类提供了面向对象编程(Object Oriented Programming)的所有标准特性,同时也包含了Python语言的动态特性:在运行时创建,可以在创建后修改。
9.1 名称Name和对象Object
对象具有个性(individuality),可以将多个名称(在多个作用域中)绑定到同一对象,这在其他语言中称为别名(aliasing)。在处理不可变的基本类型(immutable basic types),如number
、string
、tuple
时,可以安全地忽略这一点;但在别名涉及可变对象(mutable object),如list
、dictionary
时,可能会产生不可预料的通常有利于程序的影响,因为别名在某些方面表现得像指针(pointers)。
9.2 作用域Scope和命名空间Namespace
命名空间是一个从名字到对象的映射,大部分命名空间都由Python字典实现。
几个命名空间的例子:存放内置函数的集合、模块中的全局名称、函数调用的局部名称。
命名空间是在不同的时刻创建的,并且具有不同的生存期。
注意:不同命名空间的名称之间没有关系。
跟在点号"."
之后的名称称为属性(attribute),属性可以是只读的(read-only)或者可写的(writable)。可写的属性可以进行赋值操作modname.the_answer = 42
(修改modname
对象的the_answer
属性值)及删除操作del modname.the_answer
(从modname
对象中移除the_answer
属性)。
作用域是一个命名空间可以直接访问的Python程序的文本区域。直接访问意味着对名称的不合理引用(unqualified reference)将试图在命名空间内查找该名称。
虽然作用域是静态地被确定的,但是被动态地使用的。在执行的过程中,至少有三种嵌套的作用域可以直接访问:
- 最先搜索的(be searched first),最内层的(innermost)作用域,包含局部名称(local name);
- 从最相近的(nearest)封闭作用域(enclosing scope)开始搜索的,任何封闭函数(enclosing function)的作用域,包含非局部名称(non-local name)和非非全局名称(non-global name)。
- 倒数第二的(next-to-last)作用域,包含当前模块的全局名称;
- 最后搜索的(searched last),最外层的(outermost)作用域,是包含内置名称的命名空间。
如果一个名称被声明为全局名称(global name),则所有的引用和赋值,将直接转到包含模块全局名称的中间作用域(middle scope);否则,在innermost scope
外的所有变量(variable)都是只读的(read-only),尝试写入这样的变量,只会在innermost scope
中创建一个新的局部变量,而具有相同名称的外层变量保持不变。
通常,局部作用域引用当前函数内的局部名称,函数外的局部作用域引用和全局作用域一样的命名空间,即模块的命名空间。类的定义将在局部作用域中加入新的命名空间。
Python中,如果没有有效的global
语句,对名称的分配总是进入最内层的作用域。分配名称不会复制数据,只会将名称绑定到对象;同样的,删除语句del x
只是解除局部作用域引用的命名空间,与x
之间的绑定。
事实上,采用新名称的所有操作都使用局部作用域,特别的,import
语句和函数定义将模块和函数名称绑定到局部作用域。
global
语句可用于显示全局作用域中的特殊变量。
9.3 初探类Class
类引入了一些新语法(syntax),三种新对象类型(object type)和一些新语义(semantic)。
9.3.1 类定义语法Class Definition Syntax
简单的Python类定义:
1 | class ClassName: |
9.3.2 类对象Class Object
当程序从结尾处正常离开类定义时,将会创建一个类对象,它作为类定义中创建的命名空间的封装器(wrapper),并绑定到类定义所给出的类名称上,如上文中的ClassName
。
类对象支持两种操作:属性引用(attribute reference)和实例化(instantiation)。
9.3.2.1 属性引用
类属性引用使用Python中属性引用的标准语法obj.name
。有效的属性名称(attribute name)包括类对象被创建时存在于类命名空间中的所有名称,包括数据属性(data attribute)和方法(method)。如果类定义如下:
1 | class MyClass: |
那么有效的属性引用包括:MyClass.i
和MyClass.f
,其分别返回一个整数和一个函数对象。
类属性也可以被赋值(assignment),因此可以通过赋值操作来更改MyClass.i
的值。
__doc()__
也是一个有效的属性,其返回所属类的文档字符串,如上文中的"A simple example class"
。
9.3.2.2 实例化
类的实例化使用Python中函数表示法,可把类对象视为,返回该类的一个新实例的不带参数的函数:
1 | x = MyClass() |
实例化操作会创建一个空对象,许多类喜欢创建带有特定初始状态的自定义实例,为此,类定义中可以包含一个名为__init()__
的特殊方法,当类定义了__init()__
方法时,实例化操作会自动为新创建的类实例调用__init()__
方法。
除了第一个参数self
是必须的;__init()__
方法还可以有额外的参数,这种情况下,实例化时提供的参数将被传递给__init()__
:
1 | class MyClass: |
9.3.3 实例对象
实例对象可以接收的唯一操作为属性引用,有效的属性名称包括数据属性和方法。
数据属性不需要声明,和局部变量一样,其在第一次被赋值时产生:
1 | x.counter = 1 |
x
为上文中MyClass
的实例,代码段打印数值16
而不会保留任何追踪信息(新产生的数据属性counter
在结尾被del
语句清除)。
实例对象引用的方法是属于对象的函数,有效的方法名称依赖于实例对象所属的类,类定义中所有的函数对象属性,都是其实例的相应方法。
x.f
即为有效的方法引用,因为MyClass.f
是一个函数,但x.f
和MyClass.f
并不相同,x.f
是方法对象(method object),MyClass.f
是函数对象(function object)。
9.3.4 方法对象Method Object
通常,方法在绑定后立即被调用,在MyClass
的实例中,这将返回字符串'hello world'
:
1 | x.f() |
立即调用一个方法并不是必须的,x.f
是一个方法对象,其可以被保存起来,以后再调用:
1 | xf = x.f |
方法的特殊之处在于,调用时,对象作为函数的第一个参数self
传递给方法,x.f()
与MyClass.f(x)
完全等价。
当一个实例的非数据属性被引用时,将搜索实例所属的类,如果名称表示一个属于函数对象的有效类属性,Python将合并实例对象和函数对象到一个抽象对象中,这个抽象对象就是方法对象。调用方法对象时,如果附带参数列表,将会基于实例对象和参数列表来构建一个新的参数列表,并使用新的参数列表来调用相应的函数对象。
9.3.5 类和实例变量Instance Variable
一般的,实例变量用于每个实例的唯一数据,类变量用于类的所有实例共享的属性。
1 | class Dog: |
注意:将可变对象,如列表、字典,作为类变量时,所有类的实例将共享一个单独的变量:
1 | class Dog: |
正确的类设计应该使用实例变量:
1 | class Dog: |
9.4 补充说明
数据属性会覆盖掉具有相同名称的方法属性。
数据属性可以被方法以及一个对象的普通用户(“客户端”)所引用。换句话说,类不能用于实现纯抽象数据类型。 实际上,在 Python 中没有任何东西能强制隐藏数据 — 它是完全基于约定的。
在方法内部引用数据属性或其他方法并没有简便方式。
约定的,方法的第一个参数常常被命名为 self
,这一名称在 Python 中没有特殊含义。
任何一个作为类属性的函数都为该类的实例定义了一个相应方法,函数定义可以在类定义之外,将函数对象赋值给一个局部变量是可行的:
1 | # 在类定义外定义方法函数 |
f
,g
和h
都是C
类引用函数对象的属性,都是C
实例的方法。
方法可以通过使用self
参数调用其他方法:
1 | class Bag: |
可以通过object.__class__
来获取对象所属的类。
9.5 继承Inheritance
派生类定义的基本语法:
1 | class DerivedClassName(BaseClassName): |
名称BaseClassName
必须定义于包含派生类定义的作用域中,当基类定义在引用的模块中时,可以通过表达式表示基类位置:
1 | class DerivedClassName(modname.BaseClassName): |
派生类定义的执行过程中,当构造类对象时,基类会被记住,此信息将被用来解析属性引用:如果请求的属性在类中找不到,搜索将转往基类中进行查找;如果基类本身也派生自其他某个类,则此规则将被递归地应用。
派生类可能会重载其基类的方法。
对 C++ 程序员的提示:Python 中所有的方法实际上都是 virtual
方法。(暂时无法理解?)
在派生类中的重载方法,实际上可能想要扩展而非替换同名的基类方法。
使用BaseClassName.methodname(self, arguments)
可以简单地直接调用基类方法,此方式仅当基类名称在全局作用域中存在时可用。
Python中关于继承机制的内置函数:
isinstance(obj, int)
:用于检查一个obj
实例的类型,仅当obj.__class__
为int
或某个派生自int
的类时为True
。issubclass(class, classinfo)
:用于检查类的继承,当class
为classinfo
的子类时为True
;当class
不是classinfo
的子类,它们只有basestring()
一个共同的祖先时为False
。
9.5.1 多重继承Multiple Inheritance
Python多重继承的派生类定义的基本语法:
1 | class DerivedClassName(Base1, Base2, Base3): |
MRO
即方法解析顺序(method resolution order),用于判断子类调用的属性来自于哪个父类。在Python2.3
之前,MRO
是基于深度优先算法的,自Python2.3
开始使用C3
算法,定义类时需要继承object
,这样的类称为新式类,否则为旧式类。
9.6 私有变量Private Variables和类局部引用Class-local References
Python中不存在只能从对象内部访问的私有实例变量,约定的,以下划线开头的名称,如_span
被视为API
非公开的部分,无论其为函数、方法还是数据成员。
9.6.1 名称改写name mangling
为了避免,类似私有成员的名称与子类所定义的名称相冲突,这类情况的出现,Python提供名称改写(name mangling)机制,由至少两个前缀下划线,至多一个后缀下划线构成的标识符文本,如__spam
,将被替换为_classname__spam
,其中classname
为去除了前缀下划线的当前类名称,这种改写不考虑标识符的位置,只要出现在类定义内部就会进行。
名称改写有助于让子类重载方法而不破坏类内方法调用:
1 | class Mapping: |
上面的示例即使在 MappingSubclass
引入了一个__update
标识符的情况下也不会出错,因为它会在Mapping
类中被替换为_Mapping__update
而在MappingSubclass
类中被替换为_MappingSubclass__update
。
9.7 杂项说明
有时会需要使用类似于C
的"struct"
这样的数据类型,将一些命名数据项捆绑在一起。这种情况适合定义一个空类:
1 | class Employee: |
实例方法对象也具有属性。
文档原文:Instance method objects have attributes, too: m.im_self
is the instance object with the method m()
, and m.im_func
is the function object corresponding to the method.
9.8 异常Exception是类
如果except
子句中的类是异常的基类,那么该子句与异常兼容,此兼容不可逆:
1 | class B: |
如果将except
子句顺序颠倒,将打印B B B
。
9.9 迭代器Iterator
Python
中,大多数的容器对象都可以使用for
语句进行迭代:
1 | for element in [1, 2, 3]: |
实际上,for
语句在开始时,调用了容器对象的iter()
函数,这个函数返回一个迭代器(iterator)对象。迭代器对象定义了next()
方法,该方法一次去访问容易中的一个元素,并返回元素值,当没有更多的元素时,next()
方法抛出StopIteration
异常,来提示for
语句终止循环:
1 | 'abc' s = |
将迭代器添加到类中,只需要在类中定义__iter()__
方法,该方法返回带有next()
方法的对象;如果同时在类中定义了next()
方法,则__iter()__
可以返回self
:
1 | class Reverse: |
使用上文实现的迭代器:
1 | 'spam') rev = Reverse( |
9.10 生成器Generator
生成器是用于创建迭代器的工具,它可以自动为创建的迭代器添加__iter()__
和next()
方法。
生成器写法类似标准函数,返回数据时使用yield
语句:
1 | def reverse(data): |
生成器的局部变量和执行状态会在每次调用之间自动保存,每次对生成器调用next()
时,它会从上次离开位置恢复执行。
除了会自动创建方法和保存程序状态,当生成器终结时,它们还会自动引发StopIteration
。
9.11 生成器表达式
某些简单的生成器可以写成简洁的表达式代码,所用语法类似列表推导式,但外层为圆括号而非方括号。
这种表达式被设计用于,生成器将立即被外层函数所使用的情况。
生成器表达式相比完整的生成器更紧凑但较不灵活,相比等效的列表推导式则更为节省内存。
1 | for i in range(10)) # sum of squares sum(i*i |