Python如何设计面向对象的类(上)

本文转载自微信公众号「dongfanger」,作者dongfanger。转载本文请联系dongfanger公众号。

公司主营业务:网站设计、网站建设、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。创新互联是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。创新互联推出进贤免费做网站回馈大家。

Python是一门高级语言,支持面向对象设计,如何设计一个符合Python风格的面向对象的类,是一个比较复杂的问题,本文提供一个参考,表达一种思路,探究一层原理。

目标

期望实现的类具有以下基本行为:

  • __repr__ 为repr()提供支持,返回便于开发者理解的对象字符串表示形式。
  • __str__ 为str()提供支持,返回便于用户理解的对象字符串表示形式。
  • __bytes__ 为bytes()提供支持,返回对象的二进制表示形式。
  • __format__ 为format()和str.format()提供支持,使用特殊的格式代码显示对象的字符串表示形式。

Vector2d是一个向量类,期望它能支持以下操作:

 
 
 
  1. >>> v1 = Vector2d(3, 4)
  2. >>> print(v1.x, v1.y)  # 通过属性直接访问
  3. 3.0 4.0
  4. >>> x, y = v1  # 支持拆包
  5. >>> x, y
  6. (3.0, 4.0)
  7. >>> v1  # 支持repr
  8. Vector2d(3.0, 4.0)
  9. >>> v1_clone = eval(repr(v1))  # 验证repr描述准确
  10. >>> v1 == v1_clone  # 支持==运算符
  11. True
  12. >>> print(v1)  # 支持str
  13. (3.0, 4.0)
  14. >>> octets = bytes(v1)  # 支持bytes
  15. >>> octets
  16. b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
  17. >>> abs(v1)  # 实现__abs__
  18. 5.0
  19. >>> bool(v1), bool(Vector2d(0, 0))  # 实现__bool__
  20. (True, False)

基本实现

代码与解析如下:

 
 
 
  1. from array import array
  2. import math
  3. class Vector2d:
  4.     # Vector2d实例和二进制之间转换时使用
  5.     typecode = 'd'  
  6.     def __init__(self, x, y):
  7.         # 转换为浮点数
  8.         self.x = float(x)    
  9.         self.y = float(y)
  10.     def __iter__(self):
  11.         # 生成器表达式,把Vector2d实例变成可迭代对象,这样才能拆包
  12.         return (i for i in (self.x, self.y))  
  13.     def __repr__(self):
  14.         class_name = type(self).__name__
  15.         # {!r}是个万能的格式符
  16.         # *self是拆包,*表示所有元素
  17.         return '{}({!r}, {!r})'.format(class_name, *self)
  18.     def __str__(self):
  19.         # Vector2d实例是可迭代对象,可以得到一个元组,并str
  20.         return str(tuple(self))
  21.     def __bytes__(self):
  22.         # 转换为二进制
  23.         return (bytes([ord(self.typecode)]) +  
  24.                 bytes(array(self.typecode, self)))  
  25.     def __eq__(self, other):
  26.         # 比较相等
  27.         return tuple(self) == tuple(other)  
  28.     def __abs__(self):
  29.         # 向量的模是直角三角形的斜边长
  30.         return math.hypot(self.x, self.y) 
  31.     def __bool__(self):
  32.         # 0.0是False,非零值是True
  33.         return bool(abs(self))  
  34.     
  35.     @classmethod
  36.     def frombytes(cls, octets):  # classmethod不传self传cls
  37.         typecode = chr(octets[0])
  38.         memv = memoryview(octets[1:]).cast(typecode)
  39.         return cls(*memv)  # 拆包后得到构造方法所需的一对参数

代码最后用到了@classmethod装饰器,它容易跟@staticmethod混淆。

@classmethod的用法是:定义操作类,而不是操作实例的方法。常用来定义备选构造方法。

@staticmethod其实就是个普通函数,只不过刚好放在了类的定义体里。实际定义在类中或模块中都可以。

格式化显示

