Python面向对象

Classes - Python Tutorial
Data model - Python Tutorial
Python OOP Tutorials - Corey Schafer

程序 = 数据 + 算法,数据用于记录状态,算法用于修改状态。其中的“+”,即如何将数据与算法有机的融合,可以有不同选择,对应于不同编程范式:最简单的就是数据与算法混杂,对应于过程化编程;对于复杂问题可以选择将数据与相关联的算法封装为整体,即所谓面向对象编程;也可选择将数据与算法剥离,使函数成为不含任何状态的数据管道,并通过函数的组合构成数据加工的流水线,即所谓函数式编程。

面向对象编程中,对于数据和方法封装的抽象被称为“”。同一个类的(实例)对象,所能执行的操作(函数/方法)是一样的,区别通常仅限于数据,而这些携带了不同数据的对象被称为类的“实例”。类之间可通过继承复用基类的方法,且可以继承多个基类,面向对象编程的核心就在于类的设计。

Object、Class、Type
首先,一般意义上,Python中一切都是对象(object);而同时Python中还有一个具体的、名为’object’的类,用作一切类的基类。
其次,一切对象都有各自(所从属)的类,英文用type或class都可以;同时Python中还有一个具体的、名为’type’的类,作为类的类。

每个对象都会有标识(ID)、类型/类别(type)以及取值(value):ID可通过id()获取,可理解为内存地址;Type则可通过type()获取。ID与type在对象创建后都不可变,type决定了value是否可变,具体分为可变对象(mutable)和不可变对象(immutable)。

1
2
3
4
5
6
7
8
class A:
pass

a = A()
print(type(a)) # <class '__main__.A'>
print(type(A)) # <class 'type'>
print(type(object)) # <class 'type'>
print(type(type)) # <class 'type'>

“实例”a的type为某个自定义类A,而“类”的type为’type’类,即类属于名为type的特殊类,内置的object也属于这个type类,甚至type自身也属于这个type类。除了这个具体的type类,一般意义上的type与class使用也有细微区分,大致可理解为:type是class的抽象指代,而class是type的具体实现,但本质上两者指的是一个东西,没有区别。早期的 Python 2 中class与type是有明确划分的,现在已无需在意。更多讨论可参考What are Python’s type “objects” exactly?
注:类的类其实就是元类,即这个名为type的特殊类其实就是所谓的元类。

类、实例、属性

类的创建与函数创建类似,将关键字def替换为class即可,不同处在于名称后的括号:函数名后括号内变量用于标明要传递的形参,当没有参数时,括号可以留空,但不可省略;类名后括号内的变量用于标明要继承的基类,当没有基类时,括号可以留空,也可直接省略。类的实例化同样类似函数调用,此时类名后括号不能省略,当括号内有参数时,会自动传递给特殊方法__init__(),用于实例对象的初始化(后面会介绍)。

  • 类内部定义的函数与普通函数在参数上有一定区别:第一个参数被限定为实例(self)或类(cls),具体讨论参考命名空间类方法部分。
  • 类内部定义的变量及函数会被录入其属性字典,通过<name_obj>.<attr>方式访问。
  • 类对象 :类本身也是对象,除了实例化,也可通过<name_cls>.<attr>访问其属性。
  • 实例对象:只能执行属性访问操作<name_inst>.<attr>,属性又分数据属性及方法。
  • 方法对象:函数及对象指针的封装,调用时将对象指针插入参数列表作第一个参数。

类与实例的属性访问结果并不相同:除了数据属性,类通常返回函数对象,实例则返回方法对象。函数对象与方法对象区别在于对第一个参数的要求:前者通常需要显式提供实例对象作为首参。这也很容易理解:因为方法要作用的对象是确定的,而函数却并没有明确的作用对象;下面会看到,访问类的类方法返回的也是方法对象,因为此时方法要作用的对象(类)也是明确的。
<name_inst>.func(*args, **kwargs) == <name_cls>.func(<name_inst>, *args, **kwargs)

Duck Typing: If it looks like a duck and quacks like a duck, it must be a duck.
鸭子类型:如果一个东西看起来、叫起来都像鸭子,那它一定就是鸭子!
具体到编程,鸭子类型关注接口(方法)而非类型本身,即代码不会检查对象的类型,而只关注对象是否提供了特定方法。这使得用于处理特定数据类型的代码,通常也可以处理模仿了该类型特定方法的自定义类(它们都被视为鸭子!),从而轻松实现多态(polymorphism)。比如要用处理文件对象的函数来处理字符串缓冲,你只需要模仿文件对象,定义一个有read()及readline()方法,实际却从字符串缓冲获取数据的对象,之后直接交给处理文件对象的函数就可以了。
使用鸭子类型,意味着应当尽量避免用type()或isinstance()检查对象的类型,而仅通过hasattr()检查对象是否实现了特定方法,或者直接try…except…(EAFP v.s. LBYL)。

命名空间与作用域

