• 进阶:编写符合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的向量类

    1. import math
    2. from array import array
    3. class Vector2d:
    4. type_code = 'd' # 类属性。在于实例和字节序列转换时使用。
    5. def __init__(self, x, y):
    6. self.x = float(x)
    7. self.y = float(y)
    8. def __iter__(self):
    9. return (i for i in (self.x, self.y)) # 使用迭代器(iterator)表达式
    10. def __repr__(self):
    11. class_name = type(self).__name__ # 自省类名
    12. return '{}({!r},{!r})'.format(class_name, *self) # *self拆包,用到了自己的__iter__方法。 !r 和%r一样 返回对象本体
    13. def __str__(self):
    14. # return '(%r,%r)' % (self.x, self.y)
    15. return str(tuple(self)) # 从可迭代对象可以轻松得到一个元组,然后转成字符串。tuple( iterable ),所以会调用__iter__
    16. def __eq__(self, other):
    17. return tuple(self) == tuple(other) # 构建成元组,可以快速比较所有分量。
    18. def __bytes__(self):
    19. """向量类转换为bytes"""
    20. return bytes([ord(self.type_code)]) + bytes(array(self.type_code, self)) # b'd' + b'\x00...'
    21. def __abs__(self):
    22. return math.hypot(self.x, self.y) # hypot()函数就是计算三角形的斜边长。在向量中就是模
    23. def __bool__(self):
    24. return bool(abs(self)) # 判断模是否为0
    25. @classmethod
    26. def from_bytes(cls, bytes_):
    27. """bytes转换为向量类"""
    28. type_code = chr(bytes_[0])
    29. memy = memoryview(bytes_[1:]).cast(type_code)
    30. return cls(*memy)
    31. v1 = Vector2d(3, 4)
    32. x, y = v1
    33. print('x,y:', x, y) # 支持拆包:__iter__
    34. print('v1:', v1) # 支持打印:先调用__str__,如果找不到再继续调用__repr__
    35. print('v1.__repr__():', v1.__repr__())
    36. v1_clone = eval(repr(v1))
    37. print(v1_clone == v1) # 支持比较相等:__eq__
    38. print(bytes(v1)) # 二进制表示形式:__bytes__
    39. print(abs(v1)) # 返回实例的模:__abs__
    40. print(bool(v1), bool(Vector2d(0, 0))) # 如果实例的模为0,返回FALSE;否者返回True
    41. 打印结果
    42. x,y: 3.0 4.0
    43. v1: (3.0, 4.0)
    44. v1.__repr__(): Vector2d(3.0,4.0)
    45. True
    46. b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'
    47. 5.0
    48. 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用法:不需要实例作为参数。其实静态方法就是一个普通的函数,只是定义的类的定义体中,可能用于归类展示。

    示例,简单比较

    1. class Demo:
    2. @classmethod
    3. def class_meth(*args):
    4. print(args)
    5. @staticmethod
    6. def static_meth(*args):
    7. print(args)
    8. d = Demo()
    9. d.class_meth()
    10. d.static_meth()
    11. 打印结果
    12. (<class '__main__.Demo'>,)
    13. ()

    格式化显示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)。

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

    1. ···省略其他代码
    2. def __format__(self, format_spec):
    3. return str(tuple(format(x, format_spec) for x in self))
    4. v1 = Vector2d(3, 4)
    5. print(format(v1))
    6. print(format(v1, '.2f')) # 保留2位小数
    7. print(format(v1, '.3e')) # 使用科学计数法,保留3位小数
    8. 打印:
    9. ('3.0', '4.0')
    10. ('3.00', '4.00')
    11. ('3.000e+00', '4.000e+00')

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

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

    1. ···省略其他代码
    2. def __format__(self, format_spec=''):
    3. if format_spec.endswith('p'):
    4. # 自定义格式说明符 <模, 角度>
    5. format_spec = format_spec[:-1]
    6. coords = (abs(self), self.angle())
    7. outer_fmt = '<{}, {}>'
    8. else:
    9. coords = self
    10. outer_fmt = '({}, {})'
    11. components = (format(c, format_spec) for c in coords) # 'p'之外的格式说明符,应用到向量的各个分量上。
    12. return outer_fmt.format(*components)
    13. v1 = Vector2d(1, 1)
    14. print(format(v1, 'p'))
    15. print(format(v1, '.5fp'))
    16. print(format(v1, '.3ep'))
    17. 打印
    18. <1.4142135623730951, 0.7853981633974483>
    19. <1.41421, 0.78540>
    20. <1.414e+00, 7.854e-01>

    可散列的Vector2d

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

    要实现可散列的对象,必须实现__hash__方法和__eq__方法,前面的代码中__eq__已经实现。还有就是让向量不可变。

    示例,实现向量不可变

    1. def __init__(self, x, y):
    2. self.__x = float(x)
    3. self.__y = float(y)
    4. @property
    5. def x(self):
    6. return self.__x
    7. @property
    8. def y(self):
    9. return self.__y
    10. def __iter__(self):
    11. 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__方法也要使用,比较属性的散列值),因为相等的对象应该具有相同的散列值。

    总结:一个完成的向量类

    1. import math
    2. from array import array
    3. class Vector2d:
    4. type_code = 'd' # 类属性。在于实例和字节序列转换时使用。
    5. def __init__(self, x, y):
    6. self.__x = float(x)
    7. self.__y = float(y)
    8. @property
    9. def x(self):
    10. return self.__x
    11. @property
    12. def y(self):
    13. return self.__y
    14. def __hash__(self):
    15. return hash(self.x) ^ hash(self.y) # 位运算符 异或^
    16. def __iter__(self):
    17. return (i for i in (self.x, self.y)) # 使用迭代器(iterator)表达式
    18. def __repr__(self):
    19. class_name = type(self).__name__ # 自省类名
    20. # class_name = self.__class__.__name__ # 自省类名,另一种写法
    21. return '{}({!r},{!r})'.format(class_name, *self) # *self拆包,用到了自己的__iter__方法。 !r 和%r一样 返回对象本体
    22. def __str__(self):
    23. # return '(%r,%r)' % (self.x, self.y)
    24. return str(tuple(self)) # 从可迭代对象可以轻松得到一个元组,然后转成字符串。tuple( iterable ),所以会调用__iter__
    25. def __eq__(self, other):
    26. return tuple(self) == tuple(other) # 构建成元组,可以快速比较所有分量。
    27. def __bytes__(self):
    28. """向量类转换为bytes"""
    29. return bytes([ord(self.type_code)]) + bytes(array(self.type_code, self)) # b'd' + b'\x00...'
    30. def __abs__(self):
    31. return math.hypot(self.x, self.y) # hypot()函数就是计算三角形的斜边长。在向量中就是模
    32. def __bool__(self):
    33. return bool(abs(self)) # 判断模是否为0
    34. @classmethod
    35. def from_bytes(cls, bytes_):
    36. """bytes转换为向量类"""
    37. type_code = chr(bytes_[0])
    38. memy = memoryview(bytes_[1:]).cast(type_code)
    39. return cls(*memy)
    40. # def __format__(self, format_spec):
    41. # return str(tuple(format(x, format_spec) for x in self))
    42. def angle(self):
    43. """计算角度"""
    44. return math.atan2(self.x, self.y)
    45. def __format__(self, format_spec=''):
    46. if format_spec.endswith('p'):
    47. # 自定义格式说明符 <模, 角度>
    48. format_spec = format_spec[:-1]
    49. coords = (abs(self), self.angle())
    50. outer_fmt = '<{}, {}>'
    51. else:
    52. coords = self
    53. outer_fmt = '({}, {})'
    54. components = (format(c, format_spec) for c in coords) # 'p'之外的格式说明符,应用到向量的各个分量上。
    55. return outer_fmt.format(*components)

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

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

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

    示例,打印Vector2d类实例的__dict__

    1. v1 = Vector2d(1, 1)
    2. print(v1.__dict__)
    3. 打印结果
    4. {'_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__的使用方法

    1. class Vector2d:
    2. __slots__ = ('__x', '__y')
    3. type_code = 'd' # 类属性。在于实例和字节序列转换时使用。
    4. def __init__(self, x, y):
    5. self.__x = float(x)
    6. 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__获取,这样在子类继承后也能继续使用。

  • 相关阅读:
    一篇文章扒掉“桥梁Handler”的底裤
    Java多线程(3)
    grpc系列2-针对k8s vip超时设置,自定义服务端和客户端镜像
    StrictMode卡顿与泄漏检测-StrictMode原理(2)
    Echart 非apache 托管
    2023-10-19 指针与指针的指针,我就不信你脑壳不疼
    个人项目-部署手册
    qt-mvvm代码分析
    Python 读取PostgreSQL的geometry字段时,获取geometry的中心点
    工程优化---一维搜索方法
  • 原文地址:https://blog.csdn.net/lijiachang8/article/details/126430547