Python如何設(shè)計面向?qū)ο蟮念悾ㄏ拢?/h1>
本文將在上篇文章二維向量Vector2d類的基礎(chǔ)上,定義表示多維向量的Vector類。
第1版:兼容Vector2d類
代碼如下:
- from array import array
- import reprlib
- import math
- class Vector:
- typecode = 'd'
- def __init__(self, components):
- self._components = array(self.typecode, components) # 多維向量存數(shù)組中
- def __iter__(self):
- return iter(self._components) # 構(gòu)建迭代器
- def __repr__(self):
- components = reprlib.repr(self._components) # 有限長度表示形式
- components = components[components.find('['):-1]
- return 'Vector({})'.format(components)
- def __str__(self):
- return str(tuple(self))
- def __bytes__(self):
- return (bytes([ord(self.typecode)]) +
- bytes(self._components))
- def __eq__(self, other):
- return tuple(self) == tuple(other)
- def __abs__(self):
- return math.sqrt(sum(x * x for x in self))
- def __bool__(self):
- return bool(abs(self))
- @classmethod
- def frombytes(cls, octets):
- typecode = chr(octets[0])
- memv = memoryview(octets[1:]).cast(typecode)
- return cls(memv) # 因為構(gòu)造函數(shù)入?yún)⑹菙?shù)組,所以不用再使用*拆包了
其中的reprlib.repr()函數(shù)用于生成大型結(jié)構(gòu)或遞歸結(jié)構(gòu)的安全表達形式,比如:
- >>> Vector([3.1, 4.2])
- Vector([3.1, 4.2])
- >>> Vector((3, 4, 5))
- Vector([3.0, 4.0, 5.0])
- >>> Vector(range(10))
- Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
超過6個的元素用...來表示。
第2版:支持切片
Python協(xié)議是非正式的接口,只在文檔中定義,在代碼中不定義。比如Python的序列協(xié)議只需要__len__和__getitem__兩個方法,Python的迭代協(xié)議只需要__getitem__一個方法,它們不是正式的接口,只是Python程序員默認的約定。
切片是序列才有的操作,所以Vector類要實現(xiàn)序列協(xié)議,也就是__len__和__getitem__兩個方法,代碼如下:
- def __len__(self):
- return len(self._components)
- def __getitem__(self, index):
- cls = type(self) # 獲取實例所屬的類
- if isinstance(index, slice): # 如果index是slice切片對象
- return cls(self._components[index]) # 調(diào)用構(gòu)造方法,返回新的Vector實例
- elif isinstance(index, numbers.Integral): # 如果index是整型
- return self._components[index] # 直接返回元素
- else:
- msg = '{cls.__name__} indices must be integers'
- raise TypeError(msg.format(cls=cls))
測試一下:
- >>> v7 = Vector(range(7))
- >>> v7[-1] # <1>
- 6.0
- >>> v7[1:4] # <2>
- Vector([1.0, 2.0, 3.0])
- >>> v7[-1:] # <3>
- Vector([6.0])
- >>> v7[1,2] # <4>
- Traceback (most recent call last):
- ...
- TypeError: Vector indices must be integers
第3版:動態(tài)存取屬性
通過實現(xiàn)__getattr__和__setattr__,我們可以對Vector類動態(tài)存取屬性。這樣就能支持v.my_property = 1.1這樣的賦值。
如果使用__setitem__方法,那么只能支持v[0] = 1.1。
代碼如下:
- shortcut_names = 'xyzt' # 4個分量屬性名
- def __getattr__(self, name):
- cls = type(self) # 獲取實例所屬的類
- if len(name) == 1: # 只有一個字母
- pos = cls.shortcut_names.find(name)
- if 0 <= pos < len(self._components): # 落在范圍內(nèi)
- return self._components[pos]
- msg = '{.__name__!r} object has no attribute {!r}' # <5>
- raise AttributeError(msg.format(cls, name))
- def __setattr__(self, name, value):
- cls = type(self)
- if len(name) == 1:
- if name in cls.shortcut_names: # name是xyzt其中一個不能賦值
- error = 'readonly attribute {attr_name!r}'
- elif name.islower(): # 小寫字母不能賦值,防止與xyzt混淆
- error = "can't set attributes 'a' to 'z' in {cls_name!r}"
- else:
- error = ''
- if error:
- msg = error.format(cls_name=cls.__name__, attr_name=name)
- raise AttributeError(msg)
- super().__setattr__(name, value) # 其他name可以賦值
值得說明的是,__getattr__的機制是:對my_obj.x表達式,Python會檢查my_obj實例有沒有名為x的屬性,如果有就直接返回,不調(diào)用__getattr__方法;如果沒有,到my_obj.__class__中查找,如果還沒有,才調(diào)用__getattr__方法。
正因如此,name是xyzt其中一個時才不能賦值,否則會出現(xiàn)下面的奇怪現(xiàn)象:
- >>> v = Vector([range(5)])
- >>> v.x = 10
- >>> v.x
- 10
- >>> v
- Vector([0.0, 1.0, 2.0, 3.0, 4.0])
對v.x進行了賦值,但實際未生效,因為賦值后Vector新增了一個x屬性,值為10,對v.x表達式來說,直接就返回了這個值,不會走我們自定義的__getattr__方法,也就沒辦法拿到v[0]的值。
第4版:散列
通過實現(xiàn)__hash__方法,加上現(xiàn)有的__eq__方法,Vector實例就變成了可散列的對象。
代碼如下:
- import functools
- import operator
- def __eq__(self, other):
- return (len(self) == len(other) and
- all(a == b for a, b in zip(self, other)))
- def __hash__(self):
- hashes = (hash(x) for x in self) # 創(chuàng)建一個生成器表達式
- return functools.reduce(operator.xor, hashes, 0) # 計算聚合的散列值
其中__eq__方法做了下修改,用到了歸約函數(shù)all(),比tuple(self) == tuple(other)的寫法,能減少處理時間和內(nèi)存。
zip()函數(shù)取名自zipper拉鏈,把兩個序列咬合在一起。比如:
- >>> list(zip(range(3), 'ABC'))
- [(0, 'A'), (1, 'B'), (2, 'C')]
第5版:格式化
Vector的格式化跟Vector2d大同小異,都是定義__format__方法,只是計算方式從極坐標換成了球面坐標:
- def angle(self, n):
- r = math.sqrt(sum(x * x for x in self[n:]))
- a = math.atan2(r, self[n-1])
- if (n == len(self) - 1) and (self[-1] < 0):
- return math.pi * 2 - a
- else:
- return a
- def angles(self):
- return (self.angle(n) for n in range(1, len(self)))
- def __format__(self, fmt_spec=''):
- if fmt_spec.endswith('h'): # hyperspherical coordinates
- fmt_spec = fmt_spec[:-1]
- coords = itertools.chain([abs(self)],
- self.angles())
- outer_fmt = '<{}>'
- else:
- coords = self
- outer_fmt = '({})'
- components = (format(c, fmt_spec) for c in coords)
- return outer_fmt.format(', '.join(components))
極坐標和球面坐標是啥?我也不知道,略過就好。
小結(jié)
經(jīng)過上下兩篇文章的介紹,我們知道了Python風格的類是什么樣子的,跟常規(guī)的面向?qū)ο笤O(shè)計不同的是,Python的類通過魔法方法實現(xiàn)了Python協(xié)議,使Python類在使用時能夠享受到語法糖,不用通過get和set的方式來編寫代碼。