流畅的python学习笔记(五):面向对象惯用法(2:符合 Python 风格的对象 )

  • 得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子类型(duck typing):我们只需按照预定行为实现对象所需的方法即可。
  • 本章包含以下话题:
    • 支持用于生成对象其他表示形式的内置函数(如repr()、bytes(),等等)
    • 使用一个类方法实现备选构造方法
    • 扩展内置的 format() 函数和 str.format() 方法使用的格式微语言
    • 实现只读属性
    • 把对象变为可散列的,以便在集合中及作为 dict 的键使用
    • 利用 __slots__节省内存
  • 我们将开发一个简单的二维欧几里得向量类型,在这个过程中涵盖上述全部话题。
  • 在实现这个类型的中间阶段,我们会讨论两个概念:
    • 如何以及何时使用 @classmethod@staticmethod 装饰器
    • Python 的私有属性和受保护属性的用法、约定和局限
  • 我们从对象表示形式函数开始。
1. 对象表示形式
  • 每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。Python 提供了两种方式。
  • repr()
    • 以便于开发者理解的方式返回对象的字符串表示形式。
  • str()
    • 以便于用户理解的方式返回对象的字符串表示形式。
  • 正如你所知,我们要实现 __repr____str__ 特殊方法,为repr() 和 str() 提供支持。
  • 为了给对象提供其他的表示形式,还会用到另外两个特殊方法:__bytes____format____bytes__ 方法与 __str__方法类似:bytes() 函数调用它获取对象的字节序列表示形式。而__format__方法会被内置的 format() 函数和 str.format() 方法调用,使用特殊的格式代码显示对象的字符串表示形式。
  • 在 Python 3中,__repr__、__str__ 和 __format__都必须返回 Unicode字符串(str 类型)。只有__bytes__方法应该返回字节序列(bytes 类型)。
2. 再谈向量类
  • 为了说明用于生成对象表示形式的众多方法,我们将使用一个Vector2d类。这一节和接下来的几节会不断实现这个类。
from array import array
import math


class Vector2d:
    type_code = '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.type_code)]) + bytes(array(self.type_code, 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))
  • 暂时先不解释这个类的为什么要这样实现。我们来看下测试效果:
if __name__ == '__main__':
    v1 = Vector2d(3, 4)
    print(v1)
    # (3.0, 4.0)
    print(v1.x, v1.y)
    # 3.0 4.0

    x, y = v1
    print(x, y)
    # 3.0 4.0

    v1_clone = eval(repr(v1))
    print(v1_clone)
    # (3.0, 4.0)
    print(repr(v1) == 'Vector2d(3.0, 4.0)')
    # True
    print(v1 == v1_clone)
    # True

    octets = bytes(v1)
    print(octets)
    b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
    print(abs(v1))
    # 5.0
    print(bool(v1), bool(Vector2d(0, 0)))
    # True, False
  • print(v1)结果看出,其调用了__str__内置方法。关于何时调用__str__或者__repr__这里总结如下:
    • 这里当直接查看对象的时候调用的是__repr__方法,对象需要转字符串的时候调用的是__str__方法,但是当字典列表等容器的时候调用的还是__repr__方法, 使用 print 会调用__str__方法。实际使用比较常见的情况是在类中实现__repr__方法,因为其返回结果更准确。
  • 使用repr内置函数调用了__repr__方法,所以repr(v1) == 'Vector2d(3.0, 4.0)',经过 eval 函数计算字符串中的表达式,所以v1_clone = Vector2d(3.0, 4.0) = v1,bytes 函数调用了__bytes__方法,abs 调用了__abs__方法,这里需要说明的是__abs__中用到了math.hypot函数(欧几里德范数 sqrt(x*x + y*y)),你可以理解为直角三角形已知两直角边求斜边。使用bool函数时调用了__bool__内置方法,__bool__使用了__abs__的结果,所以bool(v1) = bool(5.0) = True,bool(Vector2d(0, 0))) = bool(0) = False
  • 通过这个测试用到了我们上面类中定义的所有内置方法,而这些内置方法是我们设计良好的对象不可或缺的。我们已经定义了很多基本方法,但是显然少了一个操作:使用bytes() 函数生成的二进制表示形式重建 Vector2d 实例。
