《流畅的Python》读书笔记——符合Python风格的对象

对象表示形式

所谓对象表示形式,就是将对象转换为一种可读的形式。

Python提供了两种方式:

  • repr():以便于开发者理解的方式返回对象的字符串表示形式
  • str():以便于用户理解的方式返回对象的字符串表示形式

我们只要实现 __repr____str__ 特殊方法,就可以为repr()str() 提供支持。

为了给对象提供其他的表示形式,还会用到另外两个特殊方
法:__bytes____format____bytes__ 方法与 __str__
法类似:bytes()函数调用它获取对象的字节序列表示形式。而
__format__ 方法会被内置的format()函数和 str.format()
法调用。

向量类

我们使用Vector2d类来说明对象表示形式的众多用法。

#vector2d_v0.py
from array import array
import math

class Vector2d:
    typecode = 'd' #类属性,在实例和字节序之间转换时使用

    def __init__(self,x,y):
        self.x = float(x) #把x和y转换成浮点数,尽早捕获错误,防止传入不当参数
        self.y = float(y)

    def __iter__(self):
        return (i for i in (self.x, self.y)) #把Vector2d实例变成可迭代的对象,这样才能拆包

    def __repr__(self):
        class_name = type(self).__name__
        return '{}({!r}, {!r})'.format(class_name, *self) #因为 Vector2d 实例是可迭代的对象,所以 *self 会
                                                          #把 x 和 y 分量提供给 format 函数。

    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return (bytes([ord(self.typecode)]) + #把 typecode 转换成字节序列
        bytes(array(self.typecode, self))) #迭代 Vector2d 实例,得到一个数组,再把数组转换成字节序列。

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    def __abs__(self):
        return math.hypot(self.x,self.y)  #模是 x 和 y 分量构成的直角三角形的斜边长

    def __bool__(self):
        return bool(abs(self))

在控制台调用如下:

>>> from vector2d_v0 import Vector2d
>>> v1 = Vector2d(3,4)
>>> print(v1.x,v1.y)
3.0 4.0
>>> x,y = v1 #Vecctor2d实例可以拆包成变量元组
>>> x,y
(3.0, 4.0)
>>> v1
Vector2d(3.0, 4.0)
>>> repr(v1)
'Vector2d(3.0, 4.0)'
>>> str(v1)
'(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)

备选构造方法

我们可以把 Vector2d实例转换成字节序列了;同理,也应该能从字
节序列转换成 Vector2d 实例。

主要在上面的类中增加如下类方法:

@classmethod #类方法使用classmethod装饰器修饰
def frombytes(cls,octets): #通过cls传入类本身
    typecode = chr(octets[0]) #从第一个字节中读取typecode
    memv = memoryview(octets[1:]).cast(typecode) #通过传入的字节序列创建一个memoryview,然后使用typecode转换
    return cls(*memv) #拆包转换后的memoryview,得到构造方法所需的一对参数

classmethod与staticmethod

classmethod定义操作类的方法,该方法的第一个参数是类本身。其最常见的用途是备选构造方法。

staticmethod装饰器也会改变方法的调用方式,但是第一个参数不是特殊的值。其实,静态方法就是普通的函数。

In [1]: class Demo: 
   ...:     @classmethod 
   ...:     def klassmeth(*args): 
   ...:         return args 
   ...:     @staticmethod 
   ...:     def statmeth(*args): 
   ...:         return args 
   ...:                                                                         

In [2]: Demo.klassmeth()#不管怎么调用该方法,第一个参数始终是Demo                                                    
Out[2]: (__main__.Demo,)
In [3]: Demo.klassmeth('spam')                                                  
Out[3]: (__main__.Demo, 'spam')
In [4]: Demo.statmeth('spam') #它的行为与普通函数类似                                               
Out[4]: ('spam',)

格式化显示

内置的 format() 函数和str.format() 方法把各个类型的格式化方
式委托给相应的 .__format__(format_spec)
法。format_spec是格式说明符,它是:

  • format(my_obj, format_spec)的第二个参数,或者
  • str.format()方法的格式字符串,{}里代换字段中冒号后面的部分
In [5]: brl = 1/2.43                                                            
In [6]: brl                                                                     
Out[6]: 0.4115226337448559
In [7]: format(brl,'0.4f')                                                      
Out[7]: '0.4115'
In [8]: '1 BRL = {rate:0.2f} USD'.format(rate=brl)  #rate被brl替换                            
Out[8]: '1 BRL = 0.41 USD'

{rate:0.2f}'这样的格式字符串其实包含两部分,
冒号左边的 rate 在代换字段句法中是字段名,冒号后面的 0.2f 是格式说明符。格式说明符使用的表示法叫格式规范微语言。

格式规范微语言是可扩展的,因为各个类可以自行决定如何解释
format_spec 参数。例如,datetime 模块中的类,它们的
__format__方法使用的格式代码与strftime() 函数一样。下面是
内置的format() 函数和str.format() 方法的几个示例:

>>> from datetime import datetime
>>> now = datetime.now()
>>> format(now,'%H:%M:%S')
'16:20:38'
>>> "It's now {:%I:%M %p}".format(now)
"It's now 04:20 PM"

如果类没有定义 __format__ 方法,从 object 继承的方法会返回
str(my_object)。我们为Vector2d类定义了 __str__ 方法,因
此可以这样做:

>>> v1 = Vector2d(3,4)
>>> format(v1)
'(3.0, 4.0)'

接下来将实现自己的微语言。假设用户提供的格式说明符是用于格式化向量中各个浮点数分量的。我们想达到这样的效果:

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

增加的方法如下:

def __format__(self, format_spec = ''):
    components = (format(c,format_spec) for c in self) #使用内置的 format 函数把 fmt_spec 应用到向量的各个分量上
    return '({}, {})'.format(*components)

可散列的Vector2d

为了让Vector2d实例变成可散列的,必须实现__hash__方法(还有__eq__方法),还需让向量不可变。

#修改了构造方法
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 __hash__(self):
    return hash(self.x) ^ hash(self.y)

下面对改造后的向量进行测试:

>>> from vector2d_v2 import Vector2d
>>> v1 = Vector2d(3,4)
>>> v2 = Vector2d(3.1,4.2)
>>> hash(v1),hash(v2)
(7, 384307168202284039)
>>> set([v1,v2])
{Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}

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

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

我们上面在属性x名称前加了两个下划线变成了__x,对于该类来说,__x会变成_Vector2d__x,这个语言特性叫名称改写。Python 会把这种属性名存入实例的__dict__属性中,而且会在前面加上一个下划线和类名:

>>> v1.__dict__
{'_Vector2d__x': 3.0, '_Vector2d__y': 4.0}
>>> v1._Vector2d__x #如果知道了如何改写名称,还是可以访问到
3.0

但是有些人不喜欢这种句法,他们约定使用
一个下划线前缀编写“受保护”的属性(如self._x

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

使用__slots__类属性节省空间

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

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

class Vector2d:
    __slots__ = ('__x','__y')

在类中定义 __slots__ 属性的目的是告诉解释器:“这个类中的所有
实例属性都在这儿了!”

覆盖类属性

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 字节单精度
浮点数表示各个分量:

>>> 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' #修改v1实例的typecode属性
>>> dumpf = bytes(v1)
>>> dumpf
b'f\xcd\xcc\x8c?\xcd\xcc\x0c@'
>>> len(dumpf)
9
>>> Vector2d.typecode # 而类属性不变
'd'
发布了131 篇原创文章 · 获赞 38 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/yjw123456/article/details/99410595