FluentPythonCh09

23rd August 2017 at 5:16pm
Fluent Python

这章描述了一些常见的 dundle function,通过 Python 的 Data Model 实现一个 Pythonic Object。

Object Representations

Python 3 有几个用来做 object representation 的函数:

  • repr() <=> __repr__:给开发者读的,返回 str
  • str() <=> __str__:给用户读的,返回 str
  • bytes() <=> __bytes__:表示一个 object 的二进制形式,返回 bytes
  • format() <=> __format__:自定义的输出函数,主要用在 str.format()format() 内置函数上

An Alternative Constructor

区别于 __init__,用 classmethod 来初始化一个函数,以提供多种初始化的形式:

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

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

注意,这里的 frombytes() 函数的命名,是参照标准库中 array.array.frombytes() 命名的。我也观察到一些第三方的库使用了 fromxxx 的命名方式。这可以说是一种 Pythonic 的命名方式。

classmethod Versus staticmethod

classmethod 有个非常实用的场景,是给一个类提供额外的构造能力(如上面的 frombytes)。作者认为 staticmethod 没有应用场景,因为跟普通函数一模一样;如果你的函数跟某个类非常相关,那么在这个类附近写这个函数就好了,没必要用 staticmethod。我有点赞同。

Formatted Display

定义 __format__ 函数,可以在使用 built-in format(), str.format() 这些函数时,对你的类进行自定义的字符串输出。

A Hashable Vector2d

如果想把一个类放进 set 或作为 dict 的 key,需要让它 hashable。官方推荐的方式是将各个属性的 hash 值异或(bitwise XOR)起来:

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

一个实例的 hash 值不应该被改变(想想你已经把它用在 set 里面后,再改变 x, y 会引起的问题)。所以 x, y 也不应该被改变。可以用 @property 实现对类属性的只读控制,从而实现 immutable

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))

使用 immutable 来达到 hashable 并不是语言强制的,但是它通常是一个好的实践。

Private and “Protected” Attributes in Python

Python 并没有提供类属性的 private / protected 机制。但是它在 name mangling 上做了一些事情,来避免一些错误的访问。

class A:
    def __init__(self):
        self.__a = 1
        self._a = 2
    def say(self):
        print(self.__a)
        print(self._a)
>>> a = A()
>>> a.__dict__
{'_a': 2, '_A__a': 1}
>>> a.say()
1
2

可以看到带有双下划线的 __a 被装饰成 _A__a。在 A 类外部(甚至是子类)访问 __a 是会报错的。这样提供了一个机制来 避免 (而非 限制)错误的访问(你仍然可以用 _A__a 来访问到它)。

但是 __a 这种并不被推崇,因为引入了隐式的规则。不要使用它。用单下划线这种即可。

Saving Space with the __slots__ Class Attribute

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

__slots__ 变量定义了一个类的所有属性。存在 __slots__ 后,你无法为一个类实例动态地新增属性。所以 Python 解释器会使用 tuple 结构来保存类属性,而不再用 dict。这可以节约 很多 内存开销。但是 __slots__ 还带来一些不便,比如子类需要重复声明 __slots____dict__ 属性消失,__weakref__ 也需要特别对待等。

大多数情况下你不需要使用它。一般情况下它适用于类似数据库纪录的数据结构。如果你要用它,注意多看看文档。

Overriding Class Attributes

Python 类中有 class attributes 和 instance attributes 之分:

class Demo:
    a = 1   # class attribute
    def __init__(self):
        self.b = 2    # instance attributes

>>> d1 = Demo()
>>> d2 = Demo()
>>> print(d1.a)
1
>>> d1.a = 3   # create instance attributes a, shadow class attribute a
>>> print(d1.a)
3
>>> print(d2.a)
1

Instance attributes 会覆盖同名的 class attributes。同时你在子类中以 class attribute 形式声明父类的同名 class attribute,也会覆盖掉父类的。这个机制可以用于继承和扩展已有的类。