3.备选构造方法
  • 我们可以把 Vector2d 实例转换成字节序列了;同理,也应该能从字节序列转换成 Vector2d 实例。array.array有个类方法 .frombytes正好符合需求。下面为 Vector2d 定义一个同名类方法,此时Vector2d如下:
from array import array
import math


class Vector2d:
    type_code = '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.type_code)]) + bytes(array(self.type_code, 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))

    @classmethod
    def frombytes(cls, b):  # 不用传入 self 参数;相反,要通过 cls 传入类本身。
        type_code = chr(b[0])
        '''
        	memoryview.cast 会把同一块内存里的内容打包成一个全新的 memoryview对象给你。
        	使用传入的字节序列创建一个 memoryview,然后使用 typecode 转换
        '''
        memv = memoryview(b[1:]).cast(type_code)
        return cls(*memv)  # 拆包转换后的 memoryview,得到构造方法所需的一对参数。
  • 此时测试自定义对象与字节序列的相互转换:
if __name__ == '__main__':
    v1 = Vector2d(3, 4)
    octets = bytes(v1)
    v2 = Vector2d.frombytes(octets)
    print(v1 == v2)
    # True
    print(v1 is v2)
    # False
  • frombytes采用了classmethod 装饰器,下节进行说明。
4. classmethod与staticmethod
  • 不知你考虑过没有,为什么Python 提供两个这样的装饰器(classmethod与staticmethod),而不是只提供一个?
  • 先来看 classmethod 的用法。上一节已经使用了该装饰器,如下所示:
 	@classmethod
    def frombytes(cls, b):
        type_code = chr(b[0])
        memv = memoryview(octets[1:]).cast(type_code)
        return cls(*memv)
  • 示例展示 classmethod 的用法:定义操作类,而不是操作实例的方法。classmethod 改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。classmethod 最常见的用途是定义备选构造方法
  • staticmethod 装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。下面通过一个示例来比较 classmethod 和 staticmethod 的行为:
class Demo:

    @classmethod
    def c_method(*args):
        return args

    @staticmethod
    def s_method(*args):
        return args


if __name__ == '__main__':
    print(Demo.c_method())
    print(Demo.c_method("spam"))
    print(Demo.s_method())
    print(Demo.s_method('spam'))
    # (<class '__main__.Demo'>,)
    # (<class '__main__.Demo'>, 'spam')
    # ()
    # ('spam',)
  • 可以看出,不管怎样调用Demo.c_method,它的第一个参数始终是 Demo类。而Demo.s_method 的行为与普通的函数相似。
  • 实际来说staticmethod更为常用,如果只是在类中使用普通函数,staticmethod 将是不二选择,但是其实这样和直接定义在模块中并没有太大区别,只不过此时将该函数封装在类中。而 staticmethod 最常使用的场景则是在类中定义多个构造函数。
5. 格式化显示
  • 内置的 format()函数和 str.format()方法把各个类型的格式化方式委托给相应的 .__format__(format_spec) 方法。format_spec 是格式说明符,它是:
    • format(my_obj, format_spec) 的第二个参数,或者
    • str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分
  • 例如:
if __name__ == '__main__':
    brl = 1 / 2.43
    print(brl)
    # 0.4115226337448559
    print(format(brl, '0.4f'))
    # 0.4115
    print('1 BRL = {rate:0.2f} USD'.format(rate=brl))
    # 1 BRL = 0.41 USD
  • 由此引出了一个重要的知识点,'{mass:5.3e}'这样的格式字符串其实包含两部分,冒号左边的'mass'在代换字段句法中是字段名,冒号后面的 '5.3e'格式说明符。格式说明符使用的表示法叫格式规范微语言(“Format Specification Mini-Language”
  • 格式规范微语言为一些内置类型提供了专用的表示代码。比如,b 和 x 分别表示二进制和十六进制的 int 类型,f 表示小数形式的 float 类型,而 % 表示百分数形式,如下示例:
if __name__ == '__main__':
    print(format(42, 'b'))
    # 101010
    print(format(255, 'x'))
    # ff
    print(format(2/3, '0.1f'))
    # 0.7
    print(format(2/3, '0.1%'))
    # 66.7%
  • 格式规范微语言是可扩展的,因为各个类可以自行决定如何解释format_spec 参数。例如, datetime 模块中的类,它们的__format__方法使用的格式代码与 strftime()函数一样。下面是内置的 format() 函数和 str.format() 方法的几个示例:
if __name__ == '__main__':
	now = datetime.now()
    print(now)
    # 2021-01-11 15:54:57.488651
    print(format(now, '%H:%M:%S'))
    # 15:54:57
    print("It is now {:%I:%M %p}".format(now))
    # It is now 03:54 PM
  • 如果类没有定义 __format__ 方法,从 object 继承的方法会返回str(my_object)。我们为 Vector2d 类定义了 __str__方法,因此可以这样做:
if __name__ == '__main__':
    v1 = Vector2d(3, 4)
    print(format(v1))
    # (3.0, 4.0)
  • 然而,如果传入格式说明符,object.__format__ 方法会抛出TypeError:
if __name__ == '__main__':
    print(format(v1, '0.3f'))
    # 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)
  • 再次测试 format 操作:
if __name__ == '__main__':
    v1 = Vector2d(3, 4)
    print(v1)
    print(format(v1, '0.2f'))
    print(format(v1, '0.3e'))
    # (3.0, 4.0)
    # (3.00, 4.00)
    # (3.000e+00, 4.000e+00)
  • 下面要在微语言中添加一个自定义的格式代码:如果格式说明符以 'p'结尾,那么在极坐标中显示向量,即<r, θ >,其中 r 是模,θ 是弧度。对极坐标来说,我们已经定义了计算模的__abs__ 方法,因此还要定义一个简单的 angle 方法,使用 math.atan2()函数计算角度(弧度)。angle 方法的代码如下:
    def angle(self):
        return math.atan2(self.y, self.x)
  • 这样便可以增强 __format__ 方法,计算极坐标,如下所示:
    def __format__(self, format_spec=''):
        if format_spec.endswith('p'):
            format_spec = format_spec[:-1]  # 格式代码去掉最后一个字符'p'
            coords = (abs(self), self.angle())  # 计算向量模和弧度值
            out_fmt = '<{}, {}>'
        else:
            coords = self
            out_fmt = '({}, {})'
        compontens = (format(c, format_spec) for c in coords)
        return out_fmt.format(*compontens)
  • 测试计算向量的极坐标(模和弧度)并格式化如下:
if __name__ == '__main__':
    v1 = Vector2d(1, 1)
    print(v1)
    print(format(v1, '0.4fp'))
    # <1.4142, 0.7854>
    print(format(Vector2d(3, 4), '0.2fp'))
    # <5.00, 0.93>
  • 如本节所示,为用户自定义的类型扩展格式规范微语言并不难。下面换个话题,它不仅事关对象的外观:我们将把 Vector2d 变成可散列的,这样便可以构建向量集合,或者把向量当作 dict 的键使用。不过在此之前,必须让向量不可变。详情参见下一节。
6. 可散列的Vector2d
  • 按照定义,目前 Vector2d 实例是不可散列的,因此不能放入集合(set)中,如下示例:
from array import array
import math


class Vector2d:
    type_code = '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.type_code)]) + bytes(array(self.type_code, 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))

    def angle(self):
    	'''计算弧度'''
        return math.atan2(self.y, self.x)

    def __format__(self, format_spec=''):
    	'''向量极坐标格式化'''
        if format_spec.endswith('p'):
            format_spec = format_spec[:-1]
            coords = (abs(self), self.angle())
            out_fmt = '<{}, {}>'
        else:
            coords = self
            out_fmt = '({}, {})'
        compontens = (format(c, format_spec) for c in coords)
        return out_fmt.format(*compontens)

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


if __name__ == '__main__':
    v1 = Vector2d(3, 4)

    try:
        print(hash(v1))
    except TypeError as e1:
        print(e1) # unhashable type: 'Vector2d'

    try:
        print(set([v1]))
    except TypeError as e2:
        print(e2)  # unhashable type: 'Vector2d'
  • 为了把 Vector2d 实例变成可散列的,必须使用 __hash__ 方法(还需要 __eq__ 方法,前面已经实现了)。此外,还要让向量不可变。至于为什么要这样做可以查看我前面的博客 字典和集合 中对散列详细的讲解。
  • 目前,我们可以为分量赋新值,如v1.x = 7,Vector2d 类的代码并不阻止这么做。也就是说 Vector2d 的属性支持读写。为了提高类的安全性,我们希望 Vector2d 类的分量仅支持读取,为此,我们要把 x 和 y 分量设为只读特性,Vector2d 类中加入两个特性方法,并修改init,如下所示:
	def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)
        
	@property  # @property 装饰器把读值方法标记为特性。
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y
  • 此时无法直接设置x,y分量的值
