python 符合Python風格的對象
對象表示形式
每門面向對象的語言至少都有一種獲取對象的字符串表示形式的標準方式。Python 提供了兩種方式
repr()
以便於開發者理解的方式返回對象字符串表示形式
str()
以便於用戶理解的方式返回對象的字符串表示形式。
正如你所知,我們要實現 __repr__ 和 __str__ 特殊方法,為 repr()和 str() 提供支持。
再談向量類
為了說明用於生成對象表示形式的眾多方法,我們將使用一個Vector2d 類。這一節和接下來的幾節會不斷實現這個類。我們期望 Vector2d 實例具有的基本行為如下所示。
Vector2d 實例有多種表示形式
>>> v1 = Vector2d(3, 4)>>> print(v1.x, v1.y) #Vector2d實例的分量可以直接通過屬性訪問 3.0 4.0 >>> x, y = v1 #Vector2d實例可以拆包成變量元祖 >>> x, y (3.0, 4.0) >>> v1 #repr函數調用Vector2d實例,得到的結果類似於構建實例的源碼 Vector2d(3.0, 4.0) >>> v1_clone = eval(repr(v1)) #這裏使用eval函數,表明repr函數調用Vector2d實例得到的是對構造方法的準確表述>>> v1 == v1_clone #Vector2d實例支持使用==比較;這樣便於測試 True >>> print(v1) #print函數會調用str函數,對Vector2d來說,輸出的是一個有序對 (3.0, 4.0) >>> octets = bytes(v1) #bytes函數會調用__bytes__方法,生成實例的二進制表示形式 >>> octets b‘d\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\x10@‘ >>> abs(v1) #abs函數會調用__abs__方法,返回Vector2d實例的模 5.0 >>> bool(v1), bool(Vector2d(0, 0)) #bool函數會調用__bool__方法,如果Vector2d實例的模為零,則返回False,否則返回True
vector2d_v0.py實現的方式
1 from array import array 2 import math 3 4 5 class Vector2d: 6 typecode = ‘d‘ #類屬性 7 8 def __init__(self, x, y): #構造函數,實例化接收兩個參數,x和y,轉成float類型 9 self.x = float(x) 10 self.y = float(y) 11 12 def __iter__(self): #支持叠代,也就是支持外面的拆包操作 例如,x, y = my_vector 13 return (i for i in (self.x, self.y)) 14 15 def __repr__(self): #__repr__ 方法使用 {!r} 獲取各個分量的表示形式,然後插值, 16 class_name = type(self).__name__ # 構成一個字符串;因為 Vector2d 實例是可叠代的對象,所以 17 return ‘{}({!r}, {!r})‘.format(class_name, *self) # *self 會把x 和 y 分量提供給 format 函數 18 19 def __str__(self): #從可叠代的 Vector2d 實例中可以輕松地得到一個元組,顯示為一個有序對 20 return str(tuple(self)) 21 22 def __bytes__(self): 23 return (bytes([ord(self.typecode)])+ #為了生成字節序列,我們把 typecode 轉換成字節序列 24 bytes(array(self.typecode, self))) #叠代 Vector2d 實例,得到一個數組,再把數組轉換成字節序列 25 26 def __eq__(self, other): #為了快速比較所有分量,在操作數中構建元組 27 return tuple(self) == tuple(other) 28 29 def __abs__(self): #模是 x 和 y 分量構成的直角三角形的斜邊長 30 return math.hypot(self.x, self.y) 31 32 def __bool__(self): #__bool__ 方法使用 abs(self) 計算模,然後把結果轉換成布爾值,因此,0.0 是 False,非零值是 True。 33 return bool(abs(self))
備選構造方法
我們可以把 Vector2d 實例轉換成字節序列了;同理,也應該能從字節序列轉換成 Vector2d 實例。使用之前我們用過得array.array 有個類方法 .frombytes。
?? 只需要在我們剛創建的vector2d_v0.py中添加一個類方法即可
1 @classmethod #類方法使用 classmethod 裝飾器修飾 2 def frombytes(cls, octets): #不用傳入 self 參數;相反,要通過 cls 傳入類本身 3 typecode = chr(octets[0]) #從第一個字節中讀取 typecode 4 memv = memoryview(octets[1:]).cast(typecode) #使用傳入的 octets 字節序列創建一個 memoryview,然後使用typecode 轉換。 5 return cls(*memv) #拆包轉換後的 memoryview,得到構造方法所需的一對參數
classmethod與staticmethod
先來看 classmethod。下面的?? 展示了它的用法:定義操作類,而不是操作實例的方法。classmethod 改變了調用方法的方式,因此類方法的第一個參數是類本身,而不是實例。classmethod 最常見的用途是定義備選構造方法,例如上面 ?? 中的 frombytes。註意,frombytes的最後一行使用 cls 參數構建了一個新實例,即 cls(*memv)。按照約定,類方法的第一個參數名為 cls(但是 Python 不介意具體怎麽命名)。
staticmethod 裝飾器也會改變方法的調用方式,但是第一個參數不是特殊的值。其實,靜態方法就是普通的函數,只是碰巧在類的定義體中,而不是在模塊層定義。示例對classmethod 和staticmethod 的行為做了對比。
?? 比較 classmethod 和 staticmethod 的行為
1 class Demo: 2 3 @classmethod 4 def klassmeth(*args): 5 return args #返回klassmeth所用的參數 6 7 @staticmethod 8 def statmeth(*args): 9 return args #statmeth的所有參數 10 11 print(Demo.klassmeth()) #不管怎樣調用 Demo.klassmeth,它的第一個參數始終是 Demo 類 12 print(Demo.klassmeth(‘spam‘)) 13 print(‘-‘*40) 14 print(Demo.statmeth()) #Demo.statmeth 的行為與普通的函數相似 15 print(Demo.statmeth(‘spam‘))
以上代碼執行的結果為:
(<class ‘__main__.Demo‘>,) (<class ‘__main__.Demo‘>, ‘spam‘) ---------------------------------------- () (‘spam‘,)
格式化顯示
內置的 format() 函數和 str.format() 方法把各個類型的格式化方式委托給相應的 .__format__(format_spec) 方法。format_spec 是格式說明符,它是:
-
format(my_obj, format_spec)的第二個參數,或者
-
str.format()方法的字符串,{}裏代替字段中冒號後面的部分
?? 如下
>>> brl = 1/2.43 >>> brl 0.4115226337448559 >>> format(br1, ‘0.4f‘) #第一個參數為需要格式化的字符,第二個是格式話字符串的像是,0.4f保留小數點後4位,f是float類型 ‘0.4115‘ >>> ‘1 BRL = {rate:0.2f} USD‘.format(rate=brl)#{rate:02.f} #{}括號中:前面的rate是個命名參數,需要在後面的format裏面傳遞給需要替換的字符,0.2f是保留小數點後兩位 ‘1 BRL = 0.41 USD‘
格式規範微語言為一些內置類型提供了專用的表示代碼。比如,b 和 x分別表示二進制和十六進制的 int 類型,f 表示小數形式的 float 類型,而 % 表示百分數形式:
>>> format(42, ‘b‘) ‘101010‘ >>> format(2/3, ‘.1%‘) ‘66.7%‘
格式規範微語言是可擴展的,因為各個類可以自行決定如何解釋format_spec 參數。例如, datetime 模塊中的類,它們的__format__ 方法使用的格式代碼與 strftime() 函數一樣。下面是內置的 format() 函數和 str.format() 方法的幾個示例:
>>> from datetime import datetime >>> now = datetime.now() >>> now datetime.datetime(2017, 8, 21, 14, 33, 46, 527811) >>> format(now, ‘%H:%M:%S‘) ‘14:33:46‘ >>> "It‘s now {:%I:%M %p}".format(now) "It‘s now 02:33 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)‘
?? Vector2d.__format__ 方法,第1版,實現這種輸出的 __format__ 方法
1 def __format__(self, format_spec=‘‘): 2 components = (format(c, format_spec) for c in self) #使用內置的 format 函數把 format_spec 應用到向量的各個分量上,構建一個可叠代的格式化字符串 3 return ‘({}, {})‘.format(*components) #把格式化字符串代入公式 ‘(x, y)‘ 中
下面要在微語言中添加一個自定義的格式代碼:如果格式說明符以 ‘p‘結尾,那麽在極坐標中顯示向量,即 <r, θ >,其中 r 是模,θ(西塔)是弧度;其他部分(‘p‘ 之前的部分)像往常那樣解釋。
對極坐標來說,我們已經定義了計算模的 __abs__ 方法,因此還要定義一個簡單的 angle 方法,使用 math.atan2() 函數計算角度。angle方法的代碼如下:
def angle(self): #計算極坐標 return math.atan2(self.y, self.x)
?? Vector2d.__format__ 方法,第 2 版,現在能計算極坐標了
1 def __format__(self, format_spec=‘‘): 2 if format_spec.endswith(‘p‘): #如果format_spec是格式最後一位是以p結尾,代表我們要計算極坐標 3 format_spec = format_spec[:-1] #從format_spec中刪除 ‘p‘ 後綴 4 coords = (abs(self), self.angle()) #構建一個元組,表示極坐標:(magnitude, angle) 5 outer_fmt = ‘<{}, {}>‘ #把外層格式設為一對尖括號 6 else: #如果不以 ‘p‘ 結尾,使用 self 的 x 和 y 分量構建直角坐標 7 coords = self 8 outer_fmt = ‘({}, {})‘ #把外層格式設為一對圓括號 9 components = (format(c, format_spec) for c in coords)#使用各個分量生成可叠代的對象,構成格式化字符串 10 return outer_fmt.format(*components) #把格式化字符串代入外層格式
上面代碼執行的結果為:
>>> format(Vector2d(1, 1), ‘p‘) ‘<1.4142135623730951, 0.7853981633974483>‘ >>> format(Vector2d(1, 1), ‘.3ep‘) ‘<1.414e+00, 7.854e-01>‘ >>> format(Vector2d(1, 1), ‘0.5fp‘) ‘<1.41421, 0.78540>‘
可散列的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__ 方法,前面已經實現了)。此外,還要讓向量不可變。
目前,我們可以為分量賦新值,如 v1.x = 7,Vector2d 類的代碼並不阻止這麽做。我們想要的行為是這樣的:
>>> v1.x, v1.y (3.0, 4.0) >>> v1.x = 7 Traceback (most recent call last): ... AttributeError: can‘t set attribute
為此,我們要把 x 和 y 分量設為只讀特性
1 class Vector2d: 2 typecode = ‘d‘ #類屬性 3 4 def __init__(self, x, y): #構造函數,實例化接收兩個參數,x和y,轉成float類型 5 self.__x = float(x) #使用兩個前導線,把屬相編程私有 6 self.__y = float(y) 7 8 @property #@property 裝飾器把讀值方法標記為特性 9 def x(self): #讀值方法與公開屬性同名,都是 x 10 return self.__x #直接返回 self.__x 11 12 @property 13 def y(self): 14 return self.__y 15 16 def __iter__(self): #支持叠代,也就是支持外面的拆包操作 例如,x, y = my_vector 17 return (i for i in (self.x, self.y))
實現__hash__方法
def __hash__(self): return hash(self.x) ^ hash(self.y) #x和y的值做異或
添加 __hash__ 方法之後,向量變成可散列的了:
>>> v1 = Vector2d(3, 4) >>> v2 = Vector2d(3.1, 4.2) >>> hash(v1), hash(v2) (7, 384307168202284039) >>> set([v1, v2]) {Vector2d(3.1, 4.2), Vector2d(3.0, 4.0)}
vector2d_v3.py:完整版
1 from array import array 2 import math 3 4 5 class Vector2d: 6 typecode = ‘d‘ 7 8 def __init__(self, x, y): 9 self.__x = float(x) 10 self.__y = float(y) 11 12 @property 13 def x(self): 14 return self.__x 15 16 @property 17 def y(self): 18 return self.__y 19 20 def __iter__(self): 21 return (i for i in (self.x, self.y)) 22 23 def __repr__(self): 24 class_name = type(self).__name__ 25 return ‘{}({!r},{!r})‘.format(class_name, *self) 26 27 def __str__(self): 28 return str(tuple(self)) 29 30 def __bytes__(self): 31 return (bytes([ord(self.typecode)])+ 32 bytes(array(self.typecode, self))) 33 34 def __eq__(self, other): 35 return tuple(self) == tuple(other) 36 37 def __hash__(self): 38 return hash(self.x) ^ hash(self.y) 39 40 def __abs__(self): 41 return math.hypot(self.x, self.y) 42 43 def __bool__(self): 44 return bool(abs(self)) 45 46 def angle(self): 47 return math.atan2(self.y, self.x) 48 49 def __format__(self, fmt_spec): 50 if fmt_spec.endswith(‘p‘): 51 fmt_spec = fmt_spec[:-1] 52 coords = (abs(self), self.angle()) 53 outer_fmt = ‘<{}, {}>‘ 54 else: 55 coords = self 56 outer_fmt = ‘({}, {})‘ 57 components = (format(c, fmt_spec) for c in coords) 58 return outer_fmt.format(*components) 59 60 @classmethod 61 def frombytes(cls, octets): 62 typecode = chr(octets[0]) 63 memv = memoryview(octets[1:]).cast(typecode) 64 return cls(*memv)
以上代碼的測試結果如下:
""" A two-dimensional vector class >>> v1 = Vector2d(3, 4) >>> print(v1.x, v1.y) 3.0 4.0 >>> x, y = v1 >>> x, y (3.0, 4.0) >>> v1 Vector2d(3.0, 4.0) >>> v1_clone = eval(repr(v1)) >>> v1 == v1_clone True >>> print(v1) (3.0, 4.0) >>> octets = bytes(v1) >>> octets b‘d\\x00\\x00\\x00\\x00\\x00\\x00\\x08@\\x00\\x00\\x00\\x00\\x00\\x00\\[email protected] >>> abs(v1) 5.0 >>> bool(v1), bool(Vector2d(0, 0)) (True, False) Test of ``.frombytes()`` class method: >>> v1_clone = Vector2d.frombytes(bytes(v1)) >>> v1_clone Vector2d(3.0, 4.0) >>> v1 == v1_clone True Tests of ``format()`` with Cartesian coordinates: >>> format(v1) ‘(3.0, 4.0)‘ >>> format(v1, ‘.2f‘) ‘(3.00, 4.00)‘ >>> format(v1, ‘.3e‘) ‘(3.000e+00, 4.000e+00)‘ Tests of the ``angle`` method:: >>> Vector2d(0, 0).angle() 0.0 >>> Vector2d(1, 0).angle() 0.0 >>> epsilon = 10**-8 >>> abs(Vector2d(0, 1).angle() - math.pi/2) < epsilon True >>> abs(Vector2d(1, 1).angle() - math.pi/4) < epsilon True Tests of ``format()`` with polar coordinates: >>> 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>‘ Tests of `x` and `y` read-only properties: >>> v1.x, v1.y (3.0, 4.0) >>> v1.x = 123 Traceback (most recent call last): ... AttributeError: can‘t set attribute Tests of hashing: >>> v1 = Vector2d(3, 4) >>> v2 = Vector2d(3.1, 4.2) >>> hash(v1), hash(v2) (7, 384307168202284039) >>> len(set([v1, v2])) 2 """
Python的私有屬性和“受保護的”屬性
舉個例子。有人編寫了一個名為 Dog 的類,這個類的內部用到了 mood實例屬性,但是沒有將其開放。現在,你創建了 Dog 類的子類:Beagle。如果你在毫不知情的情況下又創建了名為 mood 的實例屬性,那麽在繼承的方法中就會把 Dog 類的 mood 屬性覆蓋掉。這是個難以調試的問題。
為了避免這種情況,如果以 __mood 的形式(兩個前導下劃線,尾部沒有或最多有一個下劃線)命名實例屬性,Python 會把屬性名存入實例的__dict__ 屬性中,而且會在前面加上一個下劃線和類名。因此,對Dog 類來說,__mood 會變成 _Dog__mood;對 Beagle 類來說,會變成_Beagle__mood。這個語言特性叫名稱改寫(name mangling)。
?? 私有屬性的名稱會被“改寫”,在前面加上下劃線和類名
>>> v1 = Vector2d(3, 4) >>> v1.__dict__ {‘_Vector2d__y‘: 4.0, ‘_Vector2d__x‘: 3.0} >>> v1._Vector2d__x 3.0
Python 解釋器不會對使用單個下劃線的屬性名做特殊處理,不過這是很多 Python 程序員嚴格遵守的約定,他們不會在類外部訪問這種屬性。遵守使用一個下劃線標記對象的私有屬性很容易,就像遵守使用全大寫字母編寫常量那樣容易。
使用 __slots__ 類屬性節省空間
默認情況下,Python 在各個實例中名為 __dict__ 的字典裏存儲實例屬性。為了使用底層的散列表提升訪問速度,字典會消耗大量內存。如果要處理數百萬個屬性不多的實例,通過 __slots__類屬性,能節省大量內存,方法是讓解釋器在元組中存儲實例屬性,而不用字典。
註意:
繼承自超類的 __slots__ 屬性沒有效果。Python 只會使用各個類中定義的 __slots__ 屬性。
定義 __slots__ 的方式是,創建一個類屬性,使用 __slots__ 這個名字,並把它的值設為一個字符串構成的可叠代對象,其中各個元素表示各個實例屬性。我喜歡使用元組,因為這樣定義的 __slots__ 中所含的信息不會變化。
舉個?? vector2d_v3_slots.py:只在 Vector2d 類中添加了__slots__ 屬性
class Vector2d: __slots__ = (‘__x‘, ‘__y‘)
typecode = ‘d‘
# 下面是各個方法(因排版需要而省略了)
在類中定義 __slots__ 屬性的目的是告訴解釋器:“這個類中的所有實例屬性都在這兒了!”這樣,Python 會在各個實例中使用類似元組的結構存儲實例變量,從而避免使用消耗內存的 __dict__ 屬性。如果有數百萬個實例同時活動,這樣做能節省大量內存。
註意:
在類中定義 __slots__ 屬性之後,實例不能再有__slots__ 中所列名稱之外的其他屬性。這只是一個副作用,不是__slots__ 存在的真正原因。不要使用 __slots__ 屬性禁止類的用戶新增實例屬性。__slots__ 是用於優化的,不是為了約束程序員。
python 符合Python風格的對象