第八章:类与对象
8.1 改变对象的字符串显示
__str__()
和 __repr__()
方法。
__repr__()
方法返回一个实例的代码表示形式,通常用来重新构造这个实例。内置的 repr() 函数返回这个字符串,跟我们使用交互式解释器显示的值是一样的。__str__()
方法将实例转换为一个字符串,使用 str() 或 print() 函数会输出这个字
符串。
8.2 自定义字符串的格式化
定义 __format__()
方法
8.3 让对象支持上下文管理协议
实现 __enter__()
和__exit__()
方法。
8.4 创建大量对象时节省内存方法
给类添加 __slots__
属性来极大的减少实例所占的内存。
1 | class Date: |
- 当你定义
__slots__
后,Python 就会为实例使用一种更加紧凑的内部表示。- 实例通过一个很小的固定大小的数组来构建,而不是为每个实例定义一个字典,这跟元组或列表很类似。
- 在
__slots__
中列出的属性名在内部被映射到这个数组的指定小标上。- 使用 slots 一个不好的地方就是我们不能再给实例添加新的属性了,只能使用在
__slots__
中定义的那些属性名。
- 关于
__slots__
的一个常见误区是它可以作为一个封装工具来防止用户给实例增
加新的属性。- 尽管使用 slots 可以达到这样的目的,但是这个并不是它的初衷。
__slots__
更多的是用来作为一个内存优化工具。
8.5 在类中封装属性名
约定是任何以单下划线 _ 开头的名字都应该是内部实现。
类定义中使用两个下划线 (__) 开头的命名 :
1
2
3
4
5
6
7
8
9
10 class B:
def __init__(self):
self.__private = 0
def __private_method(self):
pass
def public_method(self):
pass
self.__private_method()
- 使用双下划线开始会导致访问名称变成其他形式。
- 比如,在前面的类 B 中,私有属性
__private
,__private_method
会被分别重命名为_B__private
和_B__private_method
。- 这时候你可能会问这样重命名的目的是什么,答案就是继承——这种属性通过继承是无法被覆盖的。
- 两种不同的编码约定 (单下划线和双下划线) 来命名私有属性
- 到底哪种方式好呢?大多数而言,你应该让你的非公共名称以单下划线开头。
- 如果你清楚你的代码会涉及到子类,并且==有些内部属性应该在子类中隐藏起
来==,那么才考虑使用双下划线方案。
8.6 创建可管理的属性
给某个实例 attribute 增加除访问与修改之外的其他处理逻辑,比如类型检查或合法性验证。
自定义某个属性的一种简单方法是将它定义为一个 property。
1 | class Person: |
如果只是定义getter,并不定义setter和delete的话,可以将方法改为属性访问:
1 | import math |
8.7 调用父类方法
在子类中调用父类的某个已经被覆盖的方法。
使用 super() 函数
1 | class A: |
Python 是如何实现继承的
对于定义的每一个类,Python 会计算出一个所谓的方法解析顺序 (MRO) 列表。这个 MRO列表就是一个简单的所有基类的线性顺序表。
1
2
3 print(C.__mro__)
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
<class '__main__.Base'>, <class 'object'>)为了实现继承,Python 会在 MRO 列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。
8.8 子类中扩展 property
在子类中,你想要扩展定义在父类中的 property 的功能
在子类中扩展一个 property 可能会引起很多不易察觉的问题,因为一个 property
其实是 getter、setter 和 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
27
28
29
30
31
32
33
34 class Person:
def __init__(self, name):
self.name = name
def name(self):
return self._name
def name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._name = value
def name(self):
raise AttributeError("Can't delete attribute")
class SubPerson(Person):
def name(self):
print('Getting name')
return super().name
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
def name(self):
print('Deleting name')
super(SubPerson, SubPerson).name.__delete__(self)单个方法的重新定义:
1
2
3
4
5 class SubPerson(Person):
def name(self):
print('Getting name')
return super().name
1
2
3
4
5 class SubPerson(Person):
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
简单来说:
三个方法的重新定义:
1只重新定义一个方法:
1也就是说 :
获取方法的唯一途径是使用类变量而不是实例变量来访问它。
这也是为什么我们要使用super(SubPerson, SubPerson) 的原因。
8.9 创建新的类或实例属性
创建一个新的拥有一些额外功能的实例属性类型,比如类型检查。
通过描述器类
- 描述器就是一个实现了三个核心的属性访问操作 (get, set, delete) 的类,分别为
__get__()
、__set__()
和__delete__()
这三个特殊的方法。- 这些方法接受一个实例作为输入,之后相应的操作实例底层的字典。
- 为了使用描述器,需将这个描述器的实例作为类属性放到一个类的定义中。
1 | class Integer: |
注意:
==描述器只能在类级别被定义,而不能为每个实例单独定义==。
8.10 使用延迟计算属性(重要)
你想将一个只读属性定义成一个 property,并且只在访问的时候才会计算结果。但是一旦被访问后,你希望结果值被缓存起来,不用每次都去计算。
定义一个延迟属性的一种高效方法是通过使用一个描述器类
1 | class lazyproperty: |
这种方案有一个小缺陷就是计算出的值被创建后是可以被修改的。
我们可以稍微修改属性名:
1 | def lazyproperty(func): |
8.11 简化数据结构的初始化
一个基类中写一个公用的__init__()
函数
1 | import math |
如果还想支持关键字参数,可以将关键字参数设置为实例属性:
1 | class Structure2: |
甚至你还能传入额外的属性:
1 | extra_args = kwargs.keys() - self._fields |
8.12 定义接口或者抽象基类
使用 abc 模块可以很轻松的定义抽象基类:
1 | from abc import ABCMeta, abstractmethod |
抽象类的目的就是让别的类继承它并实现特定的抽象方法
1
2
3
4
5
6 class SocketStream(IStream):
def read(self, maxbytes=-1):
pass
def write(self, data):
pass
抽象基类的一个主要用途是在代码中检查某些类是否为特定类型,实现了特定接口
1
2
3
4 def serialize(obj, stream):
if not isinstance(stream, IStream):
raise TypeError('Expected an IStream')
pass