类有自己的局部命名空间,在类定义(而非调用)时创建,类定义结束后其命名空间被封装为类对象;类似的,实例对象的局部命名空间,在实例对象生成(类实例化)时创建,之后便被打包为实例对象。随着类/实例对象创建结束,其局部空间被打包带走,不允许直接访问,只能以属性方式间接访问。注意,实例会自动共享类的属性:访问实例对象属性时,若在对象属性中找不到,会到类属性中继续查找。由于类的数据属性在其所有实例对象间共享,应注意避免滥用,仅提供必要的变量,尤其是尽量避免列表字典等可变类型。

类/实例对象局部命名空间中变量的作用域都仅限于类/实例对象自身,而不包含其中定义的函数或方法!这些函数(方法)内不能直接访问类(实例对象)的局部命名空间,而只能以属性方式间接访问。而函数(方法)内未定义的变量(自由变量)将直接到类的外层命名空间中查找,而非其局域空间!Python 3 中列表推导及生成器表达式均基于函数实现,会引入新的局部作用域,也存在上述限制。

1
2
3
4
5
6
7
8
9
10
11
12
class Cls:
x = 1
def func(self):
print(x)

inst = Cls()
inst.func() # NameError: name 'x' is not defined

class Cls:
n = 2
y = [x**n for x in range(3)] # NameError: name 'n' is not defined
print(y)

Python类定义中函数参数self是什么?
前面提到,类(实例对象)的函数(方法)无法直接访问其局部命名空间,只能以属性方式间接访问。但Python没有提供在对象内用于指向对象本身的关键字。因此,为实现属性访问,Python规定:函数的第一个参数用于指向对象自身。而对于这个形参的名字Python未做任何限制,但根据约定,通常命名为self。This is nothing more than a convention: the name self has absolutely no special meaning to Python.
需注意的是:虽然方法的第一个参数指向对象自身,在定义时必须显式提供;但由于方法对象在进行参数传递前会自动将实例对象作为第一个参数插入参数列表,因此在使用调用方法时,无需(也不能)将实例对象作为实参提供。

属性与__init__()

Python中类及实例的属性(数据/函数)都可以随时直接赋值,而无需预先定义!而所有属性都会被记录在属性字典__dict__中,通过<obj>.__dict__vars(<obj>)可查看全部属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cat():
pass

Cat.color = 'black'
Cat.age = 0
Cat.summary = lambda self: \
print(f'{self.name}: {self.color} color, {self.age} years old.')
mycat = Cat()
mycat.name = 'Kitty'
mycat.age = 3
def func():
print('miao...')
mycat.mew = func

mycat.summary() # Kitty: black color, 3 years old.
mycat.mew() # miao...
print(vars(mycat)) # name, age, mew
from pprint import pprint as print
print(vars(Cat)) # color, age, summary

虽然属性可随时添加,但这样代码会很混乱,因此通常还是会在类定义时预先进行定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Cat():
attr1 = 1

@classmethod # 类方法,参考下文介绍
def func1(cls):
Cat.attr2 = 2 # 类名Cat直接来自全局命名空间
cls.attr3 = 3 # 类名cls通过函数参数传递获得
attr4 = 4

def func2(self):
Cat.attr5 = 5 # 类名Cat直接来自全局命名空间
self.attr6 = 6 # 实例名self通过函数参数传递获得

from pprint import pprint as print
print(vars(Cat)) #attr1, func1, func2
Cat.func1()
print(vars(Cat)) #attr1, attr2, attr3, func1, func2
mycat = Cat()
mycat.func2()
print(vars(Cat)) #attr1, attr2, attr3, attr5, func1, func2

类定义主体(函数之外)中创建的变量都属于类的数据属性(会在所有实例中共享),要想设置实例对象的数据属性,必须借助实例方法,而方法又都需要手动调用才会执行,特殊方法__init__()应运而生。__init__()同样需要实例作为首个参数,与其他实例方法唯一的区别在于它会在类实例化时自动被调用,用于初始化实例对象的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Cat():
total_cats = 0
def __init__(self, name, age, color):
self.name = name
self.age = age
self.color = color
Cat.total_cats += 1

mycat = Cat() # TypeError: __init__() missing 3 required positional arguments: ...
mycat1 = Cat('Kitty', 3, 'black')
mycat2 = Cat('Mimi', 2, 'white')

print(vars(mycat1)) # name, age, color
print(vars(Cat)) # total_cats
print(Cat.total_cats) # 2

注意__init__方法只会在实例化时执行一次(主动调用除外),这意味着,若实例化后某个数据属性更新,__init__中依赖它的属性并不会随之更新!!而__init__方法通常都不会去主动调用,因此其中的变量间最好不要存在任何依赖关系,依赖于其它变量的属性应当在普通的实例方法中去创建。

类方法及静态方法

方法可分为类方法、实例方法、静态方法,由装饰器区分,区别仅在于函数第一个参数:

1
2
3
4
5
6
7
8
9
10
11
class Cls():
def func1(self, *args, **kwargs):
pass

