《流畅的Python》读书笔记11(第九章:符合Python风格的对象)

120 阅读5分钟

得益于Python数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子类型(duck typing):我们只需按照预定行为实现对象所需的方法即可。

前一章分析了很多内置对象的结构和行为,这一章则自己定义类,而且让类的行为跟真正的Python对象一样。

9.1 对象表示形式

每门面向对象语言至少都有一种获取对象的字符串表示形式的标准方式。Python提供了两种方式。

  • repr() 以便于开发者理解的方式返回对象字符串表示形式

  • str() 以便于用户理解的方式返回对象的字符串表示形式。

我们要实现__repr__和__str__特殊方法,为repr()和str()提供支持。

为了给对象提供其他的表示形式,还会用到另外两个特殊方法:__bytes__和__format__。__bytes__方法和__str__方法类似:bytes()调用它获取对象的字节序列表示形式。而__format__方法会被内置的format()和str.format()方法调用,使用特殊的格式代码显示对象的字符串表示形式。

9.2 再谈向量类

为了说明用于生成对象表示形式的众多方法,我们将使用一个Vector2d类。

示例9-1 vector2d_v0.py:目前定义的都是特殊方法

from array import array
import math


class Vector2d:
    typecode = 'd'

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

    def __iter__(self):
        return (i for i in (self.x, self.y))

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self)

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self)))

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __abs__(self):
        return math.hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

示例9-2 Vector2d实例有多种表示形式

v1 = Vector2d(3, 4)
print(v1.x, v1.y)
3.0 4.0
x, y = v1
x, y
(3.0, 4.0)
v1
Vector2d(3.0, 4.0)
v1_clone = eval(repr(v1)) # Vector2d(3.0, 4.0)
v1 == v1_clone
True
print(v1)
(3.0, 4.0)
octets = bytes(v1)
octets
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
abs(v1)
5.0
bool(v1), bool(Vector2d(0,0))
(True, False)

定义__iter__方法,把Vector2d实例变成可迭代的对象,这样才能拆包(例如 x, y = v1)。这个方法的实现方式很简单,直接调用生成器表达式一个接一个产出分量。否则的话,会出现如下错误:

x, y = v1
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: cannot unpack non-iterable Vector2d object

9.3 备选构造方法

我们可以把Vector2d实例转换成字节序列了;同理,也应该能从字节序列转换成Vector2d实例。

实例9-3

@classmethod
def frombytes(cls, octets):
    typecode = chr(octets[0])
    memv = memoryview(octets[1:]).cast(typecode)
    return cls(*memv)

从字节序列转换成Vector2d实例

v1 = Vector2d(3,4)
octets = bytes(v1)
v2 = Vector2d.frombytes(octets)
v2
Vector2d(3.0, 4.0)

9.4 classmethod 与 staticmethod

classmethod 改变了调用方法的方式,因此类方法的的第一个参数是类本身,而不是实例。

staticmethod装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实静态方法就是普通的函数,只是碰巧在类的定义体中,而不是模块层定义。

实例9-4 比较classmethod 与 staticmethod的行为

>>> class Demo:

...     @classmethod

...     def klassmeth(*args):

...         return args

...     @staticmethod

...     def statmeth(*args):

...         return args

... 

>>> Demo.klassmeth()

(<class '__main__.Demo'>,)

>>> Demo.klassmeth('spam')

(<class '__main__.Demo'>, 'spam')

>>> Demo.statmeth()

()

>>> Demo.statmeth('spam')

('spam',)

>>>

不管怎样调用Demo.klassmeth,它的第一个参数始终是Demo类

9.5 格式化显示

内置的format()函数和str.format()方法把各个类型的格式化方式委托给相应的.__format__(format_spec)方法。format_spec是格式说明符。

如果没有定义__format__方法,从object继承的方法会返回str(my_object)。我们为Vector2d类定义了__str__方法,因此可以这样做:

>>> v1 = Vector2d(3,4)
>>> format(v1)
   '(3.0, 4.0)'

然而,如果传入格式说明符,object.__format__方法会抛出TypeError:

format(v1, '.3f')
Traceback (most recent call last):
  ...
TypeError: unsupported format string passed to Vector2d.__format__

我们将实现自己的微语言来解决这个问题。

def __format__(self, format_spec):
    components = (format(c, format_spec) for c in self)
    return '({},{})'.format(*components)

9.6 可散列的Vector2d

按照定义,目前Vector2d实例是不可散列的,因此不能放入集合(set)中:

v1 = Vector2d(3,4)
format(v1)
'(3.0,4.0)'
hash(v1)
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Vector2d'
set([v1])
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Vector2d'

为了把Vector2d实例变成可散列的,必须使用__hash__方法(还需要__eq__方法,签名已经实现了)。此外,还要让向量不可变。

实例9-7 这里只给出让Vector2d不可变的代码

class Vector2d:
    typecode = 'd'

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y
    
    def __iter__(self):
        return (i for i in (self.x, self.y))

实例9-8 实现__hash__方法

def __hash__(self):
    return hash(self.x) ^ hash(self.y)

添加__hash__方法之后,向量变成可散列的了:

v1 = Vector2d(3, 4)
v2 = Vector2d(3.1, 4.2)
hash(v1), hash(v2)
(7, 384307168202284039)
set([v1, v2])
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}

9.7 Python的私有属性和”受保护的“属性

Python不能像Java那样使用private修饰符创建私有属性,但是Python有个简单的机制,能避免子类意外覆盖"私有"属性。

Python会把属性名存入实例的__dict__属性中,而且会在前面加上一个下划线和类型。这个语言特性叫作名称改写(name mangling)

实例9-10

v1 = Vector2d(3,4)
v1.__dict__
{'_Vector2d__x': 3.0, '_Vector2d__y': 4.0}
v1._Vector2d__x
3.0

9.8 使用__slots__类属性节省空间

默认情况下,Python在各个实例中名为__dict__的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存。通过__slots__类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。

class Vector2d:
    __slots__ = ('__x', '__y')
    
    typecode = 'd'
    
    ...

9.9 覆盖类属性

Python有个很独特的特性:类属性可用于为实例属性提供默认值。

但是,为不存在的实例属性赋值,会新建实例属性。不过,实例属性会把同名类属性遮盖了。