《流畅的Python》笔记。
本篇是“面向对象惯用方法”的第二篇。前一篇讲的是内置对象的结构和行为,本篇则是自定义对象。本篇继续“Python学习之路20”,实现更多的特殊方法以让自定义类的行为跟真正的Python对象同样。
本篇要讨论的内容以下,重点放在了对象的各类输出形式上:python
repr()
,bytes()
等);format()
函数和str.format()
方法使用的格式微语言;__slots__
节省内存;@classmethod
和@staticmethd
装饰器;本篇将经过实现一个简单的二维欧几里得向量类型,来涵盖上述内容。程序员
不过在开始以前,咱们须要补充几个概念:数据库
repr()
:以便于开发者理解的方式返回对象的字符串表示形式,它调用对象的__repr__
特殊方法;str()
:以便于用户理解的方式返回对象的字符串表示形式,它调用对象的__str__
特殊方法;bytes()
:获取对象的字节序列表示形式,它调用对象的__bytes__
特殊方法;format()
和str.format()
:格式化输出对象的字符串表示形式,调用对象的__format__
特殊方法。咱们但愿这个类具有以下行为:数组
# 代码1 >>> v1 = Vector2d(3, 4) >>> print(v1.x, v1.y) # Vector2d实例的份量可直接经过实例属性访问,无需调用读值方法 3.0 4.0 >>> x, y = v1 # 实例可拆包成变量元组 >>> x, y (3.0, 4.0) >>> v1 # 咱们但愿__repr__返回的结果相似于构造实例的源码 Vector2d(3.0, 4.0) >>> v1_clone = eval(repr(v1)) # 只是为了说明repr()返回的结果能用来生成实例 >>> v1 == v1_clone # Vector2d需支持 == 运算符 True >>> print(v1) # 咱们但愿__str__方法以以下形式返回实例的字符串表示 (3.0, 4.0) >>> octets = bytes(v1) # 可以生成字节序列 >>> octets b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@' >>> abs(v1) # 可以求模 5.0 >>> bool(v1), bool(Vector2d(0, 0)) # 能进行布尔运算 (True, False)
Vector2d
的初始版本以下:bash
# 代码2 from array import array import math class Vector2d: # 类属性,在Vector2d实例和字节序列之间转换时使用 typecode = "d" # 转换成C语言中的double类型 def __init__(self, x, y): self.x = float(x) # 构造是就转换成浮点数,尽早在构造阶段就捕获错误 self.y = float(y) def __iter__(self): # 将Vector2d实例变为可迭代对象 return (i for i in (self.x, self.y)) # 这是生成器表达式! def __repr__(self): class_name = type(self).__name__ # 获取类名,没有采用硬编码 # 因为Vector2d实例是可迭代对象,因此*self会把x和y提供给format函数 return "{}({!r}, {!r})".format(class_name, *self) def __str__(self): return str(tuple(self)) # 由可迭代对象构造元组 def __bytes__(self): # ord()返回字符的Unicode码位;array中的数组的元素是double类型 return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))) def __eq__(self, other): # 这样实现有缺陷,Vector(3, 4) == [3, 4]也会返回True return tuple(self) == tuple(other) # 但这个缺陷会在后面章节修复 def __abs__(self): # 计算平方和的非负数根 return math.hypot(self.x, self.y) def __bool__(self): # 用到了上面的__abs__来计算模,若是模为0,则是False,不然为True return bool(abs(self))
第一版Vector2d
可将它的实例转换成字节序列,但却不能从字节序列构造Vector2d
实例,下面添加一个方法实现此功能:微信
# 代码3 class Vector2d: -- snip -- @classmethod def frombytes(cls, octets): # 不用传入self参数,但要经过cls传入类自己 typecode = chr(octets[0]) # 从第一个字节中读取typecode,chr()将Unicode码位转换成字符 # 使用传入的octets字节序列构建一个memoryview,而后根据typecode转换成所须要的数据类型 memv = memoryview(octets[1:]).cast(typecode) return cls(*memv) # 拆包转换后的memoryview,而后构造一个Vector2d实例,并返回
代码3
中用到了@classmethod
装饰器,与它相伴的还有@staticmethod
装饰器。闭包
从上述代码能够看出,classmethod
定义的是传入类而不是传入实例的方法,即传入的第一个参数必须是类,而不是实例。classmethod
改变了调用方法的方式,可是,在实际调用这个方法时,咱们不须要手动传入cls
这个参数,Python会自动传入。(按照传统,第一个参数通常命名为cls
,固然你也能够另起名)函数
staticmethod
也会改变方法的调用方式,但第一个参数不是特殊值,既不是cls
,也不是self
,就是用户传入的普通参数。如下是它们的用法对比:学习
# 代码4 >>> class Demo: ... @classmethod ... def klassmeth(*args): ... return args # 返回传入的所有参数 ... @staticmethod ... def statmeth(*args): ... return args # 返回传入的所有参数 ... >>> Demo.klassmeth() (<class 'Demo'>,) # 无论如何调用Demo.klassmeth,它的第一个参数始终是Demo类本身 >>> Demo.klassmeth("spam") (<class 'Demo'>, 'spam') >>> Demo.statmeth() () # Demo.statmeth的行为与普通函数相似 >>> Demo.statmeth("spam") ('spam',)
classmethod
颇有用,但staticmethod
通常都能找到很方便的替代方案,因此staticmethod
并非必须的。网站
内置的format()
函数和str.format()
方法把各个类型的格式化方式委托给相应的.__format__(format_spec)
方法。format_spec
是格式说明符,它是:
format(my_obj, format_spec)
的第二个参数;也是str.format()
方法的格式字符串,{}
里替换字段中冒号后面的部分,例如:
# 代码5 >>> brl = 1 / 2.43 >>> "1 BRL = {rate:0.2f} USD".format(rate=brl) # 此时 format_spec为'0.2f'
其中,冒号后面的0.2f
是格式说明符,冒号前面的rate
是字段名称,与格式说明符无关。格式说明符使用的表示法叫格式规范微语言(Format Specification Mini-Language)。格式规范微语言为一些内置类型提供了专门的表示代码,好比b
表示二进制的int
类型;同时它仍是可扩展的,各个类能够自行决定如何解释format_spec
参数,好比时间的转换格式%H:%M:%S
,就可用于datetime
类型,但用于int
类型则可能报错。
若是类没有定义__format__
方法,则会返回__str__
的结果,好比咱们定义的Vector2d
类型就没有定义__format__
方法,但依然能够调用format()
函数:
# 代码6 >>> v1 = Vector2d(3, 4) >>> format(v1) '(3.0, 4.0)'
但如今的Vector2d
在格式化显示上还有缺陷,不能向format()
传入格式说明符:
>>> format(v1, ".3f") Traceback (most recent call last): -- snip -- TypeError: non-empty format string passed to object.__format__
如今咱们来为它定义__format__
方法。添加自定义的格式代码,若是格式说明符以'p'
结尾,则以极坐标的形式输出向量,即<r, θ>
,'p'
以前的部分作正常处理;若是没有'p'
,则按笛卡尔坐标形式输出。为此,咱们还须要一个计算弧度的方法angle
:
# 代码7 class Vector2d: -- snip -- 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()) outer_fmt = "<{}, {}>" else: coords = self outer_fmt = "({}, {})" components = (format(c, format_spec) for c in coords) return outer_fmt.format(*components)
如下是实际示例:
# 代码8 >>> format(Vector2d(1, 1), "0.5fp") '<1.41421, 0.78540>' >>> format(Vector2d(1, 1), "0.5f") '(1.00000, 1.00000)'
关于可散列的概念能够参考以前的文章《Python学习之路22》。
目前的Vector2d
是不可散列的,为此咱们须要实现__hash__
特殊方法,而在此以前,咱们还要让向量不可变,即self.x
和self.y
的值不能被修改。之因此要让向量不可变,是由于咱们在计算向量的哈希值时须要用到self.x
和self.y
的哈希值,若是这两个值可变,那向量的哈希值就能随时变化,这将不是一个可散列的对象。
补充:
id()
的返回值。可是此处的Vector2d
倒是不可散列的,这是为何?其实,若是咱们要让自定义类变为可散列的,正确的作法是同时实现__hash__
和__eq__
这两个特殊方法。当这两个方法都没有重写时,自定义类的哈希值就是id()
的返回值,此时自定义类可散列;当咱们只重写了__hash__
方法时,自定义类也是可散列的,哈希值就是__hash__
的返回值;可是,若是只重写了__eq__
方法,而没有重写__hash__
方法,此时自定义类便不可散列。这里再次给出可散列对象必须知足的三个条件:
hash()
函数,而且经过__hash__
方法所获得的哈希值是不变的;__eq__
方法来检测相等性;a == b
为真,则hash(a) == hash(b)
也必须为真。根据官方文档,最好使用异或运算^
混合各份量的哈希值,下面是Vector2d
的改进:
# 代码9 class Vector2d: -- snip -- def __init__(self, x, y): self.__x = float(x) self.__y = float(y) @property # 把方法变为属性调用,至关于getter方法 def x(self): return self.__x @property def y(self): return self.__y def __hash__(self): return hash(self.x) ^ hash(self.y) -- snip --
文章至此说的都是一些特殊方法,若是想到获得功能完善的对象,这些方法多是必备的,但若是你的应用用不到这些东西,则彻底没有必要去实现这些方法,客户并不关心你的对象是否符合Python风格。
Vector2d
暂时告一段落,如今来讲一说其它比较杂的内容。
Python不像C++、Java那样能够用private
关键字来建立私有属性,但在Python中,能够以双下划线开头来命名属性以实现"私有"属性,可是这种属性会发生名称改写(name mangling):Python会在这样的属性前面加上一个下划线和类名,而后再存入实例的__dict__
属性中,以最新的Vector2d
为例:
# 代码10 >>> v1 = Vector2d(1, 2) >>> v1.__dict__ {'_Vector2d__x': 1.0, '_Vector2d__y': 2.0}
当属性以双下划线开头时,实际上是告诉别的程序员,不要直接访问这个属性,它是私有的。名称改写的目的是避免意外访问,而不能防止故意访问。只要你知道规则,这些属性同样能够访问。
还有以单下划线开头的属性,这种属性在Python的官方文档的某个角落里被称为了"受保护的"属性,但Python不会对这种属性作特殊处理,这只是一种约定俗成的规矩,告诉别的程序员不要试图从外部访问这些属性。这种命名方式很常见,但其实不多有人把这种属性叫作"受保护的"属性。
仍是那句话,Python中全部的属性都是公有的,Python没有不能访问的属性!这些规则并不能阻止你有意访问这些属性,一切都看你遵不遵照上面这些"不成文"的规则了。
这里首先须要区分两个概念,类属性与实例属性:
Vector2d
中定义typecode
的方式来定义类属性,即直接在class
中定义属性,而不是在__init__
中;self
绑定,self
指向的是实例,而不是类。Python有个很独特的特性:类属性可用于为实例属性提供默认值。
Vector2d
中有个typecode
类属性,注意到,咱们在__bytes__
方法中经过self.typecode
两次用到了它,这里明明是经过self
调用实例属性,可Vector2d
的实例并无这个属性。self.typecode
其实获取的是Vector2d.typecode
类属性的值,而至于怎么从实例属性跳到类属性的,之后有机会单独用一篇文章来说。
补充:证实实例没有typecode
属性
# 代码11 >>> v = Vector2d(1, 2) >>> v.__dict__ {'_Vector2d__x': 1.0, '_Vector2d__y': 2.0} # 实例中并无typecode属性
若是为不存在的实例属性赋值,则会新建该实例属性。假如咱们为typecode
实例属性赋值,同名类属性不会受到影响,但会被实例属性给覆盖掉(相似于以前在函数闭包中讲的局部变量和全局变量的区别)。借助这一特性,能够为各个实例的typecode
属性定制不一样的值,好比在生成字节序列时,将实例转换成4字节的单精度浮点数:
# 代码12 >>> v1 = Vector2d(1.1, 2.2) >>> dumpd = bytes(v1) # 按双精度转换 >>> dumpd b'd\x9a\x99\x99\x99\x99\x99\xf1?\x9a\x99\x99\x99\x99\x99\x01@' >>> len(dumpd) 17 >>> v1.typecode = "f" >>> dumpf = bytes(v1) # 按单精度转换 >>> dumpf b'f\xcd\xcc\x8c?\xcd\xcc\x0c@' # 明白为何要在字节序列前加上typecode的值了吗?为了支持不一样格式。 >>> len(dumpf) 9 >>> Vector2d.typecode 'd'
若是想要修改类属性的值,必须直接在类上修改,不能经过实例修改。若是想修改全部实例的typecode
属性的默认值,能够这么作:
# 代码13 Vector2d.typecode = "f"
然而有种方式更符合Python风格,并且效果持久,也更有针对性。经过继承的方式修改类属性,生成专门的子类。Django基于类的视图就大量使用了这个技术:
# 代码14 >>> class ShortVector2d(Vector2d): ... typecode = "f" # 只修改这一处 ... >>> sv = ShortVector2d(1/11, 1/27) >>> sv ShortVector2d(0.09090909090909091, 0.037037037037037035) # 没有硬编码class_name的缘由 >>> len(bytes(sv)) 9
默认状况下,Python在各个实例的__dict__
属性中以映射类型存储实例属性。正如《Python学习之路22》中所述,为了使用底层的散列表提高访问速度,字典会消耗大量内存。若是要处理数百万个属性很少的实例,其实能够经过__slots__
类属性来节省大量内存。作法是让解释器用相似元组的结构存储实例属性,而不是字典。
具体用法是,在类中建立这个__slots__
类属性,并把它的值设为一个可迭代对象,其中的元素是其他实例属性的字符串表示。好比咱们将以前定义的Vector2d
改成__slots__
版本:
# 代码15 class Vector2d: __slots__ = ("__x", "__y") typecode = "d" # 其他保持不变 -- snip --
试验代表,建立一千万个以前版本的Vector2d
实例,内存用量高达1.5GB,而__slots__
版本的Vector2d
的内存用量不到700MB,而且速度也比以前的版本快。
但__slots__
也有一些须要注意的点:
__slots__
以后,实例不能再有__slots__
中所列名称以外的属性,即,不能动态添加属性;若是要使其能动态添加属性,必须在其中加入'__dict__'
,但这么作又违背了初衷;__slots__
属性,解释器会忽略掉父类的__slots__
属性;__weakref__
属性,但若是定义了__slots__
属性,并且还要自定义类支持弱引用,则须要把'__weakref__'
加入到__slots__
中。总之,不要滥用__slots__
属性,也不要用它来限制用户动态添加属性(除非有意为之)。__slots__
在处理列表数据时最有用,例如模式固定的数据库记录,以及特大型数据集。然而,当遇到这类数据时,更推荐使用Numpy和Pandas等第三方库。
本篇首先按照必定的要求,定义了一个Vector2d
类,重点是若是实现这个类的不一样输出形式;随后,能从字节序列"反编译"成咱们须要的类,咱们实现了一个备选构造方法,顺带介绍了@classmethod
和@staticmethod
装饰器;接着,咱们经过重写__format_
方法,实现了自定义格式化输出数据;而后,经过使用@property
装饰器,定义"私有"属性以及重写__hash__
方法等操做实现了这个类的可散列化。至此,关于Vector2d
的内容基本结束。最后,咱们介绍了两种常见类型的属性(“私有”,“保护”),覆盖类属性以及如何经过__slots__
节省内存等问题。
本文实现了这么多特殊方法只是为展现如何编写标准Python对象的API,若是你的应用用不到这些内容,大可没必要为了知足Python风格而给本身增长负担。毕竟,简洁胜于复杂。
迎你们关注个人微信公众号"代码港" & 我的网站 www.vpointer.net ~