if __name__ == '__main__':
    v1 = Vector2d(3, 4)
    print(v1.x)
    # 3.0
    v1.x = 4
    # AttributeError: can't set attribute
  • 这里很关键的一步是把 x 和 y 标记为了私有属性。python规定:使用两个前导下划线(尾部没有下划线,或者有一个下划线),把属性标记为私有的
  • 我们已经让向量类 Vector2d 不可变(只读)了,这样就可以实现__hash__方法:
    def __hash__(self):
        return hash(self.x) ^ hash(self.y)  # ^ 为异或操作符
  • 添加 __hash__方法之后,向量变成可散列的了:
if __name__ == '__main__':
    v1 = Vector2d(3, 3)
    v2 = Vector2d(3, 4)
    print(hash(v1), hash(v2))
    # 0 7
  • 这里计算散列使用了异或操作,3 ^ 3 = 0011 ^ 0011 = 0000 = 0,3 ^ 4 = 0011 ^ 0100 = 0111 = 7
  • 要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需正确地实现 __hash____eq__方法即可。但是,实例的散列值绝不应该变化,因此我们借机提到了只读特性。
  • 如果定义的类型有标量数值,可能还要实现 __int____float__方法(分别被 int() 和 float() 构造函数调用),以便在某些情况下用于强制转换类型。此外,还有用于支持内置的 complex() 构造函数的 __complex__方法:
    def __complex__(self):
    	"""real为复数的实部,img为复数的虚部"""
        return complex(real=self.x, imag=self.y)
  • 测试复数用法:
	print(complex(Vector2d(3, 4)))  # (3+4j)
    print(complex(Vector2d(1, -1)))  # (1-1j)
  • 到此为止,我们在 Vector2d 中实现很多内置方法。要想得到功能完善的对象,这些方法可能是必备的。当然,如果你的应用用不到,就没必要全部实现这些方法。下一节暂时不继续定义 Vector2d 类了,我们将讨论 Python 对私有属性(带两个下划线前缀的属性,如 self.__x)的设计方式及其缺点。