@classmethod
def func2(cls, *args, **kwargs):
pass

@staticmethod
def func3(*args, **kwargs):
pass

方法默认为实例方法,类方法、静态方法需分别通过@classmethod@staticmethod装饰器标识。类/实例方法第一个参数为“奇怪”的特殊参数cls/self,静态方法则没有特殊参数,除此之外三者没有区别。类(实例)方法默认是要对类(实例)进行操作,因此 规定 第一个参数为类(实例),并 约定 命名为cls/self。类(实例)对象调用对应的类(实例)方法时,返回的是方法对象,因此无需显式传递类名或实例名;实例同样可直接调用类方法,但容易造成困惑,不推荐这样用;最后,类也可以调用实例方法,返回的是函数对象,而非方法对象(需显式传递实例名)。

在逻辑上,类方法和静态方法都从属于当前类,而不依赖任何子类或具体对象实例,也不能在子类中改写。其中类方法与类本身关联,修改类的属性或行为;静态方法则是普通函数,用于实现特定处理逻辑,放在类定义内可实现一定程度的封装隔离,只能通过类/实例对象访问。

类方法的一个重要应用是用于实现不同方式的类实例化。__init__方法只有一个,因此类实例化的参数是固定的,但我们实际中却会遇到各种形式的数据。比如某个类的__init__方法接受一系列特定参数,如果要让它能从文件读取数据进行实例化,可以添加一个名为from_file的方法,从文件读入数据,转换为__init__所需参数,再执行类实例化,甚至不借助__init__,以完全独立的方式初始化类。这里的操作发生在实例初始化之前,同时又需要访问类的属性,因此适合用类方法实现。

1
2
3
4
5
6
7
8
9
10
11
class Cls():
def __init__(self, *args, **kwargs):
pass

@classmethod
def from_file(cls, file_path):
data = load(file_path) # load file data
args1, args2 = func(data) # convert to args
return cls(args1, args2, ...) # instantiation

Cls.from_file(file_path)

类方法或静态方法要在类(实例)对象上去调用,在类定义的内部不可直接调用,实例方法则可以直接调用。类方法在要作用在类身上,在类还没定义时就调用,没什么意义;这里主要讨论下调用静态方法的选项:

  1. 将静态方法移出对象,作为全局空间中的一个普通函数,在类内可直接访问
  2. 用普通的实例方法替代静态方法,此时参数传递时第一个参数必须是实例,而在类定义时并没有可用实例。由于原本是静态方法,函数内实际并不会真正用到实例对象,从而可选择用任意的实例替代(仅用于占位),比如None
  3. 通过静态方法的特殊属性__func__获得到原本的函数对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Cls():
def func1(self, *args, **kwargs):
print(10)

@staticmethod
def func2(*args, **kwargs):
print(10)

@classmethod
def func3(cls, *args, **kwargs):
print(10)


func1() # TypeError: func1() missing 1 required positional argument: 'self'
func2() # TypeError: 'staticmethod' object is not callable
func3() # TypeError: 'classmethod' object is not callable

class Cls():
def func1(self, *args, **kwargs):
print(10)

@staticmethod
def func2(*args, **kwargs):
print(10)

func1(None) # 10
func2.__func__() # 10

派生与类继承

派生类会自动继承基类的所有属性:类的局部命名空间(属性字典)中找不到的属性,会继续到基类中查找。派生类属性可以覆盖掉基类的同名属性。值得注意的,在派生类中调用基类未被覆盖的方法时,该基类方法如果调用了另一个基类方法,最终实际调用的可以是被覆盖之后的派生类方法!😂️

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Cat():
total_cats = 0
def __init__(self, name, age, color):
self.name = name
self.age = age
self.color = color
Cat.total_cats += 1

mycat = Cat('Kitty', 3, 'black')

class WildCat(Cat):
pass

print(vars(WildCat))
help(WildCat) #total_cats=1

派生类可通过<name_basecls>.<attr>直接访问基类被派生类覆盖掉的的属性,也可借助内置函数super()访问。单继承时,super()可实现不具名的情况下获取基类属性;多继承时,super()会影响属性搜索的默认顺序。super()有两个可选参数,前一个(type)指定搜索的起点,后一个(obj/type)则决定搜索的顺序(确切说是由其属性__mro__决定)。
注:mro代表Method Resolution Order,如果某个对象是派生自其他基类的,则其帮助文档(help(<obj_name>))的的第一条就是mro。

1
2
3
4
5
6
class WildCat(Cat):
def __init__(self, name, age, color, habitat):
super().__init__(name, age, color) #super(WildCat, self)
self.habitat = habitat
def func(self):
super().func() #super(WildCat, self)

多继承时,属性搜索顺序可大致理解为:深度优先、由左到右(depth-first, left-to-right),对重复出现的类以最后出现的位置为准,而不会搜索二次。下面例子中:Derived中找不到的属性,会到Base1及其基类中查找,仍找不到才会到Base2及其基类,依次类推;但若存在菱形结构,如Base1, Base2都继承自Base4,则Base4会在Base2之后搜索。