代码与解析如下:

 
 
 
  1. def angle(self):
  2.     return math.atan2(self.y, self.x)
  3. def __format__(self, fmt_spec=''):
  4.     if fmt_spec.endswith('p'):  # 以'p'结尾,使用极坐标
  5.         fmt_spec = fmt_spec[:-1]
  6.         coords = (abs(self), self.angle())  # 计算极坐标(magnitude, angle)
  7.         outer_fmt = '<{}, {}>'  # 尖括号
  8.     else:
  9.         coords = self  # 不以'p'结尾,构建直角坐标(x, y)
  10.         outer_fmt = '({}, {})'  # 圆括号
  11.     components = (format(c, fmt_spec) for c in coords)  # 使用内置format函数格式化字符串
  12.     return outer_fmt.format(*components)  # 拆包后代入外层格式

它实现了以下效果:

直角坐标:

 
 
 
  1. >>> format(v1)
  2. '(3.0, 4.0)'
  3. >>> format(v1, '.2f')
  4. '(3.00, 4.00)'
  5. >>> format(v1, '.3e')
  6. '(3.000e+00, 4.000e+00)'

极坐标:

 
 
 
  1. >>> format(Vector2d(1, 1), 'p')  # doctest:+ELLIPSIS
  2. '<1.414213..., 0.785398...>'
  3. >>> format(Vector2d(1, 1), '.3ep')
  4. '<1.414e+00, 7.854e-01>'
  5. >>> format(Vector2d(1, 1), '0.5fp')
  6. '<1.41421, 0.78540>'

可散列的

实现__hash__特殊方法能让Vector2d变成可散列的,不过在这之前需要先让属性不可变,代码如下:

 
 
 
  1. def __init__(self, x, y):
  2.     # 双下划线前缀,变成私有的
  3.     self.__x = float(x)
  4.     self.__y = float(y)
  5. @property  # 标记为特性
  6. def x(self):
  7.     return self.__x
  8. @property
  9. def y(self):
  10.     return self.__y

这样x和y就只读不可写了。

属性名字的双下划线前缀叫做名称改写(name mangling),相当于_Vector2d__x和_Vector2d__y,能避免被子类覆盖。

然后使用位运算符异或混合x和y的散列值:

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

节省内存

Python默认会把实例属性存储在__dict__字典里,字典的底层是散列表,数据量大了以后会消耗大量内存(以空间换时间)。通过__slots__类属性,能把实例属性存储到元组里,大大节省内存空间。

示例:

 
 
 
  1. class Vector2d:
  2.     __slots__ = ('__x', '__y')
  3.     typecode = 'd'

有几点需要注意:

必须把所有属性都定义到__slots__元组中。

子类也必须定义__slots__。

实例如果要支持弱引用,需要把__weakref也加入__slots__。

覆盖类属性

实例覆盖

Python有个很独特的特性:类属性可用于为实例属性提供默认值。实例代码中的typecode就能直接被self.typecode拿到。但是,如果为不存在的实例属性赋值,会新建实例属性,类属性不会受到影响,self.typecode拿到的是实例属性的typecode。

示例:

 
 
 
  1. >>> v1 = Vector2d(1, 2)
  2. >>> v1.typecode = 'f'
  3. >>> v1.typecode
  4. 'f'
  5. >>> Vector2d.typecode
  6. 'd'

子类覆盖

类属性是公开的,所以可以直接通过Vector2d.typecode = 'f'进行修改。但是更符合Python风格的做法是定义子类:

 
 
 
  1. class ShortVector2d(Vector2d):
  2.     typecode = 'f'

Django基于类的视图大量使用了这个技术。

小结

本文先介绍了如何实现特殊方法来设计一个Python风格的类,然后分别实现了格式化显示与可散列对象,使用__slots__能为类节省内存,最后讨论了类属性覆盖技术,子类覆盖是Django基于类的视图大量用到的技术。

参考资料:

《流畅的Python》第9章 符合Python风格的对象

https://www.jianshu.com/p/7fc0a177fd1f

新闻标题:Python如何设计面向对象的类(上)
网址分享:http://www.shufengxianlan.com/qtweb/news44/138994.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联