第八章:类与对象

8.1 改变对象的字符串显示

__str__() __repr__() 方法。

  • __repr__() 方法返回一个实例的代码表示形式,通常用来重新构造这个实例。内置的 repr() 函数返回这个字符串,跟我们使用交互式解释器显示的值是一样的。
  • __str__() 方法将实例转换为一个字符串,使用 str() 或 print() 函数会输出这个字
    符串。

8.2 自定义字符串的格式化

定义 __format__() 方法

8.3 让对象支持上下文管理协议

实现 __enter__()__exit__()方法。

8.4 创建大量对象时节省内存方法

给类添加 __slots__属性来极大的减少实例所占的内存。

1
2
3
4
5
6
class Date:
__slots__ = ['year', 'month', 'day']
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
  • 当你定义 __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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person:
def __init__(self, first_name):
self.first_name = first_name

@property
def first_name(self):
return self._first_name

@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value

@first_name.deleter
def first_name(self):
raise AttributeError("Can't delete attribute")

如果只是定义getter,并不定义setter和delete的话,可以将方法改为属性访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import math
class Circle:
def __init__(self, radius):
self.radius = radius

@property
def area(self):
return math.pi * self.radius ** 2

@property
def diameter(self):
return self.radius * 2

@property
def perimeter(self):
return 2 * math.pi * self.radius

8.7 调用父类方法

在子类中调用父类的某个已经被覆盖的方法。

使用 super() 函数

1
2
3
4
5
6
7
8
class A:
def spam(self):
print('A.spam')

class B(A):
def spam(self):
print('B.spam')
super().spam() # Call parent spam()

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

    @property
    def name(self):
    return self._name

    @name.setter
    def name(self, value):
    if not isinstance(value, str):
    raise TypeError('Expected a string')
    self._name = value

    @name.deleter
    def name(self):
    raise AttributeError("Can't delete attribute")


    class SubPerson(Person):
    @property
    def name(self):
    print('Getting name')
    return super().name

    @name.setter
    def name(self, value):
    print('Setting name to', value)
    super(SubPerson, SubPerson).name.__set__(self, value)

    @name.deleter
    def name(self):
    print('Deleting name')
    super(SubPerson, SubPerson).name.__delete__(self)
  • 单个方法的重新定义:

    1
    2
    3
    4
    5
    class SubPerson(Person):
    @Person.name.getter
    def name(self):
    print('Getting name')
    return super().name
    1
    2
    3
    4
    5
    class SubPerson(Person):
    @Person.name.setter
    def name(self, value):
    print('Setting name to', value)
    super(SubPerson, SubPerson).name.__set__(self, value)

简单来说:

  • 三个方法的重新定义:

    1
    @property
  • 只重新定义一个方法:

    1
    @Person.name.getter
  • 也就是说 :
    获取方法的唯一途径是使用类变量而不是实例变量来访问它。
    这也是为什么我们要使用super(SubPerson, SubPerson) 的原因。

8.9 创建新的类或实例属性

创建一个新的拥有一些额外功能的实例属性类型,比如类型检查。

通过描述器类

  • 描述器就是一个实现了三个核心的属性访问操作 (get, set, delete) 的类,分别为 __get__()__set__()__delete__() 这三个特殊的方法。
  • 这些方法接受一个实例作为输入,之后相应的操作实例底层的字典。
  • 为了使用描述器,需将这个描述器的实例作为类属性放到一个类的定义中。
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
class Integer:
def __init__(self, name):
self.name = name

def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name]

def __set__(self, instance, value):
if not isinstance(value, int):
raise TypeError('Expected an int')
instance.__dict__[self.name] = value

def __delete__(self, instance):
del instance.__dict__[self.name]

class Point:
x = Integer('x')
y = Integer('y')

def __init__(self, x, y):
self.x = x
self.y = y

注意:
==描述器只能在类级别被定义,而不能为每个实例单独定义==。

8.10 使用延迟计算属性(重要)

你想将一个只读属性定义成一个 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
35
36
37
38
39
40
41
42
43
44
45
46
47
class lazyproperty:
def __init__(self, func):
self.func = func

# 使用 __get__() 方法在实例中存储计算出来的值
def __get__(self, instance, cls):
if instance is None:
return self
else:
value = self.func(instance)
# 将值存储成实例的属性
setattr(instance, self.func.__name__, value)
return value


import math
class Circle:
def __init__(self, radius):
self.radius = radius

@lazyproperty
def area(self):
return math.pi * self.radius ** 2

@lazyproperty
def perimeter(self):
return 2 * math.pi * self.radius

c = Circle(4.0)

print(c.radius)
# 4.0

print(vars(c))
# {'radius': 4.0}

print(c.area)
# 50.26548245743669

print(vars(c))
# {'radius': 4.0, 'area': 50.26548245743669}

print(c.perimeter)
# 25.132741228718345

print(vars(c))
# {'radius': 4.0, 'area': 50.26548245743669, 'perimeter': 25.132741228718345}

这种方案有一个小缺陷就是计算出的值被创建后是可以被修改的。
我们可以稍微修改属性名:

1
2
3
4
5
6
7
8
9
10
11
12
13
def lazyproperty(func):
name = '_lazy_' + func.__name__

@property
def lazy(self):
if hasattr(self, name):
return getattr(self, name)
else:
value = func(self)
setattr(self, name, value)
return value

return lazy

8.11 简化数据结构的初始化

一个基类中写一个公用的__init__()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import math

class Structure1:
# 指定预期字段的类变量
_fields = []
def __init__(self, *args):
if len(args) != len(self._fields):
raise TypeError('Expected {} arguments'.format(len(self._fields)))

# 设置变量
for name, value in zip(self._fields, args):
setattr(self, name, value)


class Stock(Structure1):
_fields = ['name', 'shares', 'price']

class Point(Structure1):
_fields = ['x', 'y']

class Circle(Structure1):
_fields = ['radius']
def area(self):
return math.pi * self.radius ** 2

如果还想支持关键字参数,可以将关键字参数设置为实例属性:

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 Structure2:
_fields = []
def __init__(self, *args, **kwargs):
if len(args) > len(self._fields):
raise TypeError('Expected {} arguments'.format(len(self._fields)))

# 设置所有位置参数
for name, value in zip(self._fields, args):
setattr(self, name, value)

# 设置其余关键字参数(因为关键字参数在位置参数后面,所以使用切片)
for name in self._fields[len(args):]:
setattr(self, name, kwargs.pop(name))

# 检查是否有任何剩余的未知参数
if kwargs:
raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs)))


if __name__ == '__main__':
class Stock(Structure2):
_fields = ['name', 'shares', 'price']

s1 = Stock('ACME', 50, 91.1)
s2 = Stock('ACME', 50, price=91.1)
s3 = Stock('ACME', shares=50, price=91.1)

甚至你还能传入额外的属性:

1
2
3
extra_args = kwargs.keys() - self._fields
for name in extra_args:
setattr(self, name, kwargs.pop(name))

8.12 定义接口或者抽象基类

使用 abc 模块可以很轻松的定义抽象基类:

1
2
3
4
5
6
7
8
9
10
from abc import ABCMeta, abstractmethod
class IStream(metaclass=ABCMeta):

@abstractmethod
def read(self, maxbytes=-1):
pass

@abstractmethod
def write(self, data):
pass

抽象类的目的就是让别的类继承它并实现特定的抽象方法

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

8.13 实现数据模型的类型约束