11. 符合Python风格的对象
一个库或框架是否符合Python风格 , 要看它能不能让Python程序员以一种简单而自然的方式执行任务 .
--Martijn Faassen Python 和JavaScript 框架开发者①
( 注 1 : 摘自Faassen的题为 'What is Pythonic?' 的博客文章 . )
得益于Python数据模型 , 自定义类型的行为可以像内置类型那样自然 .
实现如此自然的行为 , 靠的不是继承 , 而是鸭子类型 : 只需按照预定行为实现对象所需的方法即可 .
前几章分析了很多内置对象的行为 .
本章会自己定义类 , 让类的行为跟真正的Python对象一样 .
你在开发应用程序时 , 不一定要像本章的示例那样实现那么多特殊方法 .
然而 , 对库或框架来说 , 程序员可能希望你定义的类能像Python内置的类一样 .
满足这个预期也算得上是符合 'Python风格' .
本章接续第 1 章 , 说明如何实现很多Python类型中常见的特殊方法 .
本章包含以下话题 :
? 把对象转换成其他类型的内置函数 ( 例如repr ( ) , bytes ( ) , complex ( ) 等 ) ;
? 通过一个类方法实现备选构造函数 ;
? 扩展f字符串 , 内置函数format ( ) 和str . format ( ) 方法使用的格式化微语言 ;
? 实现只读属性 ;
? 把对象变为可哈希的 , 以便在集合中及作为dict的键使用 ;
? 利用__slots__节省内存 .
本章将开发一个简单的二维欧几里得向量类型Vector2d ,
在这个过程中涵盖上述全部话在实现这个类型的中间阶段 , 本章会讨论如下两个概念 :
? 如何以及何时使用 @ classmethod装饰器和 @ staticmethod装饰器 ;
? Python中私有属性和受保护属性的用法 , 约定和局限 .
11.1 本章新增内容
本章引言部分内容换了 , 第 2 段又增加了几句话 , 说清 '符合Python 风格' 是什么意思 .
第 1 版直到全书最后才讨论这个问题 .
11.6 节有更新 , 增加了Python3 . 6 引入的f字符串 .
但那一节改动不大 , 因为f字符串支持的格式化微语言与内置函数format ( ) 和str . format ( ) 方法一样 ,
以前实现的_format_方法也适用于f字符串 .
本章其他内容基本没变 , 毕竟自Python 3.0 以来 , 特殊方法没什么变化 , 而且核心理念在Python2 . 2 中就出现了 .
从对象表示形式函数开始讲起 .
11.2 对象表示形式
每门面向对象语言至少都有一种获取对象字符串表示形式的标准方式 .
Python提供了两种方式 .
repr ( )
以便于开发者理解的方式返回对象的字符串表示形式 .
Python 控制台或调试器在显示对象时采用这种方式 .
str ( )
以便于用户理解的方式返回对象的字符串表示形式 .
使用print ( ) 打印对象时采用这种方式 .
第 1 章讲过 , 在背后支持repr ( ) 和str ( ) 的是特殊方法__repr__和__str__ .
除此之外 , 还有两个特殊方法 ( __bytes__和__format__ ) 可为对象提供其他表示形式 .
__bytes__方法与__str__方法类似 , bytes ( ) 函数调用它获取对象的字节序列表示形式 .
而__format__方法供f字符串 , 内置函数format ( ) 和str . format ( ) 方法使用 ,
通过调用obj . __format__ ( format_spec ) 以特殊的格式化代码显示对象的字符串表示形式 .
本章将先讨论__bytes__方法 , 随后再讨论__format__方法 .
* * * ----------------------------------------------------------------------------------------- * * *
如果你是Python 2 用户 , 那么请记住 , 在Python3中 ,
__repr__ , __str___和__format__都必须返回Unicode字符串 ( str 类型 ) .
只有__bytes__方法应该返回字节序列 ( bytes类型 ) .
* * * ----------------------------------------------------------------------------------------- * * *
11.3 再谈向量类
本章将以一个与第 1 章类似的Vector2d类为例 , 说明用于生成对象表示形式的众多方法 .
本节和接下来的几节会逐渐实现这个类 .
我们期望Vector2d实例具有的基本行为如示例 11 - 1 所示 .
>> > v1 = Vector2d( 3 , 4 )
>> > print ( v1. x, v1. y)
3.0 4.0
>> > ×, y = v1
( 3.0 , 4.0 )
>> > v1
Vector2d( 3.0 , 4.0 )
>> > v1_clone = eval ( repr ( v1) )
>> > v1 == v1_clone
True
>> > print ( v1)
( 3.0 , 4.0 )
>> > octets = bytes ( v1)
>> > octets
b'd\\x00\\x0O\\xO0\\x00\|x00\|xO0\|xO8@\\x00\\x00\|x00\\x00\|x00\|x00\|x10@'
>> > abs ( v1)
5.0
>> > bool ( v1) , bool ( Vector2d( 0 , 0 ) )
( True , False )
示例 11 - 1 中的Vector2d类在vector2d_v0 . py 文件中实现 ( 参见示例 11 - 2 ) .
这段代码基于示例 1 - 2 , 支持 + 运算和 * 运算的方法将在第 16 章实现 .
另外 , 为了方便测试 , 示例 11 - 2 增加了支持 = = 运算符的方法 .
现在 , vector2d用到了几个特殊方法 , 这些方法提供的操作是Python程序员预期设计良好的对象应当提供的 .
from array import array
import math
class Vector2d :
typecode = 'd'
"""
typecode属性为'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. typecode) ] ) +
bytes ( array( self. typecode, 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) )
* * * ----------------------------------------------------------------------------------------- * * *
示例 11 - 2 中的__eq__方法 , 在两个运算对象都是Vector2d实例时没有向题 ,
不过拿Vector2d实例与其他具有相同数值的可迭代对象相比 , 结果也是True ( 例如 , Vector ( 3 , 4 ) = = [ 3 , 4 ] ) .
这个行为既可以被视为特性 , 也可以被视为bug .
第 16 章在讲到运算符重载时将进一步讨论 .
* * * ----------------------------------------------------------------------------------------- * * *
我们已经定义了很多基本方法 , 但是显然少了一个操作 :
使用bytes ( ) 函数生成的二进制表示形式重建Vector2d实例 .
11.4 备选构造函数
现在可以把Vector2d实例转换成字节序列了 .
同理 , 我们也希望能从字节序列构建vector2d 实例 .
在标准库中探索一番之后 , 我们发现array . array 有个类方法 . frombytes ( 2.10 .1 节介绍过 ) 正好符合需求 .
下面在vector2d_v1 . py 文件中为Vector2d定义一个同名类方法 , 如示例 11 - 3 所示 .
@classmethod
def frombytes ( cls, octets) :
typecode = chr ( octets[ 0 ] )
memv = memoryview ( octets[ 1 : ] ) . cast( typecode)
return cls( * memv)
我们用的classmethod装饰器是Python特有的 , 下面来讲解一下 .
11.5 classmethod与staticmethod
Python官方教程没有提到classmethod装饰器 , 也没有提到staticmethod .
学过Java面向对象编程的人可能觉得奇怪 , 为什么Python提供两个这样的装饰器 , 而不是只提供一个?
先来看classmethod .
示例 11 - 3 展示了它的用法 : 定义操作类而不是操作实例的方法 .
由子classmethod改变了调用方法的方式 , 因此接收的第一个参数是类本身 , 而不是实例 .
classnethod最常见的用途是定义备选构造函数 , 例如示例 11 - 3 中的frombytes .
注意 , frombytes的最后一行使用cls参数构建了一个新实例 , 即cls ( * memv ) .
相比之下 , staticmethod装饰器也会改变方法的调用方式 , 使其接收的第一个参数没什么特殊的 .
其实 , 静态方法就是普通的函数 , 只是碰巧位于类的定义体中 , 而不是在模块层定义 .
示例 11 - 4 对classmethod和staticmethod的行为做了对比 .
class Demo :
. . . @classmethod
. . . def klassmeth ( * args) :
. . . return args
. . . @staticmethod
. . . def statmeth ( * args) :
. . . return args
>> > Demo. klassmeth( )
( < class '__main__.Demo' > , )
>> > Demo. klassmeth( 'spam' )
( < class '__main__.Demo' > , 'spam' )
Demo. statmeth( )
( )
Demo. statmeth( 'spam' )
( 'spam' , )
classmethod装饰器非常有用 , 但是我从未见过不得不使用staticmethod的情况 .
有些函数即使不直接处理类 , 也与类联系紧密 , 因此你会想把函数与类放在一起定义 .
对于这种情况 , 在类的前面或后面定义函数 , 保持二者在同一个模块中基本上就可以了 . ⑤
现在 , 我们对classmethod的作用已经有所了解 ( 而且知道staticmethod不是特别有用 ) .
下面继续讨论对象的表示形式 , 说明如何支持格式化输出 .
( 注 5 : 本书的技术审校之--Leonardo Rochael不同意我对staticmethod的贬低 ,
作为反驳 , 他推荐阅读Julien . Dunyou写的一篇题为 ' The Definitive Guide on How to Use Static ,
Class or Abstract Methrods i Pytben ' 的博客文章 .
Danjou的这篇文章写得很好 , 我推荐阅读 . 但是 , 我对staticnethod的观点依然不变 . 请读者自辦 . )
11.6 格式化显示
f字符串 , 内置函数format ( ) 和str . format ( ) 方法
会把各种类型的格式化方式委托给相应的 . __format__ ( format_spec ) 方法 .
format_spec是格式说明符 , 它是 :
? format ( my_obj , format_spec ) 的第二个参数 ;
? { } 内代换字段中冒号后面的部分 , 或者fnt . str . format ( ) 中的fnt .
请看以下示例 .
>> > brl= 1 / 4.82
>> > brl
0.20746887966804978
>> > format ( brl, '0.4f' )
'0.2075'
>> > '1 BRL = {rate: 0.2f} USD' . format ( rate= brl)
'1 BRL = 0.21 USD'
>> > f'1 USD = { 1 / brl: 0.2f } BRL'
'1 USD = 4.82 BRL'
第 2 和第 3 个标号指出了一个重要知识点 :
像 '{0.mass: 5.3e}' 这样的格式字符串其实包含两部分 ,
冒号左边的 '0.mass' 在代换字段句法中是字段名 , 在f字符串中可以是任意表达式 ;
冒号右边的 '5.3e' 是格式说明符 .
格式说明符使用的表示法叫格式规范微语言 ( Format Specification Mini-Language ) .
* --------------------------------------------------------------------------------------------- *
如果你对f字符串 , format ( ) 和str . format ( ) 感到陌生 ,
根据我的教学经验 , 最好先学内置函数format ( ) 因为它只使用格式规范微语言 .
学会这些表示法之后 , 再阅读 'Formatted String Literals' 和 'Format StringSyntax' ,
学习f字符串和str . format ( ) 方法使用的代换字段表示法 { : } ( 包括转换标志 ! s , ! r和 ! a ) .
f字符串出现之后 , str . format ( ) 并没有被淘汰 :
f字符串适用于多数情况 , 不过有时候格式化字符串不在渲染的地方而是在别处指定 .
* --------------------------------------------------------------------------------------------- *
格式规范微语言为一些内置类型提供了专用的表示代码 .
例如 , b和×分别表示二进制和十六进制的int类型 , f表示小数形式的float类型 , 而 % 表示百分数形式 .
>> > format ( 42 , 'b' )
'101010'
>> > format ( 2 / 3 , '.1%' )
166.7 %
格式规范微语言是可扩展的 , 各个类可以自行决定如何解释format_spec参数 .
例如 , datetime模块中的类的__format__方法所使用的格式代码与strftime ( ) 函数一样 .
下面是内置函数format ( ) 和str . format ( ) 方法的几个示例 .
>> > from datetime import datetime
>> > now = datetime. now( )
>> > format ( now, '%H:%M:%S' )
18 : 49 : 05
>> > "It's now {:%I:%M %p}" . format ( now)
"It's now 06:49 PM"
如果一个类没有定义__format__ , 那么该方法就会从object继承 , 并返回str ( my_object ) .
由于Vector2d类有__str__方法 , 因此可以这样做 .
>> v1 = Vector2d( 3 , 4 )
>> > format ( v1)
'(3.0, 4.0)'
然而 , 如果传入格式说明符 , 则object . __format__会抛出TypeError .
>> > format ( v1, '.3f' )
Traceback ( most recent call last) :
. . .
TypeError: non- empty format string passed to object . __format__
我们将实现自己的格式微语言来解决这个问题 .
首先 , 假设用户提供的格式说明符是用于格式化向量中各个float分量的 .
我们想达到的效果如下所示 .
>> > v1 = Vector2d( 3 , 4 )
>> > format ( v1)
'(3.0, 4.0)'
>> > format ( v1, '.2f' )
'( 3.00 , 4.00 )
>> > format ( v1, '.3e' )
'(3.000e+00, 4.000e+00)'
实现这种输出的__format__方法如示例 11 - 5 所示 .
def __format__ ( self, fmt_spec= '' ) :
components = ( format ( c, fmt_spec) for c in self)
return '({}, {})' . format ( * components)
然后 , 在微语言中添加一个自定义的格式代码 :
如果格式说明符以 'P' 结尾 , 就在极坐标中显示向量 , 即 < r , θ > , 其中r是模 , θ ( 西塔 ) 是弧度 .
格式说明符中的其他部分 ( 'p' 前面 ) 像往常那样解释 .
* --------------------------------------------------------------------------------------------- *
为自定义的格式代码选择字母时 , 我会避免使用其他类型用过的字母 .
根据格式规范微语言文档 , 整数使用的代码是 'bcdoxXn' ,
浮点数使用的代码是eEfFgGn % ', 字符串使用的代码是' s ' .
因此 , 我为极坐标选的代码是 'P' .
因为各个类会使用自己的方式解释格式代码 ,
所以在自定义的格式代码中重复使用代码字母不会出错 , 但是可能会让用户感到困惑 .
* --------------------------------------------------------------------------------------------- *
为了生成极坐标 , 我们已经定义了计算模的__abs__方法 , 但还要定义一个简单的angle方法 ,
使用math . atan2 ( ) 函数计算角度 . angle方法的代码如下所示 .
def angle ( self) :
return math. atan2( self. y, self. x)
现在可以增强__format__方法 , 计算极坐标了 , 如示例 11 - 6 所示 .
def __format__ ( self, fmt_spec= '' ) :
if fmt_spec. endswith( 'p' ) :
fmt_spec = fmt_spec[ : - 1 ]
coords = ( abs ( self) , self. angle( ) )
outer_fmt = '<{}, {}>'
else :
coords = self
outer_fmt = '({}, {})'
components = ( format ( c, fmt_spec) for c in coords)
return outer_fmt. format ( * components)
import math
class Vector2d :
typecode = '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 __abs__ ( self) :
return math. hypot( self. x, self. y)
def angle ( self) :
return math. atan2( self. y, self. x)
def __format__ ( self, fmt_spec= '' ) :
if fmt_spec. endswith( 'p' ) :
fmt_spec = fmt_spec[ : - 1 ]
coords = ( abs ( self) , self. angle( ) )
outer_fmt = '<{}, {}>'
else :
coords = self
outer_fmt = '({}, {})'
components = ( format ( c, fmt_spec) for c in coords)
return outer_fmt. format ( * components)
运行示例 11 - 6 , 结果如下所示 .
>> > format ( Vector2d( 1 , 1 ) , 'p' )
< 1.4142135623730951 , 0.7853981633974483 >
>> > format ( Vector2d( 1 , 1 ) , '.3ep' )
"< 1.414e+00 , 7.854e-01 > '
>> > format ( Vector2d( 1 , 1 ) , '0.5fp' )
< 1.41421 , 0.78540 >
如本节所示 , 为用户定义的类型扩展格式规范微语言并不难 .
下面换一个不仅仅事关对象外在表现的话题 .
我们将把Vector2d变成可哈希的 , 以便构建向量集合 , 或者把向量当作dict的键使用 .
11.7 可哈希的Vector2d
按照定义 , 目前Vector2d实例不可哈希 , 因此不能放入集合中 .
>> > V1 = Vector2d( 3 , 4 )
>> > hash ( v1)
Traceback ( most recent call last) :
. . .
TypeError: unhashable type : 'Vector2d'
>> > set ( [ v1] )
Traceback ( most recent call last) :
. . .
TypeError: unhashable type : 'Vector2d"
为了把Vector2d实例变成可哈希的 , 必须实现__hash__方法 ( 还需要__eq__方法 , 前面已经实现了 ) .
此外 , 还要让向量实例不可变 ( 详见 3.4 .1 节 ) .
目前 , 可以为分量赋新值 ( 例如v1 . x = 7 ) , Vector2d类的代码并不阻止这么做 .
而我们想要的行为如下所示 .
>> > v1. x, v1. y
( 3.0 , 4.0 )
>> > v1. x = 7
Traceback ( most recent call last) :
. . .
AttributeError: can't set attribute
为此 , 要把×分量和y分量设为只读特性 , 如示例 11 - 7 所示 .
class Vector2d :
typecode = 'd'
def __init__ ( self, x, y)
self. __x= float ( x)
self. __y = float ( y)
@property
def x ( self) :
return self. __x
@property
def y ( self) :
return self. __y
def __iter__ ( self) :
return ( i for i in ( self. x, self. y) )
* * * ----------------------------------------------------------------------------------------- * * *
Vector . x和Vector . y是只读特性 .
第 22 章将讨论读写特性 , 届时会深入说明 @ property装饰器 .
* * * ----------------------------------------------------------------------------------------- * * *
现在 , 向量不会被意外修改 , 有了一定的安全性 , 接下来可以实现__hash__方法了 .
这个方法应该返回一个int值 , 理想情况下还要考虑对象属性的哈希值 ( __eq__方法也是如此 ) ,
因为相等的对象应该具有相同的哈希值 .
特殊方法__hash__的文档建议根据元组的分量计算哈希值 , 例 11 - 8 所示 .
def __hash__ ( self) :
return hash ( ( self. x, self. y) )
实现__hash__方法之后 , 向量就变成可哈希的了 .
>> > v1 = Vector2d( 3 , 4 )
>> > v2 = Vector2d( 3.1 , 4.2 )
>> > hash ( v1) , hash ( v2)
( 1079245023883434373 , 1994163070182233067 )
>> > { v1, v2}
{ Vector2d( 3.1 , 4.2 ) , Vector2d( 3.0 , 4.0 ) }
* --------------------------------------------------------------------------------------------- *
为了创建可哈希的类型 , 不一定要实现特性 , 也不一定要保护实例属性 , 正确实现__hash__方法和__eq__方法即可 .
但是 , 可哈希对象的值绝不应该变化 , 因此我们借机提到了只读特性 .
* --------------------------------------------------------------------------------------------- *
如果你定义的类型有标量数值 , 那么可能还要实现__int__方法和__float__方法
( 分别被int ( ) 构造函数和float ( ) 构造函数调用 ) , 以便在某些情况下强制转换类型 .
此外 , 还有用于支持内置构造函数complex ( ) 的__complex__方法 .
Vector2d或许应该提供__complex__方法 , 这就给读者留作练习吧 .
import math
class Vector2d :
def __init__ ( self, x, y) :
self. x = float ( x)
self. y = float ( y)
def __abs__ ( self) :
return math. hypot( self. x, self. y)
def __float__ ( self) :
return float ( abs ( self) )
def __complex__ ( self) :
return complex ( self. x, self. y)
v1 = Vector2d( 3 , 4 )
print ( float ( v1) )
print ( complex ( v1) )
11.8 支持位置模式匹配
目前 , vector2d实例兼容关键字类模式 ( 参见 5.8 .2 节 ) .
在示例 11 - 9 中 , 所有关键字模式都能按预期匹配 .
def keyword_pattern_demo ( v: Vector2d) - > None :
match v:
case Vector2d( x= 0 , y= 0 ) :
print ( f' { v!r } is null' )
case Vector2d( x= 0 ) :
print ( f' { v!r } is vertical' )
case Vector2d( y= 0 ) :
print ( f' { v!r } is horizontal' )
case Vector2d( x= x, y= y) if x== y:
print ( f' { v!r } is diagonal' )
case _ :
print ( f' { v!r } is awesome' )
然而 , 如果使用如下位置模式 :
case Vector2d( _, 0 ) :
print ( f' { v!r } is horizontal' )
则会得到如下结果 .
TypeError: Vector2d( ) accepts 0 positional sub- patterns ( 1 given)
为了让vector2d支持位置模式 , 需要添加一个名为__match_args__的类属性 ,
按照在位置模式匹配中的使用顺序列出实例属性 .
class Vector2d :
__match_args__ = ( 'x' , 'y' )
现在 , 编写匹配vector2d对象的模式时可以少敲几次键盘了 , 如示例 11 - 10 所示 .
def positional_pattern_demo ( v: Vector2d) - > None :
match v:
case Vector2d( 0 , 0 ) :
print ( f' { v!r } is null' )
case Vector2d( 0 ) :
print ( f' { v!r } is vertical' )
case Vector2d( _, 0 ) :
print ( f' { v!r } is horizontal' )
case Vector2d( x, y) if x== y:
print ( f' { v!r } is diagonal' )
case _ :
print ( f' { v!r } is awesome' )
import math
class Vector2d :
__match_args__ = ( 'x' , 'y' )
typecode = '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 __abs__ ( self) :
return math. hypot( self. x, self. y)
def __repr__ ( self) :
class_name = type ( self) . __name__
return '{}({!r}, {!r})' . format ( class_name, * self)
def keyword_pattern_demo ( v: Vector2d) - > None :
match v:
case Vector2d( x= 0 , y= 0 ) :
print ( f' { v!r } is null' )
case Vector2d( x= 0 ) :
print ( f' { v!r } is vertical' )
case Vector2d( _, 0 ) :
print ( f' { v!r } is horizontal' )
case Vector2d( x= x, y= y) if x == y:
print ( f' { v!r } is diagonal' )
case _ :
print ( f' { v!r } is awesome' )
obj = Vector2d( 1 , 0 )
keyword_pattern_demo( obj)
__match_args__类属性不一定要把所有公开的实例属性都列出来 .
如果一个类的__init__方法可能有全都赋值给实例属性的必需的参数和可选的参数 ,
那么__match_args__应当列出必需的参数 , 而不必列出可选的参数 .
现在 , 暂停一下 , 看看Vector2d类目前的代码 .
11.9 第3版Vector2d的完整代码
前面一直在定义Vector2d类 , 不过每次只给出了部分片段 .
示例 11 - 11 是整理后的完整代码清单 , 保存在vector2d_v3 . py文件中 , 包含我在开发时编写的doctest .
"""
一个二维向量类
>>> v1 = Vector2d(3, 4)
>>> print(v1.x, v1.y)
3.0 4.0
>>> x, y = v1
>>> x, y
(3.0, 4.0)
>>> v1
Vector2d(3.0, 4.0)
>>> v1_clone = eval(repr(v1))
>>> v1 == v1_clone
True
>>> print(v1)
(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)
测试类方法``.frombytes()``::
>>> v1_clone = Vector2d.frombytes(bytes(v1))
>>> v1_clone
Vector2d(3.0, 4.0)
>>> v1 == v1_clone
True
使用笛卡儿坐标测试``format()``::
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'
测试``angle``方法::
>>> Vector2d(0, 0).angle()
0.0
>>> Vector2d(1, 0).angle()
0.0
>>> epsilon = 10**-8
>>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon
True
>>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon
True
使用极坐标测试``format()``::
>>> format(Vector2d(1, 1), 'p') # doctest:+ELLIPSIS
'<1.414213..., 0.785398...>'
>>> format(Vector2d(1, 1), '.3ep')
'<1.414e+00, 7.854e-01>'
>>> format(Vector2d(1, 1), '0.5fp')
'<1.41421, 0.78540>'
测试只读特性`x`和`y`::
>>> v1.x, v1.y
(3.0, 4.0)
>>> v1.x = 123
Traceback (most recent call last):
...
AttributeError: can't set attribute 'x'
测试哈希::
>>> v1 = Vector2d(3, 4)
>>> v2 = Vector2d(3.1, 4.2)
>>> len({v1, v2})
2
"""
from array import array
import math
class Vector2d :
__match_args__ = ( 'x' , 'y' )
typecode = 'd'
def __init__ ( self, x, y) :
self. __x = float ( x)
self. __y = float ( y)
@property
def x ( self) :
return self. __x
@property
def y ( self) :
return self. __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. typecode) ] ) +
bytes ( array( self. typecode, self) ) )
def __eq__ ( self, other) :
return tuple ( self) == tuple ( other)
def __hash__ ( self) :
return hash ( ( self. x, self. y) )
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, fmt_spec= '' ) :
if fmt_spec. endswith( 'p' ) :
fmt_spec = fmt_spec[ : - 1 ]
coords = ( abs ( self) , self. angle( ) )
outer_fmt = '<{}, {}>'
else :
coords = self
outer_fmt = '({}, {})'
components = ( format ( c, fmt_spec) for c in coords)
return outer_fmt. format ( * components)
@classmethod
def frombytes ( cls, octets) :
typecode = chr ( octets[ 0 ] )
memv = memoryview ( octets[ 1 : ] ) . cast( typecode)
return cls( * memv)
小结一下 , 前两节说明了一些特殊方法 , 要想得到功能完善的对象 , 这些方法可能是必备的 .
* * * ----------------------------------------------------------------------------------------- * * *
当应用程序真正需要这些特殊方法时才应实现它们 .
终端用户并不关心应用程序中的对象是否符合 'Python风格' .
另外 , 如果你的类是供其他Python程序员使用的库的一部分 , 那么你肯定猜不到程序员会对你的对象做什么 ,
他们或许更希望你的代码符合 'Python风格' .
* * * ----------------------------------------------------------------------------------------- * * *
示例 11 - 11 中的Vector2d类只是为了教学 , 我们为它定义了许多与对象表示形式有关的特殊方法 .
不是每个用户定义的类都要这样做 .
11.10 节暂时不继续定义Vector2d类了 ,
我们将讨论Python对私有属性 ( 带两个下划线前缀的属性 , 例如self . __x ) 的设计方式及其缺点 .
11.10 Python私有属性和"受保护"的属性
Python不能像Java那样使用private修饰符创建私有属性 ,
但是它有一个简单的机制 , 能避免子类意外覆盖 '私有' 属性 .
举个例子 .
有人编写了一个名为Dog的类 , 内部用到了mood实例属性 , 但是没有将其开放 .
现在 , 你创建了Dog类的子类Beagle . 如果你在毫不知情的情况下又创建了名为mood的实例属性 ,
那么在继承的方法中就会把Dog类的mood属性覆盖 .
这是难以调试的问题 .
为了避免这种情况 , 如果以mood的形式 ( 两个前导下划线 , 尾部没有或最多有一个下划线 ) 命名实例属性 ,
那么Python就会把属性名存入实例属性_dict_中 , 而且会在前面加上一个下划线和类名 .
因此 , 对Dog类来说 , __mood会变成_Dog__mood ; 对Beagle类来说 , __mood会变成_Beagle__mood .
这个语言功能叫 '名称改写' ( name mangling ) .
示例 11 - 12 以示例 11 - 7 中定义的Vector2d类为例对名称改写进行了说明 .
>> > v1 = Vector2d( 3 , 4 )
>> > v1. __dict__
{ '_Vector2d__y' : 4.0 , '_Vector2d__x' : 3.0 }
>> > v1. _Vector2d__x
3.0
名称改写是一种安全措施 , 不能保证万无一失 : 它的目的是避免意外访问 , 不能防止故意做错事 .
图 11 - 1 也是一种保护装置 .
图 11 - 1 : 把手上的盖子是一种保护装置 , 而不是安全装置 :
它能避免意外触动把手 , 但是不能防止有意转动 .
如示例 11 - 12 的最后一行所示 , 只要知道改写私有属性名称的机制 ,
任何人都能直接读取私有属性--这实际上对调试和序列化很有用 .
此外 , 编写v1 . _Vector2d__x = 7 这样的代码也能直接为Vector2d实例的私有分量赋值 .
但是 , 如果真在生产环境中这么做了 , 那么出问题时可别抱怨 .
( 这样做存在以下两个问题 :
破坏了对象的封装性 :
私有成员的作用是被对象所封装 , 只能通过类的接口来访问和修改 .
如果直接通过这种方式来操作私有成员 , 就违反了面向对象编程的封装原则 .
不安全 :
私有成员的命名方式只是为了表明这是类内部使用的成员 , 实际上还是可以被外部访问 .
如果直接通过 '_Classname__private_var' 的方式来访问私有成员 ,
就容易在不经意间修改了类的内部状态 , 导致程序出现不可预料的行为 .
因此 , 在正式的生产环境中 , 我们应该遵守类的封装原则 , 通过类的接口来访问和修改成员 .
如果需要修改私有成员 , 可以通过公共的方法或属性来实现 .
同时 , 我们也应该编写完善的测试用例来测试类的功能和稳定性 , 以确保代码的正确性和安全性 . )
不是所有Python程序员都喜欢名称改写功能 , 也不是所有人都喜欢self . __×这种头重脚轻的名称 .
有些人不喜欢这种句法 , 他们约定使用一个下划线前缀编写 '受保护' 的属性 ( 例如self . _x ) .
批评使用两个下划线这种改写机制的人认为 , 应该使用命名约定来避免意外覆盖属性 .
pip , virtualenv等项目的开发者lan Bicking指出 :
千万千万不要使用两个前导下划线 , 这是很烦人的自私行为 .
如果担心名称冲突 , 则应该明确使用一种名称改写方式 ( 例如_MyThing_blahblah ) .
这其实与使用双下划线一样 , 不过自己定的规则比双下划线易于理解 . ⑦
( 注 7 : 摘自 'Paste StyleGuide' . )
Python解释器不会对使用单下划线的属性名做特殊处理 ,
不过这是很多Python程序员严格遵守的约定 , 他们不会在类的外部访问这种属性 .
⑧遵守使用一个下划线标记对象的私有属性很容易 , 就像遵守使用全大写字母编写常量一样 .
( 注 8 : 不过 , 在模块中 , 如果顶层名称使用一个前导下划线 , 那么的确会有影响 : 对 from mymod import * 说 ,
mymod中前缀为一个下划线的名称不会被导入 .
然而 , 依旧可以使用fron mymod inport _privatefunc将其导入 .
详见Python官方教程 6.1 节 'Moreon Modules' . )
Python文档的某些角落把使用一个下划线前缀标记的属性称为 '受保护' 的属性 .
⑨使用self . x这种形式的 '保护' 属性的做法很常见 , 但很少有人把这种属性叫作 '受保护' 的属性 .
有些人甚至将其称为 '私有' 属性 .
( 注 9 : gettext模块文档中就有一个例子 . )
总之 , Vector2d的分量都是 '私有' 的 , 而且Vector2d实例都是 '不可变' 的 .
我用了两对引号 , 因为并不能真正实现私有和不可变 . ⑩
( 注 10 : 如果这种说法令你感到沮丧 , 让你觉得在这方面Python应该向Java看齐 ,
那么就不要阅读本章的 '杂谈' 了 . 我在 '杂谈' 中探讨了Java private修饰符的相对优势 . )
下面继续定义Vector2d类 .
11.11 节将讨论一个特殊的属性 ( 不是方法 ) ,
它会影响对象的内部存储 , 对内存用量可能也有重大影响 , 但是对对象的公开接口没什么影响 .
这个属性是__slots__ .
11.11 使用__slots__
节省空间
默认情况下 , Python把各个实例的属性存储在一个名为__dict__的字典中 .
3.9 节讲过 , 字典消耗的内存很多--即使有一些优化措施 .
但是 , 如果定义一个名为__slots__的类属性 , 以序列的形式存储属性名称 ,
那么Python将使用其他模型存储实例属性 :
__slots__中的属性名称存储在一个隐藏的引用数组中 , 消耗的内存比字典少 .
下面通过几个简单的示例说明一下 , 先看示例 11 - 13.
>> > class Pixel :
. . . __slots__ = ( 'x' , 'y' )
. . .
>> > p = Pixel( )
>> > p. __dict__
Traceback ( most recent call last) :
. . .
AttributeError: 'Pixel' object has no attribute '__dict__'
>> > p. x = 10
>> > p. y = 20
>> > p. color = 'red'
Traceback ( most recent call last) :
AttributeError: 'Pixel' object has no attribute 'color'
目前还没有什么难以理解的 .
现在 , 在示例 11 - 14 中定义一个Pixel的子类 , 看看__slots__违反直觉的一面 .
>> > class OpenPixel ( Pixel) :
. . . pass
>> > op = OpenPixel( )
>> > op. __dict__
{ }
>> > op. x = 8
>> > op. __dict__
( )
>> > op. ×
8
>> > op. color = 'green
>> > op. __dict__
{ 'color' : 'green' }
示例 11 - 14 表明 , 子类只继承__slots__的部分效果 .
为了确保子类的实例也没有__dict__属性 , 必须在子类中再次声明__slots__属性 .
如果在子类中声明__slots__ = ( ) ( 一个空元组 ) , 则子类的实例将没有__dict__属性 ,
而且只接受基类的__slots__属性列出的属性名称 .
如果子类需要额外属性 , 则在子类的__slots__属性中列出来 , 如示例 11 - 15 所示 .
>> > class ColorPixel ( Pixel) :
. . . __slots__ = ( 'color' , )
. . .
>> > cp = ColorPixel( )
>> > cp. __dict__
Traceback ( most recent call last) :
. . .
AttributeError: 'ColorPixel' object has no attribute '__dict__"
>> > cp. x= 2
>> > cp. color = 'blue'
>> > cp. flavor = 'banana'
Traceback ( most recent call last) :
. . .
AttributeError: 'ColorPixel' object has no attribute 'flavor'
然而 , '节省的内存也可能被再次吃掉' : 如果把 '__dict__' 这个名称添加到__slots__列表中 ,
则实例会在各个实例独有的引用数组中存储__slots__中的名称 ,
不过也支持动态创建属性 , 存储在常规的__dict__中 .
如果想使用 @ cached_property装饰器 , 就要这么做 ( 详见 22.3 .5 节 ) .
当然 , 把 '__dict__' 添加到__slots__中可能完全违背了初衷 ,
这取决于各个实例的静态属性和动态属性的数量及其用法 .
粗心的优化甚至比提早优化还糟糕 , 往往得不偿失 .
此外 , 还有一个实例属性可能需要注意 , 即__weakref__ .
为了让对象支持弱引用 ( 6.6 节简单提过 ) , 必须有这个属性 .
用户定义的类默认就有__weakref__属性 .
然而 , 如果类中定义了__slots__ , 而且想把该类的实例作为弱引用的目标 ,
则必须把 '__weakref__' 添加到__slots__中 .
下面来看为Vector2d添加_slots_的效果 .
11.11.1 简单衡量__slot__
节省的内存
示例 11 - 16 在Vector2d中实现了__slots__ .
class Vector2d :
__match_args__ = ( 'x' , 'y' )
__slots__ = ( '__x' , '__y' )
typecode = 'd'
( __slot__为什么可以节省的内存 :
在Python中 , 每个对象都有一个字典用于存储它所有的属性和方法 , 这对于动态添加属性非常有用 .
但同时 , 这也意味着每个对象都需要额外的内存来存储这些属性 .
对于属性数量较少的对象来说 , 这些额外的内存开销并不明显 , 但对于属性数量众多的对象来说 ,
这一开销就会变得非常明显 , 甚至会影响整个程序的性能 .
而__slots__可以有效地解决这个问题 , 它是一个特殊的属性 , 用于限制一个类的实例可以有哪些属性 .
通过将 __slots__属性设置为一个列表或元组 , 我们可以明确地告诉Python , 在这个类的实例中 ,
只会存在__slots__列表中所列出的属性 , 这些属性名字会被编译成固定位置的偏移量 , 从而省去了实例字典的开销 .
这样一来 , 由于不需要为每个实例都分配一个字典 , 因此可以显著地减少内存占用 .
此外 , 由于属性名被编译成偏移量 , 因此访问属性的速度也可以更快 .
需要注意的是 . 使用__slots__会限制动态添加实例属性的能力 ,
原因是实例只能具有__slots__列表中所列出的属性 .
但是 , 这种开销的省略和提高的速度通常可以补偿这种限制 . )
为了衡量节省了多少内存 , 我编写了mem_test . py脚本 .
这个脚本在命令行中运行 , 参数是两版Vector2d类所在的模块名 .
我们使用列表推导式构建一个列表 , 存储 10000000 个Vector2d实例 .
第一次运行时 , 使用vector2d_v3 . Vector2d ( 来自示例 11 - 7 ) .
第二次运行时 , 使用示例 11 - 16 中带__slots__的版本 , 如示例 11 - 17 所示 .
import importlib
import sys
import resource
NUM_VECTORS = 10 ** 7
module = None
if len ( sys. argv) == 2 :
module_name = sys. argv[ 1 ] . replace( '.py' , '' )
module = importlib. import_module( module_name)
else :
print ( f'Usage: { sys. argv[ 0 ] } <vector-module-to-test>' )
if module is None :
print ( 'Running test with built-in `complex`' )
cls = complex
else :
fmt = 'Selected Vector2d type: {.__name__}.{.__name__}'
print ( fmt. format ( module, module. Vector2d) )
cls = module. Vector2d
mem_init = resource. getrusage( resource. RUSAGE_SELF) . ru_maxrss
print ( f'Creating { NUM_VECTORS: , } { cls. __qualname__!r } instances' )
vectors = [ cls( 3.0 , 4.0 ) for i in range ( NUM_VECTORS) ]
mem_final = resource. getrusage( resource. RUSAGE_SELF) . ru_maxrss
print ( f'Initial RAM usage: { mem_init: 14, } ' )
print ( f' Final RAM usage: { mem_final: 14, } ' )
$ time python3 mem_test. py vector2d_v3
Selected Vector2d type : vector2d_v3. Vector2d
Creating 10 , 000 , 000 Vector2d instances
Initial RAM usage: 6 , 983 , 680
Final RAM usage: 1 , 666 , 535 , 424
real 0m11. 990s
user 0m10. 861s
sys 0m0. 978s
$ time python3 mem_test. py vector2d_v3_slots
Selected Vector2d type : vector2d_v3_slots. Vector2d
Creating 10 , 000 , 000 Vector2d instances
Initial RAM usage: 6 , 995 , 968
Final RAM usage: 577 , 839 , 104
real 0m8. 381s
user 0m8. 006s
sys 0m0. 352s
如示例 11 - 17 所示 , 当 10 000 000 个Vector2d实例使用__dict__属性时 , RAM用量高达 1.55 GiB ,
而当Vector2d有__slots__属性之后 , RAM用量降到了 551 MiB .
而且 , 带__slots__属性的版本运行速度也更快 .
这个测试中使用的mem_test . py脚本其实只是加载一个模块 , 检查内存用量和格式化结果 ,
源码放在fuentpython / example-code- 2 e中 .
* --------------------------------------------------------------------------------------------- *
处理数百万个具有数值数据的对象其实应该使用NumPy数组 ( 参见 2.10 .3 节 ) .
NumPy数组不仅节约内存 , 还高度优化了数值处理函数 ( 很多函数一次性处理整个数组 ) .
我设计 Vector2d类的目的只是为了讨论特殊方法 , 因为不想随意举个含糊不清的示例 .
* --------------------------------------------------------------------------------------------- *
11.11.2 总结__slots__
的问题
如果使用得当 , 则类属性__slots__能显著节省内存 , 不过有几个问题需要注意 .
? 每个子类都要重新声明__slots__属性 , 以防止子类的实例有__dict__属性 .
? 实例只能拥有__slots__列出的属性 , 除非把 '__dict__' 加入__slots__中
( 但是这样做就失去了节省内存的功效 ) .
? 有__slots__的类不能使用 @ cached_property装饰器 , 除非把 '__dict__' 加入__slots__中 .
? 如果不把 '__weakref__' 加入__slots__中 , 那么实例就不能作为弱引用的目标 .
class B :
__slots__ = ( '__dict__' , )
b = B( )
b. a = 1
print ( b. a)
import weakref
class A :
def __init__ ( self, x) :
self. x = x
a = A( 1 )
obj_a = weakref. ref( a)
class B :
__slots__ = ( 'x' , '__weakref__' )
b = B( )
obj_b = weakref. ref( b)
本章最后一个话题讨论如何在实例和子类中覆盖类属性 .
11.12 覆盖类属性
Python有一个很独特的功能 : 类属性可为实例属性提供默认值 .
Vector2d中有一个名为typecode的类属性 .
__bytes__方法两次用到了这个属性 , 而且都故意使用self . typecode读取它的值 .
因为Vector2d实例本身没有typecode属性 , 所以self . typecode默认获取的是Vector2d . typecode类属性的值 .
但是 , 如果为不存在的实例属性赋值 , 那么将创建一个新实例属性 .
假如为typecode实例属性赋值 , 那么同名类属性将不受影响 .
然而 , 一旦这样做 , 实例读取的self . typecode是实例属性typecode , 也就是把同名类属性遮盖了 .
借助这个功能 , 可以为各个实例的typecode属性定制不同的值 .
Vector2d . typecode属性的默认值是 'd' , 即转换成字节序列时使用 8 字节双精度浮点数表示向量的各个分量 .
如果在转换之前把Vector2d实例的typecode 属性设为 'f' , 那么将使用 4 字节单精度浮点数表示各个分量 ,
如示例 11 - 18 所示 .
>> > from vector2d_v3 import Vector2d
>> > 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\xOc@'
>> > len ( dumpf)
9
>> > Vector2d. typecode
'd'
* * ------------------------------------------------------------------------------------------- * *
这里在讨论如何添加自定义的实例属性 ,
因此示例 11 - 18 使用的是示例 11 - 11 中不带__slots__属性的Vector2d类 .
* * ------------------------------------------------------------------------------------------- * *
现在你应该知道为什么要在得到的字节序列前面加上typecode的值了 : 为了支持不同的格式 .
如果想修改类属性的值 , 那么必须直接在类上修改 , 不能通过实例修改 .
如果想修改所有实例 ( 自身没有typecode 属性 ) 的typecode属性的默认值 , 则可以像下面这样做 .
>> > Vector2d. typecode = 'f'
>> > from vector2d_v3 import Vector2d
>> > class ShortVector2d ( Vector2d) :
. . . typecode = 'f'
. . .
>> > sv = Shortvector2d( 1 / 11 , 1 / 27 )
>> > sv
ShortVector2d( 0.09090909090909091 , 0.037037037037037035 )
>> > len ( bytes ( sv) )
9
这也说明了在Vecto2d . __repr__方法中为什么没有硬编码class_name的值 ,
而是使用type ( self ) . __name__获取 , 如下所示 .
def __repr__ ( self) :
class_name = type ( self) . __name__
return '{}({!r}, {!r})' . format ( class_name, * self)
如果硬编码class_name的值 , 那么仅为了修改class_name的值 ,
Vector2d的子类 ( 例如Shortvector2d ) 就要覆盖__repr_方法 .
从实例的类型中读取类名 , __repr__方法可以放心继承 .
'硬编码' : 是一种编码方式 , 它将常量和其他固定的值直接写入程序中 ,
不使用变量或配置文件等外部数据源来存储这些值 .
class Vector2d :
def __init__ ( self, x, y) :
self. x = x
self. y = y
def __repr__ ( self) :
class_name = type ( self) . __name__
return f' { class_name} ( { self. x} , { self. y} )'
class ShortVector2d ( Vector2d) :
def __init__ ( self, x: int , y: int ) :
super ( ) . __init__( x= x, y= y)
sv = ShortVector2d( 1 , 2 )
print ( f' { sv!r } ' )
由于ShortVector2d是Vector2d的子类 , 它会自动继承__repr__方法 ,
这样它就会正确地返回类名和向量的坐标 .
这也使得我们可以轻松地扩展Vector2d类 , 并创建其他类似的子类 , 而不需要重复相同的代码 .
至此 , 本章通过一个简单的类说明了如何利用数据模型处理Python的其他功能 ,
包括提供不同的对象表示形式 , 实现自定义的格式代码 , 公开只读属性 , 以及通过hash ( ) 函数支持集合和映射 .
11.13 本章小结
本章的目的是说明如何使用特殊方法和约定的结构来定义行为良好且符合Python风格的类 .
vector2d_v3 . py ( 参见示例 11 - 11 ) 比 vector2d_v0 . py ( 参见示例 11 - 2 ) 更符合Python风格吗?
vector2d_v3 . py中的Vector2d类用到的Python功能肯定更多 ,
但是Vector2d类的第 1 版和最后一版相比哪个更符合Python风格要看其使用上下文 .
Tim Peter 写的 < < Python之禅 > > 说道 :
简洁胜于复杂 .
符合Python风格的对象应该正好符合所需 , 而不是堆砌语言功能 .
开发应用程序时 , 应该集中精力满足终端用户的需求 , 仅此而已 .
编写供其他程序员使用的库时 , 应该实现一些特殊方法 , 提供Python程序员预期的行为 .
例如 , __eq__方法对业务需求可能没有必要 , 但是方便程序员测试 .
本章不断改写Vector2d类是为了提供上下文 , 以便讨论Python的特殊方法和编程约定 .
回看表 1 - 1 , 你会发现本章的几个代码清单说明了以下特殊方法 .
? 字符串和字节序列表示形式的方法 : __repr__ , __str__ , __format__和__bytes__ .
? 把对象转换成数值的方法 : __abs__ , __bool__和__hash__ .
? 支持测试和哈希的__eq__方法 ( 外加__hash__ ) .
为了转换成字节序列 , 本章还实现了一个名为Vector2d . frombytes ( ) 的备选构造函数 ,
顺便又讨论了 @ classmethod ( 十分有用 ) 和 @ staticmethod ( 不太有用 , 使用模块层函数更简单 ) 两个装饰器 .
frombytes方法的实现方式借鉴了array . array类中的同名方法 .
我们了解到 , 格式规范微语言可通过__format__方法扩展 .
在__format__方法中 , 我们要做的是解析format_spec .
这就是传给内置函数format ( obj , format_spec ) 的参数 , f字符串的代换字段 '[: <<format_specn>>]' ,
以及str . format ( ) 方法处理的字符串 .
为了把vector2d实例变成可哈希的 ,
先让实例不可变 , 至少要把×和y设为私有属性 , 再以只读特性公开 , 以防意外修改 .
随后 , 实现__hash__方法 , 使用推荐的异或运算符计算实例属性的哈希值 .
接着 , 本章讨论了如何使用_slots__属性节省内存 , 以及这么做要注意的问题 .
__slots__属性有点儿棘手 , 因此仅当处理特别多的实例 ( 数百万个 , 而不是几千个 ) 时才建议使用 .
如果真有这么多的数量 , 那么使用pandas或许是最好的选择 .
最后 , 本章说明了如何通过访问实例属性 ( 例如self . typecode ) 覆盖类属性 .
我们先创建一个实例属性 , 然后创建子类 , 在类中覆盖类属性 .
前面多次提到 , 本章的示例受Python API的影响很大 , 这是我长期研究Python标准对象的结果 .
如果用一句话总结本章的内容 , 那就是 :
要构建符合Python风格的对象 , 就要观察真正的Python对象的行为 .
--源自古老的中国谚语
11.14 延伸阅读
本章介绍了数据模型的几个特殊方法 , 因此主要参考资料与第 1 章一样 , 阅读那些资料能对这个话题有整体了解 .
为方便起见 , 这里再次给出之前推荐的 4 份资料 , 同时再多加几份 .
< < Python 语言参考手册 > > 中的第 3 章 '数据模型'
本章用到的方法大部分见于 3.3 .1 节 '基本定制' .
Python in a Nutshell , 3 rd ed . ( Alex Martelli , Anna Ravenscroft 和 Steve Holden 著 )
深入讲解特殊方法 .
< < Python Cookbook ( 第 3 版 ) 中文版 > >
通过经典实例演示现代Python编程实践 . 尤其是第 8 章 '类与对象' , 其中有好几种方案与本章讨论的话题有关 .
< < Python参考手册 ( 第 4 版 ) > >
详细说明了数据模型 , 即使 ( 第 4 版 ) 只涵盖了Python 2.6 和Python 3.
基础概念都是一样的 , 自Python 2.2 统一内置类型和用户定义的类以来 , 数据模型API没有任何变化 .
2015 年 , 也就是写完本书第 1 版那一年 , Hynek Schlawack 启动了attrs包的开发 .
attrs文档中有这么一句话 :
attrs包把你从实现对象协议 ( 那些双下划线方法 ) 的苦差事中解放出来 , 让你重拾编写类的乐趣 .
5.10 节提到过attrs包 , 它功能强大 , 是 @ dataclass之外的另一种选择 .
第 5 章中所述的数据类构建器和attrs包能自动为类配备几个特殊方法 .
但是 , 知道如何自己编写特殊方法才能理解那些包的作用 ,
才能判断是否真正需要使用包 , 才能在必要时覆盖包生成的方法 .
本章涵盖了与对象表示形式有关的全部特殊方法 , 唯有__index__和__fspath__没有讲到 .
__index__将在 12.5 .2 节讨论 .
本书不讲_fspath_ , 如果你想学习 , 请阅读 'PEP 519 Adding a file system path protocol' .
意识到应该区分字符串表示形式的早期语言是Smalltalk .
1996 年 , Bobby Woolf写了一篇题为 ' How to Display an Object as a String :
printString and displayString”的文章 ,
讨论了Smalltalk xprintString方法和displaystring方法的实现 .
11.2 节在说明repr ( ) 和str ( ) 的作用时 , 从这篇文章中借用了言简意赅的表述 ,
即 '便于开发者理解的方式' 和 '便于用户理解的方式' .
* --------------------------------------------杂谈---------------------------------------------- *
'特性有助于减少前期投入' :
在Vector2d类的第 1 版中 , x属性和y属性是公开的 .
默认情况下 , Python的所有实例属性和类属性都是公开的 .
这对向量来说是合理的 , 因为需要访问分量 .
虽然这些向量是可迭代的对象 , 而且可以拆包成一对变量 ,
但我们还是希望能够通过my_vector . x和my_vector . y获取各个分量 .
如果觉得应该避免意外更新×属性和y属性 , 则可以实现特性 ( 私有化属性 ) ,
但是代码的其他部分没有变化 , Vector2d的公开接口也不受影响 , 这一点从doctest中可以得知 .
我们依然能够访问my_vector . ×和my_vector . y .
这表明可以先以最简单的方式定义类 , 也就是使用公开属性 ,
因为如果以后需要对读值方法和设值方法增加控制 , 则可以通过特性实现 ,
这样做对一开始通过公开属性的名称 ( 例如×和y ) 与对象交互的代码没有影响 .
Java语言采用的方式则截然相反 :
Java程序员不能先定义简单的公开属性 , 等需要时再实现特性 , 因为Java 语言没有特性 .
因此 , 在Java中编写读值方法和设值方法是常态 , 就算这些方法没什么实际作用 .
这是因为API不能从简单的公开属性变成读值方法和设值方法 , 同时又不影响使用那些属性的代码 .
此外 , Martelli , Ravenscroft和Holden在Python in a Nutshell , 3 rd ed . 一书中指出 ,
到处使用读值方法和设值方法是愚蠢的行为 .
如果想编写如下代码 :
> > > my_object . set_foo ( my_object . get_foo ( ) + 1 )
则这样做就行了 .
> > > my_object . foo + = 1
维基的发明人和极限编程先驱WardCunningham建议问以下问题 :
'做这件事最简单的方法是什么?' 意即 , 我们应该把焦点放在目标上 .
?提前实现设值方法和读值方法偏离了目标 . 在Python中 , 可以先使用公开属性 , 然后等需要时再变成特性 .
( 注 11 : 参见 ' Simplest Thing that Could Possibly Work :
A Conversation with Ward Cunningham , Part V ' . )
'私有属性的安全性和保障性' :
Perl不会强制你保护隐私 . 你应该待在客厅外 , 因为你没有收到邀请 , 而不 是因为里面有把枪 .
--Larry Wall
Perl之父
Python和Perl在很多方面的做法截然相反 , 但是Guido和Larry似乎都同意要保护对象的隐私 .
这些年我教过许多Java程序员学习Python , 我发现很多人对Java提供的隐私保障推崇备至 .
可事实是 , Java的private修饰符和protected修饰符往往只是为了防止意外发生 ( 一种安全措施 ) .
只有使用SecurityManager部署Java应用程序时才能保障绝对安全 , 防止恶意访问 .
但是 , 实际上很少有人这么做 , 即便在企业中也少见 .
下面通过一个Java类来证明这一点 , 如示例 11 - 20 所示 .
public class Confidential {
private String secret = "" ;
public Confidential ( String text) {
this . secret = text. toUpperCase ( ) ;
}
}
示例 11 - 20 把text转换成大写后存入了secret字段 .
转换成大写只是为了表明secret字段中的值全部是大写形式 .
要使用Jython运行expose . py脚本才能真正说明问题 .
该脚本使用内省 ( Java称之为 '反射' ) 获取私有字段的值 .
示例 11 - 21 中是expose . py脚本的代码 .
import Confidential
message = Confidential( 'top secret text' )
secret_field = Confidential. getDeclaredField( 'secret' )
secret_field. setAccessible( True )
print 'message.secret =' , secret_field. get( message)
运行示例 11 - 21 , 得到的结果如下所示 .
$ jython expose. py
message. secret = TOP SECRET TEXT
我们从Confidential类的私有字段secret中读取到了字符串 'TOP SECRET TEXT' .
这里没有什么黑魔法 : expose . py脚本使用Java反射API获取私有字段 'secret' 的引用 ,
然后调用 'secret_field.setAccessible(True)' 把它设为可读的 .
显然 , 使用Java代码也能做到这一点 ( 不过所需的代码行数是这里的 3 倍多 , 参见本书代码中的Expose . java文件 ) .
如果这个Jython脚本或Java主程序 ( 例如 Expose . class ) 在SecurityManager的监管下运行 ,
那么关键调用 . setAccessible ( True ) 就会失败 .
但是在现实中 , 很少有人使用SecurityManager部署Java应用程序 , 浏览器以前支持的Java applet除外 .
我的观点是 , Java中的访问控制修饰符基本上也是安全措施 , 不能保证万无一失 , 至少在实践中是这样 .
因此 , 安心享受Python提供的强大功能 , 放心去用吧 .
* --------------------------------------------------------------------------------------------- *