流畅的python 符合python风格的对象

Posted a3384451

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了流畅的python 符合python风格的对象相关的知识,希望对你有一定的参考价值。

对象表示形式

每门面向对象的语言至少都有一种获取对象的字符串表示形式的标准方式。Python 提供了两种方式。

repr()
  以便于开发者理解的方式返回对象的字符串表示形式。
str()
  以便于用户理解的方式返回对象的字符串表示形式。正如你所知,我们要实现 __repr__ 和 __str__ 特殊方法,为 repr()和 str() 提供支持。

为了给对象提供其他的表示形式,还会用到另外两个特殊方法:__bytes__ 和 __format__。__bytes__ 方法与 __str__ 方法类似:bytes() 函数调用它获取对象的字节序列表示形式。而
__format__ 方法会被内置的 format() 函数和 str.format() 方法调用,使用特殊的格式代码显示对象的字符串表示形式。我们将在下一个示例中讨论 __bytes__ 方法,随后再讨论 __format__ 方法。

在 Python 3中,__repr__、__str__ 和 __format__ 都必须返回 Unicode 字符串(str 类型)。只有 __bytes__ 方法应该返回字节序列(bytes 类型)。

再谈向量类

示例 9-1 Vector2d 实例有多种表示形式

>>> 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
bd\x00\x00\x00\x00\x00\x00\[email protected]\x00\x00\x00\x00\x00\x00\[email protected]
>>> abs(v1) ?
5.0
>>> bool(v1), bool(Vector2d(0, 0)) ?

? Vector2d 实例的分量可以直接通过属性访问(无需调用读值方法)。
? Vector2d 实例可以拆包成变量元组。
? repr 函数调用 Vector2d 实例,得到的结果类似于构建实例的源码。
? 这里使用 eval 函数,表明 repr 函数调用 Vector2d 实例得到的是对构造方法的准确表述。
? Vector2d 实例支持使用 == 比较;这样便于测试。
? print 函数会调用 str 函数,对 Vector2d 来说,输出的是一个有序对。
? bytes 函数会调用 __bytes__ 方法,生成实例的二进制表示形式。
? abs 函数会调用 __abs__ 方法,返回 Vector2d 实例的模。
? bool 函数会调用 __bool__ 方法,如果 Vector2d 实例的模为零,返回 False,否则返回 True。

示例 9-2 vector2d_v0.py:目前定义的都是特殊方法

from array import array
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 __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)) ?

? typecode 是类属性,在 Vector2d 实例和字节序列之间转换时使用。
? 在 __init__ 方法中把 x 和 y 转换成浮点数,尽早捕获错误,以防调用 Vector2d 函数时传入不当参数。
? 定义 __iter__ 方法,把 Vector2d 实例变成可迭代的对象,这样才能拆包(例如,x, y = my_vector)。这个方法的实现方式很简单,直接调用生成器表达式一个接一个产出分量。
这一行也可以写成 yield self.x; yield.self.y。
? __repr__ 方法使用 {!r} 获取各个分量的表示形式,然后插值,构成一个字符串;因为 Vector2d 实例是可迭代的对象,所以 *self 会把x 和 y 分量提供给 format 函数。
? 从可迭代的 Vector2d 实例中可以轻松地得到一个元组,显示为一个有序对。
? 为了生成字节序列,我们把 typecode 转换成字节序列,然后……
? ……迭代 Vector2d 实例,得到一个数组,再把数组转换成字节序列。
? 为了快速比较所有分量,在操作数中构建元组。对 Vector2d 实例来说,可以这样做,不过仍有问题。参见下面的警告。
? 模是 x 和 y 分量构成的直角三角形的斜边长。
? __bool__ 方法使用 abs(self) 计算模,然后把结果转换成布尔值,因此,0.0 是 False,非零值是 True。

classmethod与staticmethod

先来看 classmethod。示例 9-3 展示了它的用法:定义操作类,而不是操作实例的方法。classmethod 改变了调用方法的方式,因此类方法的第一个参数是类本身,而不是实例。classmethod 最常见的用途是
定义备选构造方法,例如示例 9-3 中的 frombytes。注意,frombytes的最后一行使用 cls 参数构建了一个新实例,即 cls(*memv)。按照约定,类方法的第一个参数名为 cls(但是 Python 不介意具体怎么命
名)。