1
2
class Derived(Base1, Base2, Base3):
pass

当然实际情况会更复杂,可参考Guido van Rossum对此的介绍,基本原则就是由左到右、由特殊到一般(深度优先)的同时,并确保单调性,否则将报错。比如类X派生自C, D,而C, D又都派生自A, B,但定义C时将A放在了前面(左边),定义D时却又将B放在了前面,最终解析时会同时出现相互冲突的A-B与B-A序,无法保持单调,因此会报错。更深入的理解可查看此处对于C3线性化算法的介绍。

继承(Inheritance)与委派(delegation) why?
与继承相类似的还有委派,可在不进行继承的情况下,使一个类获得另一个类的部分功能。继承中,派生类自动获得全部基类属性;而在委派中,委派方主动选取指定方法。Python中委派通过特殊方法__getatrr__(及内置函数getattr) 实现

私有属性 __xyz

Python中变量默认都是公共的,名字以下划线开始的变量会被视作非公开的“私有”成员。但严格来说Python中并不存在真正的私有,要访问总可以做到,所谓私有只是一种约定:名字以双下划线起始,且至多以单下划线结束(__xyz)的变量会自动触发Python的名称改写。除了类自身正常访问外,派生类及外部引用时需手动修改名字:访问方式由通常的<obj>.<attr> 被改写为<obj>._<name_cls><attr>

注意,不要混淆类私有属性与模块的保护变量:模块保护变量,名称以下划线起始(_xyz),效果是不会被from <module> import *导出。类内部定义的变量不会出现于全局命名空间,本来就不会被导出,用这种命名方式没有任何效果。

最后,还有一些名字以双下划线起始和结束的方法(__xyz__),如__init__。这些方法通常是Python内建的特殊方法,用于在特定条件下执行特定操作,具体可参考Python官方文档中对Datamodel的介绍。注意,这些特殊方法并非私有属性,不会触发名称改写。

属性装饰器 @property

方法调用必须带括号,而Python内置的@property装饰器可将方法转化为同名的只读数据属性,从而无需括号即可访问;@<method>.setter, @<method>.deleter则进一步为上述只读数据属性增加覆写和删除操作,而不再是只读。需注意的是,一旦使用了property装饰器,就只能通过数据属性方式访问,而无法再通过方法调用的方式访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class People():
def __init__(self, first, last):
self.first = first
self.last = last

@property
def fullname(self):
return f'{self.first} {self.last}'

@fullname.setter
def fullname(self, name):
first, last = name.split(' ')
self.first = first
self.last = last

@fullname.deleter
def fullname(self):
print('Delete Name')
self.first = None
self.last = None

person = People('Jonn', 'Smith')
print(person.fullname) # Jonn Smith
person.fullname = 'Julian Brown'
print(person.fullname) # Julian Brown
del person.fullname # Delete Name

特殊方法 datamodel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#### 不建议自定义的特殊方法 #####
# provide access to the implementation and are not intended for general use
'对象' : __class__
'模块' : __dict__, __doc__, __name__, __file__, __annotations__
'类' : __dict__, __doc__, __name__, __module__, __base__, __annotations__
'实例' : __dict__, __class__
'''可调用类型特殊方法'''
'内置函数/方法' : __doc__, __name__, __self__, __module__
'用户定义函数' : __doc__, __name__, __qualname__, __module__, __defaults__,
__code__, __globals__, __dict__, __closure__, __kwdefaults__,
__annotations__
'实例方法' : __self__, __func__, __doc__, __name__, __mode__
#'类实例化': __new__, __init__
#'生成器' : __next__

########### 协程 ###############
'异步等待' : __await__,
'异步迭代' : __aiter__, __anext__
'异步上下文': __aenter__, __aexit__

### 可自定义的特殊方法(算符重载) ###
# When implementing a class that emulates any built-in type,
# it is important that the emulation only be implemented to the degree that
# it makes sense for the object being modelled.
'基本方法' : __new__, __init__, __del__; __repr__, __str__, __format__;
__lt__, __le__, __eq__, __ne__, __gt__, __ge__;
__bytes__, __hash__, __bool__
'属性访问 ': __getattr__, __getattribute__, __setattr__, __delattr__, __dir__
'<描述符>' __get__, __set__, __delete__, __set_name__
__slots__
'类创建' : __init_subclass__
'实例/继承检查' : __instancecheck__, __subclasscheck__
'上下文管理': __enter__, __exit__
'模拟泛型' : __class_getitem__
'模拟调用' : __call__
'模拟容器' : __len__, __length_hint__, __getitem__, __setitem__, __delitem__
__iter__, __reversed__, __contains__, __missing__
'模拟数值' : __add__, __sub__, __mul__, __matmul__, __truediv__, __floordiv__,
__pow__, __mod__, __divmod__, __lshift__, __rshift__,
__and__, __or__, __xor__;
__complex__, __int__, __float__,
__neg__, __pos__, __abs__, __invert__,
__index__, __round__, __trunc__, __floor__, __ceil__

