进阶:编写符合Python风格的对象

得益于Python数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子模型(duck typing)。

对象表示形式

获取对象的字符串表示形式的标准方式,Python提供了两种。

  • repr() :返回便于开发者理解的方式返回对象的字符串,用__repr__实现 Python控制台变量打印就是调用的__repr__
  • str():返回便于用户理解的方式返回对象的字符串,用__str__实现

其他的表示形式,还有:

  • __bytes__:类似于__str__方法,bytes()函数调用它获取对象的字节序列表示形式。
  • __format__:会被内置的format()函数和str.format()方法调用,适应特殊的格式代码显示对象字符串表示形式。

ps:在Python3中,__repr__/__str__和__format__都必须返回Unicode字符串(str类型)。只有__bytes__方法返回字节序列(bytes类型)

实现Pythonic的向量类

使用Python的特殊方法,实现一个Pythonic的向量类

import math
from array import array


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))  # 使用迭代器(iterator)表达式

    def __repr__(self):
        class_name = type(self).__name__  # 自省类名
        return '{}({!r},{!r})'.format(class_name, *self)  # *self拆包,用到了自己的__iter__方法。  !r 和%r一样 返回对象本体

    def __str__(self):
        # return '(%r,%r)' % (self.x, self.y)
        return str(tuple(self))  # 从可迭代对象可以轻松得到一个元组,然后转成字符串。tuple( iterable ),所以会调用__iter__

    def __eq__(self, other):
        return tuple(self) == tuple(other)  # 构建成元组,可以快速比较所有分量。

    def __bytes__(self):
        """向量类转换为bytes"""
        return bytes([ord(self.type_code)]) + bytes(array(self.type_code, self))  # b'd' + b'\x00...'

    def __abs__(self):
        return math.hypot(self.x, self.y)  # hypot()函数就是计算三角形的斜边长。在向量中就是模

    def __bool__(self):
        return bool(abs(self))  # 判断模是否为0
    
    @classmethod
    def from_bytes(cls, bytes_):
        """bytes转换为向量类"""
        type_code = chr(bytes_[0])
        memy = memoryview(bytes_[1:]).cast(type_code)
        return cls(*memy)


v1 = Vector2d(3, 4)

x, y = v1
print('x,y:', x, y)  # 支持拆包:__iter__

print('v1:', v1)  # 支持打印:先调用__str__,如果找不到再继续调用__repr__
print('v1.__repr__():', v1.__repr__())

v1_clone = eval(repr(v1))
print(v1_clone == v1)  # 支持比较相等:__eq__

print(bytes(v1))  # 二进制表示形式:__bytes__

print(abs(v1))  # 返回实例的模:__abs__

print(bool(v1), bool(Vector2d(0, 0)))  # 如果实例的模为0,返回FALSE;否者返回True

打印结果
x,y: 3.0 4.0
v1: (3.0, 4.0)
v1.__repr__(): Vector2d(3.0,4.0)
True
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
5.0
True False

以上的知识点比较密集:

  1. 实例支持拆包,要实现__iter__方法,这样实例变成可迭代对象,才能拆包。直接调用生成器表达式实现。
  2. format中的占位符{!r}和%r 一样,返回对象本体。比如字符串'abc'就返回'abc' 而不是abc
  3. 内部方法获取当前类名:class_name = self.__class__.__name__ 或者是 class_name = type(self).__name__
  4. tuple函数接收的参数是iterable,由于前面已经实现了__iter__,self是可迭代的,所以可以使用tuple(self)
  5. array数组的第一个参数是类型,这里d表示双精度浮点数,将向量转换为数组,然后再把数组转换为字节序列。
  6. ord()函数的参数是一个字符(长度为1的字符串),返回对应字符的ASCII数值。
  7. 和上面 对应的函数是chr(), 参数是ASCII数值,返回对应的字符串。
  8. memoryview.cast能用不同的方式读写同一块内存数据,这里使用bytes第一位保存的类型作为参数
  9. cls指代的是本身类Vector2d,括号内拆包了memoryview内存视图:3.0,4.0 最终结果就是 Vector2d(3.0,4.0) 产生了一个新的实例。

classmethod与staticmethod

classmethod用法:定义操作类,而不是操作实例的方法。这种方法的第一个参数是类本身,而不是实例。最常见的用途是定义备选构造方法,就像上面示例中的from_bytes方法,返回的是一个新实例。