staticmethod 装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实,静态方法就是普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。示例 9-4 对 classmethod 和
staticmethod 的行为做了对比。示例 9-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,)

? klassmeth 返回全部位置参数。
? statmeth 也是。
? 不管怎样调用 Demo.klassmeth,它的第一个参数始终是 Demo 类。
? Demo.statmeth 的行为与普通的函数相似。

格式化显示

内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的 .__format__(format_spec) 方法。format_spec 是格式说明符,它是:
format(my_obj, format_spec) 的第二个参数,或者
str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分
例如:

>>> brl = 1/2.43 # BRL到USD的货币兑换比价
>>> brl
0.4115226337448559
>>> format(brl, 0.4f) # ?
0.4115
>>> 1 BRL = {rate:0.2f} USD.format(rate=brl) # ?
1 BRL = 0.41 USD

? 格式说明符是 ‘0.4f‘。
? 格式说明符是 ‘0.2f‘。代换字段中的 ‘rate‘ 子串是字段名称,与格式说明符无关,但是它决定把 .format() 的哪个参数传给代换字段。

 

格式规范微语言为一些内置类型提供了专用的表示代码。比如,b 和 x分别表示二进制和十六进制的 int 类型,f 表示小数形式的 float 类型,而 % 表示百分数形式:

>>> format(42, b)
101010
>>> format(2/3, .1%)
66.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)

示例 9-5 Vector2d.__format__ 方法,第 1 版

# 在Vector2d类中定义
def __format__(self, fmt_spec=‘‘):
    components = (format(c, fmt_spec) for c in self) # ?
    return ({}, {}).format(*components) # ?

? 使用内置的 format 函数把 fmt_spec 应用到向量的各个分量上,构建一个可迭代的格式化字符串。
? 把格式化字符串代入公式 ‘(x, y)‘ 中。

可散列的Vector2d

按照定义,目前 Vector2d 实例是不可散列的,因此不能放入集合(set)中:

>>> 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 章的附注栏“什么是可散列的数据类型”。
目前,我们可以为分量赋新值,如 v1.x = 7,Vector2d 类的代码并不阻止这么做。我们想要的行为是这样的:

>>> v1.x, v1.y
(3.0, 4.0)
>>> v1.x = 7
Traceback (most recent call last):
...
AttributeError: cant set attribute

为此,我们要把 x 和 y 分量设为只读特性,如示例 9-7 所示。示例 9-7 vector2d_v3.py:这里只给出了让 Vector2d 不可变的代
码,完整的代码清单在示

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)) ?

? 使用两个前导下划线(尾部没有下划线,或者有一个下划线),把
属性标记为私有的。
? @property 装饰器把读值方法标记为特性。
? 读值方法与公开属性同名,都是 x。
? 直接返回 self.__x。
? 以同样的方式处理 y 特性。
? 需要读取 x 和 y 分量的方法可以保持不变,通过 self.x 和 self.y
读取公开特性,而不必读取私有属性,因此上述代码清单省略了这个类
的其他代码。

注意,我们让这些向量不可变是有原因的,因为这样才能实现__hash__ 方法。这个方法应该返回一个整数,理想情况下还要考虑对象属性的散列值(__eq__ 方法也要使用),因为相等的对象应该具有
相同的散列值。

示例 9-8 vector2d_v3.py:实现 __hash__ 方法

# 在Vector2d类中定义
def __hash__(self):
  return hash(self.x) ^ hash(self.y)

要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需正确地实现 __hash__ 和 __eq__ 方法即可。但是,实例的散列值绝不应该变化,因此我们借机提到了只读特性。

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

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


举个例子。有人编写了一个名为 Dog 的类,这个类的内部用到了 mood实例属性,但是没有将其开放。现在,你创建了 Dog 类的子类:Beagle。如果你在毫不知情的情况下又创建了名为 mood 的实例属
性,那么在继承的方法中就会把 Dog 类的 mood 属性覆盖掉。这是个难以调试的问题。

为了避免这种情况,如果以 __mood 的形式(两个前导下划线,尾部没有或最多有一个下划线)命名实例属性,Python 会把属性名存入实例的__dict__ 属性中,而且会在前面加上一个下划线和类名。因此,对Dog 类来说,__mood 会变成 _Dog__mood;对 Beagle 类来说,会变成_Beagle__mood。这个语言特性叫名称改写(name mangling)。


