Python深入-10-Python风格的对象

一、对象的表示形式

每门面向对象语言至少都有一种获取对象字符串表示形式的标准方式。Python 提供了两种方式。
repr() 以便于开发者理解的方式返回对象的字符串表示形式。Python 控制台或调试器在显示对象时采用这种 方式。
str() 以便于用户理解的方式返回对象的字符串表示形式。使用 print() 打印对象时采用这种方式。  
其实, 在背后支持 repr()str() 的是特殊方法 __repr____str__。  
    除此之外,还有两个特殊方法(__bytes____format__)可为对象提供其他表示形式。__bytes__ 方法与 __str__ 方法类似,bytes() 函数调用它获取对象的字节序列表示形式。而__format__法供 f 字符串、内置函数 format()str.format() 方法使用,通过调用 obj.__format__(format_spec) 以特殊的格式化代码显示对象的字符串表示形式。

如果你是 Python 2 用户,那么请记住,在 Python 3 中,__repr____str____format__ 都必须返回 Unicode 字符串(str 类型)。只有 __bytes__ 方法应该返回字节序列 (bytes 类型)。

二、从向量类说起

from array import array
import math


class Vector2d:
    # typecode 是类属性,在 Vector2d 实例和字节序列之间转换时使用
    typecode = 'd'

    def __init__(self, x, y):
        # 在 __init__ 方法中把 x 和 y 转换成浮点数,尽早捕获错误,以防调用 Vector2d 构造函数时传入不当参数。
        self.x = float(x)
        self.y = float(y)

    def __iter__(self):
        # 定义 __iter__ 方法,把 Vector2d 实例变成可迭代对象,这样才能拆包(例如,x, y = my_vector)。这个方法的实现方式很简单,直接调用生成器表达式依次产出分量
        return (i for i in (self.x, self.y))

    def __repr__(self):
        class_name = type(self).__name__
        # __repr__ 方法使用 {!r} 获取各个分量的表示形式,然后插值,构成一个字符串。因为 Vector2d 实例是可迭代对象,所以 *self 会把 x 分量和 y 分量提供给 format 方法
        return '{}({!r}, {!r})'.format(class_name, *self)

    def __str__(self):
        # 从可迭代的 Vector2d 实例中可以轻易得到一个元组,显示为有序对
        return str(tuple(self))

    def __bytes__(self):
        # 为了生成字节序列,把 typecode 转换成字节序列,然后……迭代 Vector2d 实例,得到一个数组,再把数组转换成字节序列
        return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self)))

    def __eq__(self, other):
        # 为了快速比较所有分量,把运算对象转换成元组。对 Vector2d 实例来说,虽然可以这样做,但仍有问题。
        return tuple(self) == tuple(other)

    def __abs__(self):
        # 模是 x 分量和 y 分量构成的直角三角形的斜边长。
        return math.hypot(self.x, self.y)

    def __bool__(self):
        # __bool__ 方法使用 abs(self) 计算模,然后把结果转换成布尔值,因此,0.0 是 False,非零值是True
        return bool(abs(self))

上例中的__eq__ 方法,在两个运算对象都是 Vector2d 实例时没有问题,不过拿 Vector2d 实例与其他具有相同数值的可迭代对象相比,结果也是 True(例如,Vector(3, 4) == [3, 4])。这个行为既可以被视为特性,也可以被视为 bug。
我们已经定义了很多基本方法,但是显然少了一个操作:使用 bytes() 函数生成的二进制表示形式重建 Vector2d 实例。

三、备选构造函数

 现在可以把 Vector2d 实例转换成字节序列了。同理,我们也希望能从字节序列构建 Vector2d 实例。在 标准库中探索一番之后,我们发现`array.array` 有个类方法 `.frombytes`正好符合需求。
from array import array
import math


class Vector2d:
    # typecode 是类属性,在 Vector2d 实例和字节序列之间转换时使用
    typecode = 'd'

    def __init__(self, x, y):
        # 在 __init__ 方法中把 x 和 y 转换成浮点数,尽早捕获错误,以防调用 Vector2d 构造函数时传入不当参数。
        self.x = float(x)
        self.y = float(y)

    def __iter__(self):
        # 定义 __iter__ 方法,把 Vector2d 实例变成可迭代对象,这样才能拆包(例如,x, y = my_vector)。这个方法的实现方式很简单,直接调用生成器表达式依次产出分量
        return (i for i in (self.x, self.y))

    def __repr__(self):
        class_name = type(self).__name__
        # __repr__ 方法使用 {!r} 获取各个分量的表示形式,然后插值,构成一个字符串。因为 Vector2d 实例是可迭代对象,所以 *self 会把 x 分量和 y 分量提供给 format 方法
        return '{}({!r}, {!r})'.format(class_name, *self)

    def __str__(self):
        # 从可迭代的 Vector2d 实例中可以轻易得到一个元组,显示为有序对
        return str(tuple(self))

    def __bytes__(self):
        # 为了生成字节序列,把 typecode 转换成字节序列,然后……迭代 Vector2d 实例,得到一个数组,再把数组转换成字节序列
        return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self)))

    def __eq__(self, other):
        # 为了快速比较所有分量,把运算对象转换成元组。对 Vector2d 实例来说,虽然可以这样做,但仍有问题。
        return tuple(self) == tuple(other)

    def __abs__(self):
        # 模是 x 分量和 y 分量构成的直角三角形的斜边长。
        return math.hypot(self.x, self.y)

    def __bool__(self):
        # __bool__ 方法使用 abs(self) 计算模,然后把结果转换成布尔值,因此,0.0 是 False,非零值是True
        return bool(abs(self))

    # classmethod 装饰的方法可直接在类上调用。
    @classmethod
    # 第一个参数不是 self,而是类自身(习惯命名为 cls)。
    def frombytes(cls, octets):
        # 从第一字节中读取 typecode
        typecode = chr(octets[0])
        # 使用传入的 octets 字节序列创建一个 memoryview,然后使用 typecode 进行转换
        memv = memoryview(octets[1:]).cast(typecode)
        # 拆包转换后的 memoryview,得到构造函数所需的一对参数
        return cls(*memv)

