我是靠谱客的博主 无奈机器猫,最近开发中收集的这篇文章主要介绍流畅的python学习笔记(五):面向对象惯用法(2:符合 Python 风格的对象 ),觉得挺不错的,现在分享给大家,希望可以做个参考。
概述
符合 Python 风格的对象
- 1. 对象表示形式
- 2. 再谈向量类
- 3.备选构造方法
- 4. classmethod与staticmethod
- 5. 格式化显示
- 6. 可散列的Vector2d
- 7. Python的私有属性和“受保护的”属性
- 8. 使用` __slots__ `类属性节省空间
- 9. 覆盖类属性
- 本章小结
- 得益于 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'dx00x00x00x00x00x00x08@x00x00x00x00x00x00x10@'
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'dx9ax99x99x99x99x99xf1?x9ax99x99x99x99x99x01@'
print(len(dump_d))
# 17
v1.type_code = 'f'
dump_f = bytes(v1)
print(dump_f)
# b'fxcdxccx8c?xcdxccx0c@'
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 对象的行为。
最后
以上就是无奈机器猫为你收集整理的流畅的python学习笔记(五):面向对象惯用法(2:符合 Python 风格的对象 )的全部内容,希望文章能够帮你解决流畅的python学习笔记(五):面向对象惯用法(2:符合 Python 风格的对象 )所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复