staticmethod用法:不需要实例作为参数。其实静态方法就是一个普通的函数,只是定义的类的定义体中,可能用于归类展示。

示例,简单比较

class Demo:

    @classmethod
    def class_meth(*args):
        print(args)

    @staticmethod
    def static_meth(*args):
        print(args)


d = Demo()
d.class_meth()
d.static_meth()
打印结果
(<class '__main__.Demo'>,)
()

格式化显示format

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

  • format(obj, format_spec) 第二个参数是格式说明符。
  • str.format()在字符串中,{}里代换字段中冒号后面的部分。

>>> brl = 1/2.43

>>> brl

0.4115226337448559

>>> format(brl, '0.4f')

'0.4115'

>>> '1 BRL = {rate:.4f} USD'.format(rate=brl)

'1 BRL = 0.4115 USD'

上面的{}中的rate是字段名,与格式说明符无关,用来决定把.format()的哪个参数传给代码字段。

冒号后面的'.4f'是格式说明符。

格式说明符使用的表示语法叫格式规范微语言。

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

>>> format(42,'b')

'101010'

>>> format(2/3,'.1%')

'66.7%'

格式规范微语言是可扩展的,因为各个类可以自行决定如何解释format_spec参数。例如,datatime模块中的类,它的__format__方法和strftime()函数一样。

>>> from datetime import datetime

>>> now = datetime.now()

>>> format(now, '%H:%M:%S')

'14:54:10'

>>> 'now {:%H:%M %p}'.format(now)

'now 14:54 PM'

如果类没有定义__format__方法,那么从object集成的方法会返回str(my_object)。

示例,让向量类支持格式说明符。

···省略其他代码    
    def __format__(self, format_spec):
        return str(tuple(format(x, format_spec) for x in self))


v1 = Vector2d(3, 4)

print(format(v1))
print(format(v1, '.2f'))  # 保留2位小数
print(format(v1, '.3e'))  # 使用科学计数法,保留3位小数
打印:
('3.0', '4.0')
('3.00', '4.00')
('3.000e+00', '4.000e+00')

下面将再次扩展,让向量类支持自定义的格式代码。

格式说明符如果以'p'结果,那么在展示向量<r, θ>,其中的r是模,θ(读西塔)是弧度。'p'之前的部分像往常那样解释。

···省略其他代码      
    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)  # 'p'之外的格式说明符,应用到向量的各个分量上。
        return outer_fmt.format(*components)


v1 = Vector2d(1, 1)

print(format(v1, 'p'))
print(format(v1, '.5fp'))
print(format(v1, '.3ep'))
打印
<1.4142135623730951, 0.7853981633974483>
<1.41421, 0.78540>
<1.414e+00, 7.854e-01>

可散列的Vector2d

目前实现的Vector2d实例是不可散列的,使用hash(v1)函数会报错.。

要实现可散列的对象,必须实现__hash__方法和__eq__方法,前面的代码中__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 __iter__(self):
        return (i for i in (self.x, self.y))  # 使用迭代器(iterator)表达式

知识点:

  • 使用两个前导下划线__把属性标记为私有的。
  • 使用@property装饰器把读值方法标记
  • 后续在调用self.x和self.y时,去到了def x和def y函数中的行为,也就是返回了私有属性,不影响读取。但是不能修改了

让这些向量不可变,接下来才能实现__hash__方法,这个方法返回一个整数。

示例2,实现__hash__方法

    def __hash__(self):
        return hash(self.x) ^ hash(self.y)

使用^(位运算符 异或)来混合各分量的散列值。

ps:理想情况下还要考虑对象属性的散列值(__eq__方法也要使用,比较属性的散列值),因为相等的对象应该具有相同的散列值。

总结:一个完成的向量类

import math
from array import array