其中最常用的特殊方法有:
__init__ 用于初始化实例对象
__repr__ 被repr()调用,主要是为调试、日志等提供明确、有效的信息(developer-friendly)
__str__ 被str(), print()调用,主要面向用户,提供对象的直观信息(user-friendly)
当只有__repr__时,__str__会自动回落至__repr__,反之则不然,因此至少应提供前者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Cat():
def __init__(self, name, age, color):
self.name = name
self.age = age
self.color = color

mycat = Cat('Kitty', 3, 'black')
print(mycat) # <__main__.Cat object at 0x7f48e96d9910>

class Cat():
def __init__(self, name, age, color):
self.name = name
self.age = age
self.color = color

def __repr__(self):
return f'{self.name}: {self.color} color, {self.age} years old.'

mycat = Cat('Kitty', 3, 'black')
print(mycat) # Kitty: black color, 3 years old.

最后,需要注意的是,特殊方法必须在类中定义,而非实例中,才能确保隐式调用正常。

1
2
3
4
5
6
7
8
9
10
class A:
pass

a = A()
a.__len__ = lambda: 5

# 显式调用
print(a.__len__()) # 5
# 隐式调用
print(len(a)) # TypeError: object of type 'A' has no len()

其背后机制在于,特殊方法的隐式调用会自动绕过实例属性及__getattribute__方法,原因则是为了避免“元类混淆”(以及提升查找速度),具体可参考官方文档,我没看懂😵️

属性访问控制 descriptor

  • 一般属性:__getattribute__, __getattr__, __setattr__, __delattr__, __dir__
  • 描述符属性(descriptor):__get__, __set__, __delete__, __set_name__
  • 属性插槽:__slots__
    推荐阅读Guido van Rossum所写的The inside story on new-style classes,以了解这些特殊属性被引入的目的。

一般属性访问
属性获取默认通过调用__getattribute__实现,__getattr__用于处理属性访问失败的情况(捕获AttributeError),__setattr____delattr__用于修改(或添加)和删除属性。
__dir__用于列出对象所有属性及方法名,可由vars()调用,不过通常会用dir()返回经过排序的(和缩减)的属性方法/列表(缺少自动补齐时很有用)。Is there a built-in function to print all the current properties and values of an object?
注1:对于特殊方法,隐式调用时会跳过__getattribute__(及实例属性)。
注2:重写属性访问方法时,若__getattribute__方法中出现self.<attr>__setattr__方法中出现self.<attr> = <value>__delattr__方法中出现del self.<attr>则意味着方法会指向自身,从而触发无限递归,应注意避免。此外,在获取(get)或设置(set)属性时,应通过super()将相关操作向基类传递。

描述符属性
上面介绍的特殊方法用于控制一般的属性访问,但有些情况我们可能需要调整某个特定属性的访问:比如一般的方法访问,会返回函数与实例对象指针所打包成的方法对象,但如果要实现类方法,就需要调整为打包函数与类对象指针。直接修改前面的特殊方法显然行不通,它们针对的是所有属性,任何修改都将改变其它属性的访问。
为此,Python另外提供了一组特殊方法__get__, __set__, __delete__, __set_name__。属性访问时,在属性字典中找到属性后,会先确认其是否定义了这些特殊方法,若定义了则调用相应方法,未定义时才会按一般属性访问规则操作。定义了这些特殊方法的属性被称为描述符。需注意的是,描述符必须作为类属性定义,而不能定义为实例属性。关于描述符的具体使用指南可参考Descriptor HowTo Guide

根据所定义的特殊方法,描述符被分为数据描述符和非数据描述符:前者指定义有__set____delete__任一方法(或两者都定义)的属性,相对的,两者均未定义的为非数据描述符。通常,非数据描述符只定义__get__,而数据描述符则会同时定义__get__, __set__
描述符只能作为类属性,且对于同时定义了__get____set__的(数据)描述符,根据属性访问规则,始终会覆盖掉实例的同名属性,无法被重写;而非数据描述符则会被实例的同名属性覆盖。上文的类方法、静态方法及方法变的属性都属于描述符,装饰器@classmethod, @staticmethod, @property所做的就是在其装饰的方法中插入__get__, __set__等特殊方法,从而改变相应方法的访问方式(将其变为描述符)。其中类方法及静态方法为非数据描述符,可以被实例覆盖;@property则为数据描述符,无法被实例覆盖!

1
2
3
4
from pprint import pprint as print
print(vars(classmethod)) # __get__
print(vars(staticmethod)) # __get__
print(vars(property)) # __get__, __set__

属性插槽
描述符只能以类属性定义,同时操作优先级高于一般属性,因此任何实例对象的属性访问都要先检查类的属性字典,以确认是否为描述符。由于担心描述符的额外性能负担,在引入描述符的同时引入了属性插槽(__slots__)。属性插槽允许人们明确声明实例对象所拥有的属性:__slots__可赋值为字符串或由字符串组成的容器类型(列表、元组等),对应于实例对象的属性列表,插槽中未声明的属性不能被实例所新建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A:
__slots__ = ('attr1', 'attr2')