7. Python的私有属性和“受保护的”属性
  • Python 不能像 Java 那样使用 private 修饰符创建私有属性,但是 Python 有个简单的机制,能避类意外覆盖“私有”属性。
  • 举个例子。有人编写了一个名为 Dog 的类,这个类的内部用到了 mood实例属性,但是没有将其开放。现在,你创建了 Dog 类的子类:Beagle。如果你在毫不知情的情况下又创建了名为 mood 的实例属性,那么在继承的方法中就会把 Dog 类的 mood 属性覆盖掉。这是个难以调试的问题。
  • 为了避免这种情况,如果以 __mood 的形式(两个前导下划线,尾部没有或最多有一个下划线)命名实例属性,Python 会把属性名存入实例的__dict__ 属性中,而且会在前面加上一个下划线和类名。因此,对Dog 类来说,__mood会变成_Dog__mood;对 Beagle 类来说,会变成 _Beagle__mood。这个语言特性叫名称改写(name mangling)
  • 下面通过之前定义的 Vector2d 类为例来说明名称改写:
'''
	私有属性的名称会被“改写”,在前面加上下划线和类名
'''
if __name__ == '__main__':
    v1 = Vector2d(3, 4)
    print(v1.__dict__)
    # {'_Vector2d__x': 3.0, '_Vector2d__y': 4.0}
    print(v1._Vector2d__x)
    # 3.0
  • 名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意做错事。只要知道改写私有属性名的机制,任何人都能直接读取私有属性——这对调试和序列化倒是有用。此外,只要编写v1._Vector__x = 7这样的代码,就能轻松地为 Vector2d实例的私有分量直接赋值,不过最好还是别在生产环境中这样做。
  • 也不是所有人都喜欢self.__x 这种不对称的名称。有些人不喜欢这种句法,他们约定使用一个下划线前缀编写“受保护”的属性(如 self._x)。Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员严格遵守的约定,他们不会在类外部访问这种属性。
  • 总之,Vector2d 的分量都是“私有的”,而且 Vector2d 实例都是“不可变的”。我用了两对引号,这是因为并不能真正实现私有和不可变。
  • 下面继续定义 Vector2d 类。在最后一节中,我们将讨论一个特殊的属性(不是方法),它会影响对象的内部存储,对内存用量可能也有重大影响,不过对对象的公开接口没什么影响。这个属性是__slots__
8. 使用__slots__类属性节省空间
  • 默认情况下,Python 在各个实例中名为__dict__ 的字典里存储实例属性。而字典使用的散列表(hash表)访问速度的提高是以牺牲内存为代价的,这样实例属性会占用大量内存。有没有什么好的解决方法呢?
  • 如果要处理数百万个属性不多的实例,通过 __slots__类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。需要指出的是:继承自超类的 __slots__属性没有效果。Python 只会使用各个类中定义的__slots__ 属性。
  • 定义 __slots__ 的方式是,创建一个类属性,使用 __slots__ 这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。推荐使用元组,因为这样定义的 __slots__中所含的信息不会变化。如下示例,在 Vector2d 类中添加__slots__属性:
class Vector2d:
    __slots__ = ('__x', '__y')
    
    type_code = 'd'

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)
        
	# 以下内容省略
	...
  • 在类中定义__slots__属性的目的是告诉解释器:“这个类中的所有实例属性都在这儿了!”这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的 __dict__ 属性。如果有数百万个实例同时活动,这样做能节省大量内存。
  • 此外,还有一个实例属性可能需要注意,即__weakref__ 属性,为了让对象支持弱引用,必须有这个属性。用户定义的类中默认就有 __weakref__属性。可是,如果类中定义了__slots__属性,而且想把实例作为弱引用的目标,那么要把 __weakref__'添加到 __slots__ 中。
  • 当然,如果实际项目使用的python实例较少,使用__slots__属性来降低微不足道的内存而去增加实例属性查询时间反而画蛇添足。与其他优化措施一样,仅当权衡当下的需求并仔细搜集资料后证明确实有必要时,才应该使用__slots__属性。
  • 本章最后一个话题讨论如何在实例和子类中覆盖类属性。