class Vector2d:
    type_code = '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 __hash__(self):
        return hash(self.x) ^ hash(self.y)  # 位运算符 异或^

    def __iter__(self):
        return (i for i in (self.x, self.y))  # 使用迭代器(iterator)表达式

    def __repr__(self):
        class_name = type(self).__name__  # 自省类名
        # class_name = self.__class__.__name__  # 自省类名,另一种写法
        return '{}({!r},{!r})'.format(class_name, *self)  # *self拆包,用到了自己的__iter__方法。  !r 和%r一样 返回对象本体

    def __str__(self):
        # return '(%r,%r)' % (self.x, self.y)
        return str(tuple(self))  # 从可迭代对象可以轻松得到一个元组,然后转成字符串。tuple( iterable ),所以会调用__iter__

    def __eq__(self, other):
        return tuple(self) == tuple(other)  # 构建成元组,可以快速比较所有分量。

    def __bytes__(self):
        """向量类转换为bytes"""
        return bytes([ord(self.type_code)]) + bytes(array(self.type_code, self))  # b'd' + b'\x00...'

    def __abs__(self):
        return math.hypot(self.x, self.y)  # hypot()函数就是计算三角形的斜边长。在向量中就是模

    def __bool__(self):
        return bool(abs(self))  # 判断模是否为0

    @classmethod
    def from_bytes(cls, bytes_):
        """bytes转换为向量类"""
        type_code = chr(bytes_[0])
        memy = memoryview(bytes_[1:]).cast(type_code)
        return cls(*memy)

    # def __format__(self, format_spec):
    #     return str(tuple(format(x, format_spec) for x in self))

    def angle(self):
        """计算角度"""
        return math.atan2(self.x, self.y)

    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)  # 'p'之外的格式说明符,应用到向量的各个分量上。
        return outer_fmt.format(*components)

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

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

如果以__name这种形式(两个前导下划线)命名实例属性,Python会把属性名以特殊形式存入__dict__属性中,会在前面加上一个下划线和类名。这种语言特征叫做名称改写(name mangling)

示例,打印Vector2d类实例的__dict__

v1 = Vector2d(1, 1)
print(v1.__dict__)
打印结果
{'_Vector2d__x': 1.0, '_Vector2d__y': 1.0}

名称改写是一种安全措施,目的是避免意外访问,不能防止故意修改,比如v1._Vector2d__x = 7 这种代码是可以正常给私有属性赋值的。

Python的另一个种属性是单个下划线,比如_name,这种一般称为“受保护的”属性,程序员们自我约定在类的外部不能访问这种属性。

在模块中的变量,如果使用一个单导线划线的话,会对导入有影响,比如from mymod import * 这种写法,就不会导入_name这种变量。

但是可以使用from mymod import _name这种写法强制导入

使用__slots__类属性节省空间

在默认情况下,Python在各个实例中名为__dict__的字典里面存储实例属性。但是字典的底层使用了散列表来提升访问速度,字典会消耗大量内存。

如果要处理数量为百万个属性,通过__slots__类属性,可以节省大量的内存,方法是让解释器在元组中存储实例属性,而不是字典。实际Python会在各个实例中使用类似元组的结构结构存储实例变量,避免使用__dict__属性消耗太多内存。

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

ps: 继承超类的__slots__并没有效果,Python只会使用各个类中定义的__slots_属性。

示例, 类属性__slots__的使用方法

class Vector2d:
    __slots__ = ('__x', '__y')
    type_code = 'd'  # 类属性。在于实例和字节序列转换时使用。

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

在类中定义了__slots__属性之后,实例不能再有__slots__中之外的其他属性,也就是这会约束用于新增实例属性,算是一个副作用,所以不能滥用__slots__,推荐当实例可能产生数百万个时才建议使用。

如果想把实例作为弱引用的目标,要把'__weakref__'加到__slots__中。__weakref__就是让对象支持弱引用,用于自定义的类中默认就有。

总结:

  • 每个子类都要定义__slots__属性,因为解释器会忽略继承__slots__
  • 实例只能有__slots__列出的属性。
  • 如果不把'__weakref__'加到__slots__中, 实例就不能作为弱引用的目标。

类属性

类属性可以为实例属性提供默认值。

比如在上述代码中,Vector2d.typecode属性的默认值是'd',即转换为字节序列时使用8字节双浮点数。如果想使用'f',即4字节单精度浮点数,可以修改实例属性

v1.typecode = 'f' 然后再执行函数bytes(v1)

如果想要修改类属性,那么必须在类上修改:

Vector2d.typecode = 'f'

但是使用继承更符合Python的风格,而且效果更持久,也更有针对性。

class NewVector2d(Vector2d):

    type_code = 'f'

这也说明了Vect2d.__repr__方法为何没有硬编码class_name的值,而是使用type(self).__name__获取,这样在子类继承后也能继续使用。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值