def __init__(self, attr1):
self.attr1 = attr1

attr3 = 3

def func():
pass

a = A(1)
a.attr2 = 2
a.attr3 = 3 # AttributeError: 'A' object attribute 'attr3' is read-only
a.attr4 = 4 # AttributeError: 'A' object has no attribute 'attr4'

注意:属性插槽中所声明的变量名只能作为实例属性,而不能用作类属性。因为属性插槽的实现是基于描述符,其中的每个属性都会被转换为描述符。而如果在类定义中对变量赋值(从而成为类属性),就会覆盖掉描述符。

1
2
3
4
5
6
class A:
__slots__ = ('attr1', 'attr2')

attr1 = 1 # ValueError: 'attr1' in __slots__ conflicts with class variable
def attr2(): # ValueError: 'attr2' in __slots__ conflicts with class variable
pass

属性插槽能提升属性的访问速度,同时避免了为实例对象创建属性字典__dict__,可节省内存,尤其是实例对象非常多时,可实现相当可观的内存节省。定义了__slots__的类实例化时会阻止__dict__(及__weakref__)的自动创建,但却无法阻止其基类或者派生类自动创建__dict__,此时__slots__优势将不复存在。因此想通过属性插槽提升性能或节省内存,就要贯彻到底,从基类到所有派生类都定义__slots__。当然属性插槽还可以起到“保护”作用,如前面所说,属性插槽中未声明的属性是不允许实例新建的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A:
__slots__ = ('attr1', 'attr2')

print(A().__dict__) # AttributeError: 'A' object has no attribute '__dict__'

class B(A):
pass

print(B().__dict__) # {}

class A:
pass

class B(A):
__slots__ = ('attr1', 'attr2')

print(B().__dict__) # {}

数据类 @dataclasses

根据前面的介绍:在实例化时初始化对象需要定义特殊方法__init__;打印对象时返回有意义的内容,还至少需要定义特殊方法__repr__。这些方法的使用很普遍,而实现起来又需要很多模式化的代码,于是人们希望能自动化这些方法的定义,尤其是对主要用于存储数据的类。数据类(Data Class)由此诞生,通过@dataclass装饰器在创建对象时使用,自动为用户定义的类添加__init__, __repr__等特殊方法,简化类定义的同时增加代码可读性。数据类在使用上有点类似命名数组(namedtuple),不过后者为不可变类型,且存在一些其它限制,具体可参考这里的讨论。注意,数据类需Python 3.7+。
数据类使用变量注释(variable annotations)对其需要初始化的数据属性进行描述,其中变量注释是必须的。没有加注释的变量会被视为类属性,从而不会加入__init__函数的参数列表!注释可以是任何Python表达式,但最好能提供有用信息,推荐使用类型提示(typing hints)。虽然变量注释是必须的,但其中的类型提示并不具有强制效用,只是“注释”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from dataclasses import dataclass
from pprint import pprint as print

class CatA():
def __init__(self, name: str, age: int, color: str = "white"):
self.name = name
self.age = age
self.color = color

def __repr__(self):
return f'{self.name}: {self.color} color, {self.age} years old.'

def mew(self):
return 'miao ...'

@dataclass
class CatB:
name: 'name of the cat, should be a string'
age: int
color: str = "white"

def mew(self):
return 'miao ...'

print(vars(CatA)) #__init__, __repr__, mew
print(vars(CatB)) #__init__, __repr__, __annotations__, __eq__, color, mew

mycat_a = CatA("ketty", 4)
mycat_b = CatB("Mimi", 2)
print(mycat_a)
print(mycat_b)
print(mycat_b.mew())

可以看到除了数据属性的定义方式有所变化,数据类的方法属性定义与普通类没有区别。使用@dataclasses后,类定义被大大简化:自动生成了__init__, __repr__;变量注释信息被自动加入__annotations____hash__被设为None,表示数据类实例不可hash;还添加了__eq__方法。

1
2
3
4
5
6
7
8
9
Cat = CatA
mycat = Cat("black", 4.8, 3)
mycat1 = Cat("black", 4.8, 3)
print(mycat1 == mycat) # False, equality of obj identity(ID)

Cat = CatB
mycat = Cat("black", 4.8, 3)
mycat1 = Cat("black", 4.8, 3)
print(mycat1 == mycat) # True, equality of obj data attr

需要注意的是,数据类自动生成的__init__函数的参数顺序与属性的定义顺序是对应的。而Python中定义函数时,要求不指定默认值的参数位于指定默认值的参数之前,因此定义数据类时,设置默认值的属性必须放在最后面。而且这一规则对于类继承同样有效,即由数据类派生新数据类时,若基类中属性设置了默认值,则派生类所有属性也都需要有默认值。更复杂的默认参数设置可使用数据类提供的field()函数,具体可参考官方文档。