四、 classmethod 与 staticmethod

先来看 classmethod。示例 备选构造函数展示了它的用法:定义操作类而不是操作实例的方法。由于classmethod改变了调用方法的方式,因此接收的第一个参数是类本身,而不是实例。classmethod 最常见的用途是定义备选构造函数,例如示例备选构造函数中的 frombytes。注意,frombytes 的最后一行使用 cls 参数构建了一个新实例,即 cls(*memv)
相比之下,staticmethod装饰器也会改变方法的调用方式,使其接收的第一个参数没什么特殊的。其实,静态方法就是普通的函数,只是碰巧位于类的定义体中,而不是在模块层定义。
下面对 classmethod 和 staticmethod 的行为做了对比。

class Demo:
    @classmethod
    def klassmeth(*args):
        return args  # klassmeth 返回全部位置参数。

    @staticmethod
    def statmeth(*args):
        return args  # statmeth 也返回全部位置参数


if __name__ == '__main__':
    print(Demo.klassmeth()) # (<class '__main__.Demo'>,)
    print(Demo.klassmeth('spam')) # (<class '__main__.Demo'>, 'spam')
    print(Demo.statmeth()) # ()

五、格式化显示

f字符串、内置函数 format()str.format() 方法会把各种类型的格式化方式委托给相应的 .__format__(format_spec) 方法。format_spec 是格式说明符,它是:

  • format(my_obj, format_spec) 的第二个参数;

  • {} 内代换字段中冒号后面的部分,或者 fmt.str.format() 中的 fmt

    如果一个类没有定义 __format__,那么该方法就会从 object继承,并返回str(my_object)

六、可hash的类

按照定义,目前 Vector2d 实例不可哈希,因此不能放入集合中。  
为了把 Vector2d 实例变成可哈希的,必须实现 __hash__方法(还需要 __eq__ 方法,前面已经实现了)。此外,还要让向量实例不可变.

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

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

举个例子。有人编写了一个名为 Dog 的类,内部用到了 mood 实例属性,但是没有将其开放。现在,你创建了 Dog 类的子类 Beagle。如果你在毫不知情的情况下又创建了名为 mood 的实例属性,那么在继承的方法中就会把 Dog 类的 mood 属性覆盖。这是难以调试的问题。
为了避免这种情况,如果以 __mood 的形式(两个前导下划线,尾部没有或最多有一个下划线)命名实例属性,那么 Python 就会把属性名存入实例属性__dict__ 中,而且会在前面加上一个下划线和类名。因 此,对 Dog 类来说,__mood会变成 _Dog__mood;对 Beagle 类来说,__mood 会变成 _Beagle__mood。这个语言功能叫名称改写(name mangling)。

八、 使用 `slots` 节省空间

 默认情况下,Python 把各个实例的属性存储在一个名为 `__dict__`的字典中。字典消耗的内存很多——即使有一些优化措施。但是,如果定义一个名为 `__slots__` 的类属性,以序列的形式存储属性名称,那么 Python 将使用其他模型存储实例属性:`__slots__` 中的属性名称存储在一个隐藏的引用数组中,消耗的内存比字典少。  
class Pixel:
    # __slots__ 必须在定义类时声明,之后再添加或修改均无效。属性名称可以存储在一个元组或列表
    # 中,不过我喜欢使用元组,因为这可以明确表明 __slots__ 无法修改。
    __slots__ = ('x''y')


if __name__ == "__main__":
    # 创建一个 Pixel 实例,因为 __slots__ 的效果要通过实例体现
    p = Pixel()
    # 第一个效果:Pixel 实例没有 __dict__ 属性
    # print(p.__dict__)  #'Pixel' object has no attribute '__dict__'. Did you mean: '__dir__'?

    # 正常设定 p.x 属性和 p.y 属性
    p.x = 10
    p.y = 20
    # 设定不在 __slots__ 中的属性抛出 AttributeError
    # p.color = 'red'

