文章目錄
- 9.1 對象表示形式
- 9.2 再談向量類
- 9.3 備選構造方法
- 9.4 classmethod與staticmethod
- 9.5 格式化顯示
- 9.6 可散列的Vector2d
- 什么是可散列的數據類型
- 9.6 可散列的Vector
- 9.7 Python的私有屬性和“受保護的”屬性
- 9.8 使用 __slots__ 類屬性節省空間
本章包含以下話題:
- 支持用于生成對象其他表示形式的內置函數(如 repr()、bytes(),等等)
- 使用一個類方法實現備選構造方法
- 擴展內置的 format() 函數和 str.format() 方法使用的格式微語言
- 實現只讀屬性
- 把對象變為可散列的,以便在集合中及作為 dict 的鍵使用
- 利用 _slots_ 節省內存。
我們將開發一個簡單的二維歐幾里得向量類型,在這個過程中涵蓋上述全部話題。
在實現這個類型的中間階段,我們會討論兩個概念:
- 如何以及何時使用 @classmethod 和 @staticmethod 裝飾器
- Python 的私有屬性和受保護屬性的用法、約定和局限
我們從對象表示形式函數開始。
9.1 對象表示形式
每門面向對象的語言至少都有一種獲取對象的字符串表示形式的標準方式。Python 提供了
兩種方式。
- repr()
以便于開發者理解的方式返回對象的字符串表示形式。 - str()
以便于用戶理解的方式返回對象的字符串表示形式。
為了給對象提供其他的表示形式,還會用到另外兩個特殊方法:_bytes_ 和_format_。_bytes_ 方法與 _str_ 方法類似:bytes() 函數調用它獲取對象的字節序列表示形式。而 _format_ 方法會被內置的 format() 函數和 str.format() 方法調用,使用特殊的格式代碼顯示對象的字符串表示形式。
記住,在 Python 3 中,
- _repr_、_str_ 和 _format_ 都必須返回 Unicode 字符串(str 類型)。
- 只有_bytes_ 方法應該返回字節序列(bytes 類型)
9.2 再談向量類
from array import array
import mathclass Vector2d:typecode='d'# typecode是類屬性def __init__(self, x, y):self.x = xself.y = ydef __iter__(self):return (i for i in (self.x, self.y))def __repr__(self):class_name = type(self).__name__return '{}{!r},{!r}'.format(class_name, *self)def __str__(self):return str(tuple(self))def __bytes__(self): # 生成實例的二進制表示形式return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self)))def __eq__(self, other):return tuple(self) == tuple(other)def __abs__(self):return math.hypot(self.x, self.y) #模是 x 和 y 分量構成的直角三角形的斜邊長def __bool__(self):return bool(abs(self))v1 = Vector2d(3, 4)
print(v1.x, v1.y)
x, y = v1
print(x, y)
print(v1)
v1_clone = eval(repr(v1))
print(v1_clone == v1)
print(v1)
octets = bytes(v1)
print(octets)
print(abs(v1))
9.3 備選構造方法
我們可以把 Vector2d 實例轉換成字節序列了;同理,也應該能從字節序列轉換成Vector2d 實例。在標準庫中探索一番之后,我們發現 array.array 有個類方法.frombytes(2.9.1 節介紹過)正好符合需求。下面在 vector2d_v1.py(見示例 9-3)中為Vector2d 定義一個同名類方法。
@classmethod ?
def frombytes(cls, octets): ?typecode = chr(octets[0]) ?memv = memoryview(octets[1:]).cast(typecode) ?return cls(*memv) ?
? 類方法使用 classmethod 裝飾器修飾。
? 不用傳入 self 參數;相反,要通過 cls 傳入類本身。
? 從第一個字節中讀取 typecode。
? 使用傳入的 octets 字節序列創建一個 memoryview,然后使用 typecode 轉換。
2.9.2 節簡單介紹過 memoryview,說明了它的 .cast 方法。
? 拆包轉換后的 memoryview,得到構造方法所需的一對參數。
9.4 classmethod與staticmethod
先來看 classmethod。示例 9-3 展示了它的用法:定義操作類,而不是操作實例的方法。classmethod 改變了調用方法的方式,因此類方法的第一個參數是類本身,而不是實例。classmethod 最常見的用途是定義備選構造方法,例如示例 9-3 中的
frombytes。注意,frombytes 的最后一行使用 cls 參數構建了一個新實例,即cls(*memv)。按照約定,類方法的第一個參數名為 cls(但是 Python 不介意具體怎么命名)。
staticmethod 裝飾器也會改變方法的調用方式,但是第一個參數不是特殊的值。其實,靜態方法就是普通的函數,只是碰巧在類的定義體中,而不是在模塊層定義。
>>> class Demo:
... @classmethod
... def klassmeth(*args):
... return args
... @staticmethod
... def statmeth(*args):
... return args
...
>>> Demo.klassmeth()
(<class '__main__.Demo'>,)
>>> Demo.statmeth()
()
>>> Demo.klassmeth('spam')
(<class '__main__.Demo'>, 'spam')
9.5 格式化顯示
內置的 format() 函數和 str.format() 方法把各個類型的格式化方式委托給相應的.format(format_spec) 方法。format_spec 是格式說明符,它是:format(my_obj, format_spec) 的第二個參數,或者str.format() 方法的格式字符串,{} 里代換字段中冒號后面的部分。
例如:
>>> br1 = 1/2.43
>>> br1
0.4115226337448559
>>> format(br1, '0.4f')
'0.4115'
>>> '1 BRL={rate:0.2f} USD'.format(rate=br1)
'1 BRL=0.41 USD'
格式規范微語言為一些內置類型提供了專用的表示代碼。比如,b 和 x 分別表示二進制和十六進制的 int 類型,f 表示小數形式的 float 類型,而 % 表示百分數形式:
>>> format(42,'b')
'101010'
>>> format(2/3, '.1%')
'66.7%'
下面是內置的 format() 函數和 str.format() 方法的幾個示例
>>> from datetime import datetime
>>> now= datetime.now()
>>> format(now, '%H:%M:%S')
'18:35:23'
>>> "Its now {:%I:%M %p}".format(now)
'Its now 06:35 PM'
如果類沒有定義 format 方法,從 object 繼承的方法會返回 str(my_object)。我
們為 Vector2d 類定義了 str 方法,因此可以這樣做:
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'
然而,如果傳入格式說明符,object.format 方法會拋出 TypeError:
>>> format(v1, '.3f')
Traceback (most recent call last):
...
TypeError: non-empty format string passed to object.__format__
我們將實現自己的微語言來解決這個問題。首先,假設用戶提供的格式說明符是用于格式
化向量中各個浮點數分量的。我們想達到的效果是:
>>> v1 = Vector2d(3, 4)
>>> format(v1)
'(3.0, 4.0)'
>>> format(v1, '.2f')
'(3.00, 4.00)'
>>> format(v1, '.3e')
'(3.000e+00, 4.000e+00)'
實現這種輸出的 format 方法如示例 9-5 所示。
示例 9-5 Vector2d._format_ 方法,第 1 版
# 在Vector2d類中定義
def __format__(self, fmt_spec=''):components = (format(c, fmt_spec) for c in self) return '({}, {})'.format(*components)
對極坐標來說,我們已經定義了計算模的 abs 方法,因此還要定義一個簡單的
angle 方法,使用 math.atan2() 函數計算角度。angle 方法的代碼如下:
# 在Vector2d類中定義
def angle(self):return math.atan2(self.y, self.x)
這樣便可以增強 format 方法,計算極坐標,如示例 9-6 所示。
示例 9-6 Vector2d.format 方法,第 2 版,現在能計算極坐標了
def __format__(self, fmt_spec=''):if fmt_spec.endswith('p'): fmt_spec = fmt_spec[:-1] coords = (abs(self), self.angle()) outer_fmt = '<{}, {}>' else:coords = self outer_fmt = '({}, {})' components = (format(c, fmt_spec) for c in coords) return outer_fmt.format(*components)
9.6 可散列的Vector2d
按照定義,目前 Vector2d 實例是不可散列的,因此不能放入集合(set)中:
>>> v1 = Vector2d(3, 4)
>>> hash(v1)
Traceback (most recent call last):
...
TypeError: unhashable type: 'Vector2d'
>>> set([v1])
Traceback (most recent call last):
...
TypeError: unhashable type: 'Vector2d'
為了把 Vector2d 實例變成可散列的,必須使用 hash 方法(還需要 eq 方法,前面已經實現了)。
什么是可散列的數據類型
可散列的(hashable)
在散列值永不改變,而且如果 a == b,那么 hash(a) == hash(b) 也是 True 的情況下,如果對象既有 _hash_ 方法,也有 _eq_ 方法,那么這樣的對象稱為可散列的對象。在內置的類型中,大多數不可變的類型都是可散列的;但是,僅當元組的每一個元素都是可散列的時,元組才是可散列的。
- 如果一個對象是可散列的,那么在這個對象的生命周期中,它的散列值是不變的,而且這個對象需要實現 _hash_() 方法。另外可散列對象還要有_eq_() 方法,這樣才能跟其他鍵做比較。如果兩個可散列對象是相等的,那么它們的散列值一定是一樣的……
- 原子不可變數據類型(str、bytes 和數值類型)都是可散列類型,frozenset 也是可散列的,因為根據其定義,frozenset 里只能容納可散列類型。元組的話,只有當一個元組包含的所有元素都是可散列類型的情況下,它才是可散列的。
有這么一句話“Python 里所有的不可變類型都是可散列的”。這個說法其實是不準確的,比如雖然元組本身是不可變序列,它里面的元素可能是其他可變類型的引用。
一般來講用戶自定義的類型的對象都是可散列的,散列值就是它們的 id() 函數的返回值,所以所有這些對象在比較的時候都是不相等的。如果一個對象實現了 _eq_ 方法,并且在方法中用到了這個對象的內部狀態的話,那么只有當所有這些內部狀態都是不可變的情況下,這個對象才是可散列的。
9.6 可散列的Vector
from array import array
import mathclass Vector2d:typecode='d'# typecode是類屬性def __init__(self, x, y):self.__x = float(x) #使用兩個前導下劃線(尾部沒有下劃線,或者有一個下劃線),把屬性標記為私有self.__y = float(y)@property # @property 裝飾器把讀值方法標記為特性def x(self):return self.__x@propertydef y(self):return self.__ydef __iter__(self):return (i for i in (self.x, self.y))def __repr__(self):class_name = type(self).__name__return '{}{!r},{!r}'.format(class_name, *self)def __str__(self):return str(tuple(self))def __bytes__(self): # 生成實例的二進制表示形式return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self)))def __eq__(self, other):return tuple(self) == tuple(other)def __abs__(self):return math.hypot(self.x, self.y) #模是 x 和 y 分量構成的直角三角形的斜邊長def __bool__(self):return bool(abs(self))v1 = Vector2d(3, 4)
注意,我們讓這些向量不可變是有原因的,因為這樣才能實現 hash 方法。這個方法應該返回一個整數,理想情況下還要考慮對象屬性的散列值(eq 方法也要使用),因為相等的對象應該具有相同的散列值。
要想創建可散列的類型,不一定要實現特性,也不一定要保護實例屬性。只需正確地實現 hash 和 eq 方法即可。但是,實例的散列值絕不應該變化,因此我們借機提到了只讀特性。
如果定義的類型有標量數值,可能還要實現 int 和 float 方法(分別被 int()和 float() 構造函數調用),以便在某些情況下用于強制轉換類型。此外,還有用于支持內置的 complex() 構造函數的 complex 方法。Vector2d 或許應該提供
complex 方法。
9.7 Python的私有屬性和“受保護的”屬性
Python 不能像 Java 那樣使用 private 修飾符創建私有屬性,但是 Python 有個簡單的機制,能避免子類意外覆蓋“私有”屬性。
舉個例子。有人編寫了一個名為 Dog 的類,這個類的內部用到了 mood 實例屬性,但是沒有將其開放。現在,你創建了 Dog 類的子類:Beagle。如果你在毫不知情的情況下又創建了名為 mood 的實例屬性,那么在繼承的方法中就會把 Dog 類的 mood 屬性覆蓋掉。這是個難以調試的問題。
為了避免這種情況,如果以__mood 的形式(兩個前導下劃線,尾部沒有或最多有一個下劃線)命名實例屬性,Python 會把屬性名存入實例的__dict__ 屬性中,而且會在前面加上一個下劃線和類名。因此,對 Dog 類來說,__mood 會變成 _Dog__mood;對 Beagle類來說,會變成 _Beagle__mood。這個語言特性叫名稱改寫(name mangling)。
示例 9-10 私有屬性的名稱會被“改寫”,在前面加上下劃線和類名
>>> v1 = Vector2d(3, 4)
>>> v1.__dict__
{'_Vector2d__y': 4.0, '_Vector2d__x': 3.0}
>>> v1._Vector2d__x
3.0
名稱改寫是一種安全措施,不能保證萬無一失:它的目的是避免意外訪問,不能防止故意做錯事。
9.8 使用 slots 類屬性節省空間
默認情況下,Python 在各個實例中名為 dict 的字典里存儲實例屬性。如 3.9.3 節所述,為了使用底層的散列表提升訪問速度,字典會消耗大量內存。如果要處理數百萬個屬性不多的實例,通過 slots 類屬性,能節省大量內存,方法是讓解釋器在元組中存儲實例屬性,而不用字典。
定義 slots 的方式是,創建一個類屬性,使用 slots 這個名字,并把它的值設為一個字符串構成的可迭代對象,其中各個元素表示各個實例屬性。我喜歡使用元組,因為這樣定義的 slots 中所含的信息不會變化。