9. 覆盖类属性
  • Python 有个很独特的特性:类属性可用于为实例属性提供默认值。Vector2d 中有个 type_code 类属性,__bytes__方法两次用到了它,而且都故意使用 self.type_code读取它的值。因为 Vector2d 实例本身没有 type_code 属性,所以 self.type_code 默认获取的是 Vector2d.type_code 类属性的值。
  • 但是,如果为不存在的实例属性赋值,会新建实例属性。假如我们为type_code 实例属性赋值,那么同名类属性不受影响。然而,自此之后,实例读取的 self.type_code 是实例属性 type_code,也就是把同名类属性遮盖了。借助这一特性,可以为各个实例的 type_code 属性定制不同的值。
  • Vector2d.type_code 属性的默认值是 ‘d’,即转换成字节序列时使用 8 字节双精度浮点数表示向量的各个分量。如果在转换之前把Vector2d 实例的 type_code 属性设为 ‘f’,那么使用 4 字节单精度浮点数表示各个分量,如示例:
'''
	我们在讨论如何添加自定义的实例属性,
	因此示例使用的是不带 __slots__ 属性的 Vector2d 类。
'''
if __name__ == '__main__':
    v1 = Vector2d(1.1, 2.2)
    dump_d = bytes(v1)
    print(dump_d)
    # b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@'
    print(len(dump_d))
    # 17

    v1.type_code = 'f'
    dump_f = bytes(v1)
    print(dump_f)
    # b'f\xcd\xcc\x8c?\xcd\xcc\x0c@'
    print(len(dump_f))
    # 9
    print(Vector2d.type_code)
    # d
    print(v1.type_code)
    # f
  • 示例说明:Vector2d.type_code 属性的值不变,只有 v1 实例的 type_code属性使用 ‘f’。现在你应该知道为什么要在得到的字节序列前面加上 type_code 的值了:为了支持不同的格式。
  • 如果想修改类属性的值,必须直接在类上修改,不能通过实例修改。如果想修改所有实例(没有 type_code 实例变量)的 type_code 属性的默认值,可以这么做:
	Vector2d.type_code = 'f'
  • 然而,有种修改方法更符合 Python 风格,而且效果持久,也更有针对性。类属性是公开的,因此会被子类继承,于是经常会创建一个子类,只用于定制类的数据属性。Django 基于类的视图就大量使用了这个技术。如下示例:
'''
	ShortVector2d 是 Vector2d 的子类,只用于覆盖type_code 的默认值
'''
class ShortVector2d(Vector2d):
    type_code = 'f'


if __name__ == '__main__':
    sv = ShortVector2d(1.1, 2.2)
    print(sv)  # (1.1, 2.2)
    print(len(bytes(sv)))  # 9
  • 确认得到的字节序列长度为 9 字节,而不是之前的 17 字节。这也说明了我在 Vecto2d.__repr__方法中为什么没有硬编码class_name 的值,而是使用 type(self).__name__ 获取,如下所示:
    def __repr__(self):
        class_name = type(self).__name__
        return '{}(!r), {!r}'.format(class_name, *self)
  • 如果硬编码 class_name 的值,那么 Vector2d 的子类(如ShortVector2d)要覆盖 __repr__ 方法,只是为了修改class_name 的值。从实例的类型中读取类名,__repr__方法就可以放心继承。
  • 至此,我们通过一个简单的类说明了如何利用数据模型处理 Python 的其他功能:提供不同的对象表示形式、实现自定义的格式代码、公开只读属性,以及通过 hash() 函数支持集合和映射。
本章小结
  • 本章的目的是说明,如何使用特殊方法和约定的结构,定义行为良好且符合 Python 风格的类。
  • 不过始终要记住的一条真理是:简洁胜于复杂。切不可为了堆砌语言特性而定义过多操作,符合 Python 风格的对象应该正好符合所需。

要构建符合 Python 风格的对象,就要观察真正的 Python 对象的行为。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值