示例 9-10 以示例 9-7 中定义的 Vector2d 类为例来说明名称改写。
示例 9-10 私有属性的名称会被“改写”,在前面加上下划线和类名

>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{_Vector2d__y: 4.0, _Vector2d__x: 3.0}
>>> v1._Vector2d__x
3.0

名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意做错事

如示例 9-10 中的最后一行所示,只要知道改写私有属性名的机制,任何人都能直接读取私有属性——这对调试和序列化倒是有用。此外,只要编写 v1._Vector__x = 7 这样的代码,就能轻松地为 Vector2d 实
例的私有分量直接赋值。如果真在生产环境中这么做了,出问题时可别抱怨。

不是所有 Python 程序员都喜欢名称改写功能,也不是所有人都喜欢self.__x 这种不对称的名称。有些人不喜欢这种句法,他们约定使用一个下划线前缀编写“受保护”的属性(如 self._x)。批评使用两个下
划线这种改写机制的人认为,应该使用命名约定来避免意外覆盖属性。本章开头引用了多产的 Ian Bicking 的一句话,那句话的完整表述如下:绝对不要使用两个前导下划线,这是很烦人的自私行为。如果担心
名称冲突,应该明确使用一种名称改写方式(如_MyThing_blahblah)。这其实与使用双下划线一样,不过自己定的规则比双下划线易于理解。



Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员严格遵守的约定,他们不会在类外部访问这种属性。


遵守使用一个下划线标记对象的私有属性很容易,就像遵守使用全大写字母编写常量那样容易。

使用 __slots__ 类属性节省空间

默认情况下,Python 在各个实例中名为 __dict__ 的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过 __slots__
类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。



定义 __slots__ 的方式是,创建一个类属性,使用 __slots__ 这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。我喜欢使用元组,因为这样定义的 __slots__ 中所含
的信息不会变化,如示例 9-11 所示。


示例 9-11 vector2d_v3_slots.py:只在 Vector2d 类中添加了__slots__ 属性

class Vector2d:
  __slots__ = (__x, __y)
  typecode = d
  # 下面是各个方法(因排版需要而省略了)

在类中定义 __slots__ 属性的目的是告诉解释器:“这个类中的所有实例属性都在这儿了!”这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的 __dict__ 属性。如果有数
百万个实例同时活动,这样做能节省大量内存。

 

在类中定义 __slots__ 属性之后,实例不能再有__slots__ 中所列名称之外的其他属性。这只是一个副作用,不是__slots__ 存在的真正原因。不要使用 __slots__ 属性禁止类的用户新增实例属性。__slots__ 是用于优化的,不是为了约束程序员。




然而,“节省的内存也可能被再次吃掉”:如果把 ‘__dict__‘ 这个名称添加到 __slots__ 中,实例会在元组中保存各个实例的属性,此外还支持动态创建属性,这些属性存储在常规的 __dict__ 中。当然,把
‘__dict__‘ 添加到 __slots__ 中可能完全违背了初衷,这取决于各个实例的静态属性和动态属性的数量及其用法。粗心的优化甚至比提早优化还糟糕。



 

__slots__ 的问题

总之,如果使用得当,__slots__ 能显著节省内存,不过有几点要注意。

每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的__slots__ 属性。

实例只能拥有 __slots__ 中列出的属性,除非把 ‘__dict__‘ 加入 __slots__ 中(这样做就失去了节省内存的功效)。如果不把 ‘__weakref__‘ 加入 __slots__,实例就不能作为弱引用的目标。

如果你的程序不用处理数百万个实例,或许不值得费劲去创建不寻常的类,那就禁止它创建动态属性或者不支持弱引用。与其他优化措施一样,仅当权衡当下的需求并仔细搜集资料后证明确实有必要时,才应该使用 __slots__ 属性。



 

以上是关于流畅的python 符合python风格的对象的主要内容,如果未能解决你的问题,请参考以下文章

python 符合Python风格的对象

《流畅的Python》高清PDF电子版附源代码下载

『流畅的Python』第9章_对象

如何写出符合 Python 审美的代码风格?

如何使用 Pylint 来规范 Python 代码风格

流畅的python