Python如何設(shè)計(jì)面向?qū)ο蟮念悾ㄉ希?/h1>
本文轉(zhuǎn)載自微信公眾號(hào)「dongfanger」,作者dongfanger。轉(zhuǎn)載本文請(qǐng)聯(lián)系dongfanger公眾號(hào)。
Python是一門高級(jí)語言,支持面向?qū)ο笤O(shè)計(jì),如何設(shè)計(jì)一個(gè)符合Python風(fēng)格的面向?qū)ο蟮念?,是一個(gè)比較復(fù)雜的問題,本文提供一個(gè)參考,表達(dá)一種思路,探究一層原理。
目標(biāo)
期望實(shí)現(xiàn)的類具有以下基本行為:
- __repr__ 為repr()提供支持,返回便于開發(fā)者理解的對(duì)象字符串表示形式。
- __str__ 為str()提供支持,返回便于用戶理解的對(duì)象字符串表示形式。
- __bytes__ 為bytes()提供支持,返回對(duì)象的二進(jìn)制表示形式。
- __format__ 為format()和str.format()提供支持,使用特殊的格式代碼顯示對(duì)象的字符串表示形式。
Vector2d是一個(gè)向量類,期望它能支持以下操作:
- >>> v1 = Vector2d(3, 4)
- >>> print(v1.x, v1.y) # 通過屬性直接訪問
- 3.0 4.0
- >>> x, y = v1 # 支持拆包
- >>> x, y
- (3.0, 4.0)
- >>> v1 # 支持repr
- Vector2d(3.0, 4.0)
- >>> v1_clone = eval(repr(v1)) # 驗(yàn)證repr描述準(zhǔn)確
- >>> v1 == v1_clone # 支持==運(yùn)算符
- True
- >>> print(v1) # 支持str
- (3.0, 4.0)
- >>> octets = bytes(v1) # 支持bytes
- >>> octets
- b'd\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@'
- >>> abs(v1) # 實(shí)現(xiàn)__abs__
- 5.0
- >>> bool(v1), bool(Vector2d(0, 0)) # 實(shí)現(xiàn)__bool__
- (True, False)
基本實(shí)現(xiàn)
代碼與解析如下:
- from array import array
- import math
- class Vector2d:
- # Vector2d實(shí)例和二進(jìn)制之間轉(zhuǎn)換時(shí)使用
- typecode = 'd'
- def __init__(self, x, y):
- # 轉(zhuǎn)換為浮點(diǎn)數(shù)
- self.x = float(x)
- self.y = float(y)
- def __iter__(self):
- # 生成器表達(dá)式,把Vector2d實(shí)例變成可迭代對(duì)象,這樣才能拆包
- return (i for i in (self.x, self.y))
- def __repr__(self):
- class_name = type(self).__name__
- # {!r}是個(gè)萬能的格式符
- # *self是拆包,*表示所有元素
- return '{}({!r}, {!r})'.format(class_name, *self)
- def __str__(self):
- # Vector2d實(shí)例是可迭代對(duì)象,可以得到一個(gè)元組,并str
- return str(tuple(self))
- def __bytes__(self):
- # 轉(zhuǎn)換為二進(jìn)制
- return (bytes([ord(self.typecode)]) +
- bytes(array(self.typecode, self)))
- def __eq__(self, other):
- # 比較相等
- return tuple(self) == tuple(other)
- def __abs__(self):
- # 向量的模是直角三角形的斜邊長(zhǎng)
- return math.hypot(self.x, self.y)
- def __bool__(self):
- # 0.0是False,非零值是True
- return bool(abs(self))
- @classmethod
- def frombytes(cls, octets): # classmethod不傳self傳cls
- typecode = chr(octets[0])
- memv = memoryview(octets[1:]).cast(typecode)
- return cls(*memv) # 拆包后得到構(gòu)造方法所需的一對(duì)參數(shù)
代碼最后用到了@classmethod裝飾器,它容易跟@staticmethod混淆。
@classmethod的用法是:定義操作類,而不是操作實(shí)例的方法。常用來定義備選構(gòu)造方法。
@staticmethod其實(shí)就是個(gè)普通函數(shù),只不過剛好放在了類的定義體里。實(shí)際定義在類中或模塊中都可以。
格式化顯示
代碼與解析如下:
- def angle(self):
- return math.atan2(self.y, self.x)
- def __format__(self, fmt_spec=''):
- if fmt_spec.endswith('p'): # 以'p'結(jié)尾,使用極坐標(biāo)
- fmt_spec = fmt_spec[:-1]
- coords = (abs(self), self.angle()) # 計(jì)算極坐標(biāo)(magnitude, angle)
- outer_fmt = '<{}, {}>' # 尖括號(hào)
- else:
- coords = self # 不以'p'結(jié)尾,構(gòu)建直角坐標(biāo)(x, y)
- outer_fmt = '({}, {})' # 圓括號(hào)
- components = (format(c, fmt_spec) for c in coords) # 使用內(nèi)置format函數(shù)格式化字符串
- return outer_fmt.format(*components) # 拆包后代入外層格式
它實(shí)現(xiàn)了以下效果:
直角坐標(biāo):
- >>> format(v1)
- '(3.0, 4.0)'
- >>> format(v1, '.2f')
- '(3.00, 4.00)'
- >>> format(v1, '.3e')
- '(3.000e+00, 4.000e+00)'
極坐標(biāo):
- >>> format(Vector2d(1, 1), 'p') # doctest:+ELLIPSIS
- '<1.414213..., 0.785398...>'
- >>> format(Vector2d(1, 1), '.3ep')
- '<1.414e+00, 7.854e-01>'
- >>> format(Vector2d(1, 1), '0.5fp')
- '<1.41421, 0.78540>'
可散列的
實(shí)現(xiàn)__hash__特殊方法能讓Vector2d變成可散列的,不過在這之前需要先讓屬性不可變,代碼如下:
- def __init__(self, x, y):
- # 雙下劃線前綴,變成私有的
- self.__x = float(x)
- self.__y = float(y)
- @property # 標(biāo)記為特性
- def x(self):
- return self.__x
- @property
- def y(self):
- return self.__y
這樣x和y就只讀不可寫了。
屬性名字的雙下劃線前綴叫做名稱改寫(name mangling),相當(dāng)于_Vector2d__x和_Vector2d__y,能避免被子類覆蓋。
然后使用位運(yùn)算符異或混合x和y的散列值:
- def __hash__(self):
- return hash(self.x) ^ hash(self.y)
節(jié)省內(nèi)存
Python默認(rèn)會(huì)把實(shí)例屬性存儲(chǔ)在__dict__字典里,字典的底層是散列表,數(shù)據(jù)量大了以后會(huì)消耗大量?jī)?nèi)存(以空間換時(shí)間)。通過__slots__類屬性,能把實(shí)例屬性存儲(chǔ)到元組里,大大節(jié)省內(nèi)存空間。
示例:
- class Vector2d:
- __slots__ = ('__x', '__y')
- typecode = 'd'
有幾點(diǎn)需要注意:
必須把所有屬性都定義到__slots__元組中。
子類也必須定義__slots__。
實(shí)例如果要支持弱引用,需要把__weakref也加入__slots__。
覆蓋類屬性
實(shí)例覆蓋
Python有個(gè)很獨(dú)特的特性:類屬性可用于為實(shí)例屬性提供默認(rèn)值。實(shí)例代碼中的typecode就能直接被self.typecode拿到。但是,如果為不存在的實(shí)例屬性賦值,會(huì)新建實(shí)例屬性,類屬性不會(huì)受到影響,self.typecode拿到的是實(shí)例屬性的typecode。
示例:
- >>> v1 = Vector2d(1, 2)
- >>> v1.typecode = 'f'
- >>> v1.typecode
- 'f'
- >>> Vector2d.typecode
- 'd'
子類覆蓋
類屬性是公開的,所以可以直接通過Vector2d.typecode = 'f'進(jìn)行修改。但是更符合Python風(fēng)格的做法是定義子類:
- class ShortVector2d(Vector2d):
- typecode = 'f'
Django基于類的視圖大量使用了這個(gè)技術(shù)。
小結(jié)
本文先介紹了如何實(shí)現(xiàn)特殊方法來設(shè)計(jì)一個(gè)Python風(fēng)格的類,然后分別實(shí)現(xiàn)了格式化顯示與可散列對(duì)象,使用__slots__能為類節(jié)省內(nèi)存,最后討論了類屬性覆蓋技術(shù),子類覆蓋是Django基于類的視圖大量用到的技術(shù)。
參考資料:
《流暢的Python》第9章 符合Python風(fēng)格的對(duì)象
https://www.jianshu.com/p/7fc0a177fd1f