1
2
3
4
5
6
# TypeError: non-default argument 'color' follows default argument
@dataclass
class Cat:
name: 'name of the cat, should be a string'
age: int = 1
color: str

最后,make_dataclass()函数可进一步可简化数据类的创建,但随着问题变复杂,使用这种方式代码可读性非常差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from dataclasses import make_dataclass
from pprint import pprint as print

# variable annotations is not mandatory in this case
Cat = make_dataclass('Cat', ['name', 'age', 'color'])
print(vars(Cat)) #__init__, __repr__, __annotations__, __eq__

# add variable annotations & default value
Cat = make_dataclass('Cat', [('name', 'arbitrary comments ...'), \
('age', int), ('color', str, 'white')])
print(vars(Cat)) #__init__, __repr__, __annotations__, __eq__, color

def mew(self):
return 'miao...'
# add methods
Cat = make_dataclass('Cat', [('name', 'arbitrary comments ...'), \
('age', int), ('color', str, 'white')],
namespace={'mew' : mew})
print(vars(Cat)) #__init__, __repr__, __annotations__, __eq__, color, mew

mycat = Cat("Mimi", 2)
print(mycat)
print(mycat.mew())

元类 metaclasses

元类(metaclass)指类的类(the class of a class),本文开篇提到过类默认属于名为type的特殊类,这个名为type的类就是Python内置的元类。类用于批量创建(实例化)实例对象,元类用于批量创建类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A:
pass

# 等价于
class A(object, metaclass=type):
pass

# 等价于type(<name:str>, <bases:tuple>, <namespace:dict>)
A = type('A', (object,), {})

a = A()
print(type(a)) # <class '__main__.A'>
print(type(A)) # <class 'type'>
print(type(object)) # <class 'type'>
print(type(type)) # <class 'type'>

自定义元类非常简单:(直接或间接)由type派生的类都属于元类。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyType(type):
pass

class MyNewType(MyType):
pass

class B(metaclass=MyNewType):
pass

b = B()
print(type(b)) # <class '__main__.B'>
print(type(B)) # <class '__main__.MyNewType'> # MyNewType instead of type
print(type(MyNewType)) # <class 'type'>

可以看到类B属于自定义的MyNewType(元)类,而不再是默认的type(元)类,而且B的所有派生类也都将属于MyNewType(元)类。

创建的具体过程为:

  1. 解析MRO(Method Resolution Order)
  2. 确定元类(继承关系上最近的)
  3. 准备命名空间(<metaclass>.__prepare__)
  4. 运行类主体代码
  5. 创建类(<metaclass>(<name>:str, <bases>:tuple, <namespace>:dict, **kwds))。

上述最后一步中会自动调用特殊方法__new__(类实例化__init__之前也会调用__new__)。

自定义元类的主要作用就是可以在类创建过程中“做手脚”,比如修改__prepare____new__,又或者添加其它自定义方法,进而实现接口检查、日志、委派、属性创建、单例(singleton)、代理、框架、自动资源锁定/同步等等功能。以派生类接口(方法)检查为例:

1
2
3
4
5
6
7
8
9
10
11
12
class Meta(type):
def __new__(cls, name, bases, namespace, **kwds):
#if 'func' not in namespace:
if not hasattr(cls, 'func'):
raise TypeError('func() method not defined')
return super().__new__(cls, name, bases, namespace, **kwds)

class Base(metaclass=Meta):
pass

class Derived(Base):
pass # TypeError: func() method not defined

元类过于强大,使用起来也有点复杂,若只是想简单干预派生类的创建,Python 还提供了特殊方法__init_subclass__。定义了该方法的类,其派生类在创建前(类创建的最后一步中,__new__执行之后)会自动调用该方法,类似于类实例化时自动调用__init__方法:

  • 类实例化:__new__ + __init__ 类实例化时的实参会先传给__new__,再传给__init__
  • 类继承:__new__ + __init_subclass__ 类继承时的关键字参数(位置参数全部用作基类)会先传给__new__,再传给__init_subclass__
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Meta(type):
def __new__(cls, name, bases, namespace):
print('__new__ of Meta Class')
return super().__new__(cls, name, bases, namespace)

class Base(metaclass=Meta): # __new__ of Meta Class
def __new__(self):
print('__new__ of Base class')
return super().__new__(self)
# caution: the return here is needed!
# if __new__() does not return an instance of cls
# then the __init__() method will not be invoked.

def __init_subclass__(cls):
print('__init_subclass__ of Base Class')

def __init__(self):
print('__init__ of Base Class')

class Derived(Base): # __new__ of Meta Class; __init_subclass__ of Base Class
pass

b = Base() # __new__ of Base Class; __init__ of Base Class
d = Derived() # __new__ of Base Class; __init__ of Base Class