子类实现

class Pixel:
    # __slots__ 必须在定义类时声明,之后再添加或修改均无效。属性名称可以存储在一个元组或列表
    # 中,不过我喜欢使用元组,因为这可以明确表明 __slots__ 无法修改。
    __slots__ = ('x''y')


# OpenPixel 自身没有声明任何属性。
class OpenPixel(Pixel):
    pass


if __name__ == "__main__":
    op = OpenPixel()

    # 奇怪的事情发生了,OpenPixel 实例有 __dict__ 属性。
    print(op.__dict__)  # {}

    # 即使设定属性 x(在基类 Pixel 的 __slots__ 属性中)
    op.x = 8

# …也不存入实例的 __dict__ 属性中
    print(op.__dict__)  # {}

    # 而是存入实例的一个隐藏的引用数组中
    print(op.x)  # 8

    # 设定不在 __slots__ 中的属性
    op.color = 'red'
    # 存入实例的 __dict__ 属性中
    print(op.__dict__)  # {'color': 'red'}
 子类只继承 `__slots__` 的部分效果。为了确保子类的实例也没有` __dict__` 属性,必须在子类中再次声明` __slots__` 属性。

如果在子类中声明 __slots__ = ()(一个空元组),则子类的实例将没有__dict__ 属性,而且只接受基类的 __slots__ 属性列出的属性名称。
如果子类需要额外属性,则在子类的 __slots__属性中列出来 。如下图所示

class Pixel:
    # __slots__ 必须在定义类时声明,之后再添加或修改均无效。属性名称可以存储在一个元组或列表
    # 中,不过我喜欢使用元组,因为这可以明确表明 __slots__ 无法修改。
    __slots__ = ('x''y')


# OpenPixel 自身没有声明任何属性。
class ColorPixel(Pixel):
    # 其实,超类的 __slots__ 属性会被添加到当前类的 __slots__ 属性中。别忘了,只有一项的元组,因此在那一项后面要加上一个逗号
    __slots__ = ('color',)


if __name__ == "__main__":
    op = ColorPixel()

    # 奇怪的事情发生了,OpenPixel 实例有 __dict__ 属性。
    # ColorPixel 实例没有 __dict__ 属性。
    # print(op.__dict__)  # 'ColorPixel' object has no attribute '__dict__'

    # 即使设定属性 x(在基类 Pixel 的 __slots__ 属性中)
    op.x = 8
    op.color = "red"
    # 可以设定在当前类和超类的 __slots__ 中声明的属性,其他属性则不能设定
    # op.flavor = "banna" # 'ColorPixel' object has no attribute 'flavor'

然而,“节省的内存也可能被再次吃掉”:如果把 '__dict__'这个名称添加到__slots__列表中,则实 例会在各个实例独有的引用数组中存储 __slots__中的名称,不过也支持动态创建属性,存储在常规的 __dict__ 中。如果想使用 @cached_property 装饰器,就要这么做。
当然,把 __dict__ 添加到 __slots__ 中可能完全违背了初衷,这取决于各个实例的静态属性和动态 属性的数量及其用法。粗心的优化甚至比提早优化还糟糕,往往得不偿失。 此外,还有一个实例属性可能需要注意,即 __weakref__。为了让对象支持弱引用, 必须有这个属性。用户定义的类默认就有 __weakref__ 属性。然而,如果类中定义__slots__,而且想把该类的实例作为弱引用的目标,则必须把 __weakref__ 添加到 __slots__ 中。  
** 总结 **__slots__** 的问题  **
如果使用得当,则类属性 __slots__能显著节省内存,不过有几个问题需要注意

  • 每个子类都要重新声明slots__`属性,以防止子类的实例有 `__dict属性。

  • 实例只能拥有 slots__` 列出的属性,除非把 `_dict_`加入 `__slots中(但是这样做就失 去了节省内存的功效)。

  • slots__` 的类不能使用 `@cached_property` 装饰器,除非把 `_dict_` 加入 `__slots 中。

  • 如果不把 weakref__`加入` __slots 中,那么实例就不能作为弱引用的目标。

九、 覆盖类属性

 Python 有一个很独特的功能:类属性可为实例属性提供默认值。Vector2d 中有一个名为 `typecode` 的类属性。`__bytes__` 方法两次用到了这个属性,而且都故意使用 `self.typecode` 读取它的值。因为 Vector2d 实例本身没有 typecode 属性,所以 `self.typecode` 默认获取的是 `Vector2d.typecode` 类属性的值。 

但是,如果为不存在的实例属性赋值,那么将创建一个新实例属性。假如为 typecode 实例属性赋值,那 么同名类属性将不受影响。然而,一旦这样做,实例读取的 self.typecode 是实例属性 typecode,也就是把同名类属性遮盖了。借助这个功能,可以为各个实例的 typecode 属性定制不同的值。  


原文始发于微信公众号(Python之家):Python深入-10-Python风格的对象

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/198384.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!