>>> from env_helper import info; info()
页面更新时间: 2024-03-29 16:20:49
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-18-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

1.9. 符合 Python 风格的对象

绝对不要使用两个前导下划线,这是很烦人的自私行为。
——Ian Bicking
pip, virtualenv 和 Paste 等项目的创建者
得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。
实现如此自然的行为,靠的不是继承,而是鸭子类型(duck typing):我们只需按照预定行为实现对象所需的方法即可。

当然,这些方法不是必须要实现的,我们需要哪些功能,再去实现它就好。

书中的 Vector 类完整实现可见官方 Repo.

1.9.1. 对象表现形式

Python 中,有两种获取对象的字符串表示形式的标准方式: * repr():以便于开发者理解的方式返回对象的字符串表示形式 * str():以便于用户理解的方式返回对象的字符串表示形式

实现 __repr____str__ 两个特殊方法,可以分别为 reprstr 提供支持。

1.9.2. classmethod & staticmethod

classmethod: 定义操作,而不是操作实例的方法。
classmethod 最常见的用途是定义备选构造方法,比如 datetime.fromordinaldatetime.fromtimestamp.
staticmethod 也会改变方法的调用方式,但方法的第一个参数不是特殊的值(selfcls)。
staticmethod 可以把一些静态函数定义在类中而不是模块中,但抛开 staticmethod,我们也可以用其它方法来实现相同的功能。
>>> class Demo:
>>>     def omethod(*args):
>>>         # 第一个参数是 Demo 对象
>>>         return args
>>>     @classmethod
>>>     def cmethod(*args):
>>>         # 第一个参数是 Demo 类
>>>         return args
>>>     @staticmethod
>>>     def smethod(*args):
>>>         # 第一个参数不是固定的,由调用者传入
>>>         return args
>>>
>>> print(Demo.cmethod(1), Demo.smethod(1))
>>> demo = Demo()
>>> print(demo.cmethod(1), demo.smethod(1), demo.omethod(1))
(<class '__main__.Demo'>, 1) (1,)
(<class '__main__.Demo'>, 1) (1,) (<__main__.Demo object at 0x7f085e4bfd90>, 1)

1.9.3. 字符串模板化

__format__ 实现了 format 方法的接口,它的参数为 format 格式声明符,格式声明符的表示法叫做格式规范微语言
str.format 的声明符表示法和格式规范微语言类似,称作格式字符串句法
>>> # format
>>> num = 15
>>> print(format(num, '2d'), format(num, '.2f'), format(num, 'X'))
15 15.00 F

1.9.4. 对象散列化

只有可散列的对象,才可以作为 dict 的 key,或者被加入 set.
要想创建可散列的类型,只需要实现 __hash____eq__ 即可。
有一个要求:如果 a == b,那么 hash(a) == hash(b).

文中推荐的 hash 实现方法是使用位运算异或各个分量的散列值:

class Vector2d:
    # 其余部分省略
    def __hash__(self):
        return hash(self.x) ^ hash(self.y)

而最新的文章中,推荐把各个分量组成一个 tuple,然后对其进行散列:

class Vector2d:
    def __hash__(self):
        return hash((self.x, self.y))

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

Python 的“双下前导”变量:如果以 __a 的形式(双前导下划线,尾部最多有一个下划线)命名一个实例属性,Python 会在 __dict__ 中将该属性进行“名称改写”为 _Klass__a,以防止外部对对象内部属性的直接访问。
这样可以避免意外访问,但不能防止故意犯错

有一种观点认为不应该使用“名称改写”特性,对于私有方法来说,可以使用前导单下划线 _x 来标注,而不应使用双下划线。

此外:在 from mymod import * 中,任何使用下划线前缀(无论单双)的变量,都不会被导入。

>>> class Vector2d:
>>>     def __init__(self, x, y):
>>>         self.__x = x
>>>         self.__y = y
>>>
>>> vector = Vector2d(1, 2)
>>> print(vector.__dict__)
>>> print(vector._Vector2d__x)
{'_Vector2d__x': 1, '_Vector2d__y': 2}
1

1.9.6. 使用 slot 类属性节省空间

默认情况下,Python 在各个实例的 __dict__ 字典中存储实例属性。但 Python 字典使用散列表实现,会消耗大量内存。
如果需要处理很多个属性有限且相同的实例,可以通过定义 __slots__ 类属性,将实例用元组存储,以节省内存。
class Vector2d:
    __slots__ = ('__x', '__y')

__slots__ 的目的是优化内存占用,而不是防止别人在实例中添加属性。所以一般也没有什么使用的必要。

使用 __slots__ 时需要注意的地方: * 子类不会继承父类的 __slots_ * 实例只能拥有 __slots__ 中所列出的属性 * 如果不把 "__weakref__" 加入 __slots__,实例就不能作为弱引用的目标。

>>> # 对象二进制化
>>> from array import array
>>> import math
>>>
>>> class Vector2d:
>>>     typecode = 'd'
>>>
>>>     def __init__(self, x, y):
>>>         self.x = x
>>>         self.y = y
>>>
>>>     def __str__(self):
>>>         return str(tuple(self))
>>>
>>>     def __iter__(self):
>>>         yield from (self.x, self.y)
>>>
>>>     def __bytes__(self):
>>>         return (bytes([ord(self.typecode)]) +
>>>                 bytes(array(self.typecode, self)))
>>>
>>>     def __eq__(self, other):
>>>         return tuple(self) == tuple(other)
>>>
>>>     @classmethod
>>>     def frombytes(cls, octets):
>>>         typecode = chr(octets[0])
>>>         memv = memoryview(octets[1:]).cast(typecode)
>>>         return cls(*memv)
>>>
>>> vector = Vector2d(1, 2)
>>> v_bytes = bytes(vector)
>>> vector2 = Vector2d.frombytes(v_bytes)
>>> print(v_bytes)
>>> print(vector, vector2)
>>> assert vector == vector2
b'dx00x00x00x00x00x00xf0?x00x00x00x00x00x00x00@'
(1, 2) (1.0, 2.0)