注意:如上所示,虽然实例化与继承都会调用__new__,但两者却不是同一个__new__!!类实例化时的__new__是类自身(或其基类)中所定义的,类继承时的__new__则是其元类中所定义的,而非其基类中所定义的。这也是为什么控制类创建需要用到元类。而另一方面,__init_subclass__却只需要在基类中定义,无需用到元类,因为它被引入的意义就在于不借助元类干预派生类的创建。基类中的__init_subclass__相比元类中的__new__,唯一区别在于前者无法作用于基类自身。利用__init_subclass__前面接口检查的例子可简化为:

1
2
3
4
5
6
7
8
class Base:
def __init_subclass__(cls):
#if 'func' not in vars(cls):
if not hasattr(cls, 'func'):
raise TypeError('func() method not defined')

class Derived(Base):
pass # TypeError: func() method not defined

注意:这里__init_subclass__方法有点特别,在基类中是作为普通的“实例方法”定义的(没有用装饰器),在派生类创建前被调用时,却是作为派生类的“类方法”,传入的首个参数是派生类自身(所以形参用了cls),这中间存在一个隐式的转换,由Python解释器自动完成。

抽象基类 ABC

鸭子类型直接提供了类似于原型(protocol)/接口(interface)的便利性,但也存在局限:

  • 对象只有被调用才会判断是否提供了必要的接口,无法在创建类时强制其实现接口
  • 当要检查大量接口时,hasattr()或try…except…就不再优雅,代码可读性降低
  • hasattr()在处理特殊方法时存在问题(?)

于是Python引入了抽象基类(Abstract Base Classes, ABC)的概念,以真正实现接口定义。此外,还提供了很多常用的抽象基类(collections.abc, numbers, io, importlib.abc)。推荐阅读PEP 3119了解相关概念。

与抽象类相关的,Python 3.8中引入了typing.Protocol用于原型类的静态类型检查。

抽象类的定义
abc模块提供了自定义抽象类的必要工具,包括:元类abc.ABCMeta、基类abc.ABC以及抽象方法装饰器@abc.abstractmethodABCMetaABC的关系,类似内置的typeobject对象的关系,前者是抽象类的元类,需依照元类的用法使用;后者是普通的抽象类,可直接派生新的抽象类,以便于在不接触元类的前提下使用抽象类。

1
2
3
4
5
6
7
from abc import ABC, ABCMeta

class MyABC(metaclass=ABCMeta):
pass

class MyABC(ABC):
pass

而所谓“抽象类”,是指声明了必要的接口(方法),却并未给出实现的类,这些未给出实现的方法则被称为抽象方法。装饰器@abc.abstractmethod就是用于定义抽象方法,可与方法的其它装饰器@classmethod, @staticmethod, @property组合使用,注意此时@abc.abstractmethod需放在最内层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from abc import ABC, ABCMeta, abstractmethod

class Container(ABC):
@abstractmethod
def __contains__(self, x):
return False

@classmethod
def __subclasshook__(cls, C):
if cls is Container:
if any("__contains__" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented

@classmethod
@abstractmethod
def func(self):
return False

不过除非是创建框架或基础库,通常你不需要自定义抽象类,而应直接使用Python内置的抽象类:容器(collections.abc)、数值(numbers)、数据流(io)以及导入(importlib.abc)。

抽象类的使用
Python中的抽象类有两种使用方式:

  • 继承:直接继承抽象类,这种方式派生类必须实现所有的抽象方法才能实例化;
  • 注册:任意类都可直接注册为抽象类的虚拟派生类,不受上述抽象方法的限制;
    这里的任意类包括内置类型及其它抽象类等。注册为虚拟派生类后,issubclass(), isinstance()将会返回True;但该类与抽象类间不存在实际继承关系,因此抽象类不会出现在其方法查找链MRO中,抽象类中定义的方法(非抽象方法)也无法被访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from collections.abc import Sequence

class Seq(Sequence):
pass

Seq() # TypeError: Can't instantiate abstract class Seq with abstract methods ...

@Sequence.register # register
class Seq():
pass

myseq = Seq()
assert issubclass(Seq, Sequence)
assert isinstance(myseq, Sequence)

继承的方式,可提供类似上一节中元类或__init_subclass__所实现的强制接口检查;而注册的方式,则只是声明实现了必要的接口(抽象方法),具体是否实现完全靠自觉。

注册机制的背原理为改写issubclass, isinstance,所对应的特殊函数为__subclasshook__ (更底层则是__instancecheck__, __subclasscheck__),这意味自定义抽象类时,通过改写该特殊方法,你可以获得更大的自由度。当然如前面所说,通常你不应自定义抽象类。

最后,关于抽象类ABCs的使用,总结如下:

  • 尽量依照鸭子类型,直接try…except…或用hasattr(),避免isinstance()
  • 如果上面的方式太繁琐,则可使用ABCs + isinstance(),尽量避免强制具体类;
  • 除非是创建框架或基础库,通常你无需自定义ABCs,Python内置了常用的ABCs;
  • 使用内置抽象基类时,应选择你希望支持的最一般的(接口要求最少的)ABCs。

扩展阅读
Guido van Rossum: The History of Python
James Powell: So you want to be a Python expert?