書接上回,繼續來講講關於類及其方法的一些冷知識和燙知識。本篇將重點講講類中的另一個重要元素——方法,也和上篇一樣用各種神奇的例子,從原理和機制的角度為你還原一個不一樣的Python。在閱讀本篇之前,推薦閱讀一下上篇的內容:Python科普系列——類與方法(上篇)
對象方法的本質
說到面向對象編程,大家應該對方法這一概念並不陌生。其實在上篇中已經提到,在Python中方法的本質就是一個字段,將一個可執行的對象賦值給當前對象,就可以形成一個方法,並且也嘗試了手動制造一個對象。
但是,如果你對Python有更進一步的了解,或者有更加仔細的觀察的話,會發現實際上方法還可以被如下的方式調用起來
class T:
def __init__(self, x, y):
self.x = x
self.y = y
def plus(self, z):
return self.x + self.y + z
t = T(2, 5)
t.plus(10) # 17
T.plus(t, 10) # 17, the same as t.plus(10)
沒錯,就是 T.plus(t, 10)
這樣的用法,這在其他一些面向對象語言中似乎並沒見到過,看起來有些費解。先別急,咱們再來做另外一個實驗
def plus(self, z):
return self.x + self.y + z
class T:
def __init__(self, x, y):
self.x = x
self.y = y
plus = plus
t = T(2, 5)
print(t)
print(plus)
print(T.plus)
print(t.plus)
# <__main__.T object at 0x7fa58afa7630>
# <function plus at 0x7fa58af95620>
# <function plus at 0x7fa58af95620>
# <bound method plus of <__main__.T object at 0x7fa58afa7630>>
在這個程序中, plus
函數被單獨定義,且在類 T
中被引入為字段。而觀察一下上面的輸出,會發現一個事實—— plus
和T.plus
完全就是同一個對象,但t.plus
就並不是同一個。根據上篇中的分析,前者是顯而易見的,但是 t.plus
卻成了一個叫做 method
的東西,這又是怎么回事呢?我們繼續來實驗,接着上一個程序
from types import MethodType
print(type(t.plus), MethodType) # <class 'method'> <class 'method'>
assert isinstance(t.plus, MethodType)
會發現傳說中的 method
原來是 types.MethodType
這個對象。既然已經有了這個線索,那么我們繼續翻閱一下這個 types.MethodType
的源代碼,源代碼有部分內容不可見,只找到了這些(此處Python版本為 3.9.6
)
class MethodType:
__func__: _StaticFunctionType
__self__: object
__name__: str
__qualname__: str
def __init__(self, func: Callable[..., Any], obj: object) -> None: ...
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
此處很抱歉沒有找到官方文檔, types
庫的文檔在 MethodType
的部分只有一行概述性文本而沒有實質性內容,所以只好去翻源代碼了,如果有有讀者找到的正經的文檔或說明歡迎貼在評論區。不過這么一看,依然有很關鍵的發現——這個__init__
方法有點東西,從名字和類型來看,func
應該是一個函數,obj
應該是一個任意對象。咱們再來想想,從邏輯要素的角度想想, t.plus
這個東西要想能運行起來,必要因素有那些,答案顯而易見:
- 運行邏輯,通俗來說就是實際運行的函數
plus
- 運行主體,通俗來說在方法前被用點隔開的那個對象
t
到這一步為止答案已經呼之欲出了,不過本着嚴謹的科學精神接下來還是需要進行更進一步的驗證,我們需要嘗試拆解這個 t.plus
,看看里面到底都有些什么東西(接上面的程序)
print(set(dir(t.plus)) - set(dir(plus))) # {'__self__', '__func__'}
print(t.plus.__func__) # <function plus at 0x7fa58af95620>
print(t.plus.__self__) # <__main__.T object at 0x7fa58afa7630>
首先第一行,將 dir
結果轉為集合,看看那些字段是t.plus
擁有而T.plus
沒有的。果不其然,剛好就倆字段—— __self__
和 __func__
。然后分別將這兩個字段的值進行輸出,發現—— t.plus.__func__
就是之前定義的那個plus
,而t.plus.__self__
就是實例化出來的t
。
到這一步,與我們的猜想基本吻合,只差一個終極驗證。還記得上篇中那個手動制造出來的對象不,沒錯,讓我們來用MethodType
來更加科學也更加符合實際代碼行為地再次搭建一回,程序如下
from types import MethodType
class MyObject(object):
pass
if __name__ == '__main__':
t = MyObject() # the same as __new__
t.x = 2 # the same as __init__
t.y = 5
def plus(self, z):
return self.x + self.y + z
t.plus = MethodType(plus, t) # a better implement
print(t.x, t.y) # 2 5
print(t.plus(233)) # 240
print(t.plus)
# <bound method plus of <__main__.MyObject object at 0x7fbbb9170748>>
運行結果和之前一致,也和常規方式實現的對象完全一致,並且這個 t.plus
也正是之前實驗中所看到的那種 method
。至此,Python中對象方法的本質已經十分清楚——對象方法一個基於原有函數,和當前對象,通過types.MethodType
類進行組合后實現的可執行對象。
延伸思考1:基於上述的分析,為什么 T.plus(t, 10)
會有和 t.plus(10)
等價的運行效果?
延伸思考2:為什么對象方法開頭第一個參數是 self
,而從第二個參數開始才是實際傳入的? MethodType
對象在被執行的時候,其內部原理可能是什么樣的?
歡迎評論區討論!
類方法與靜態方法
說完了對象方法,咱們再來看看另外兩種常見方法——類方法和靜態方法。首先是一個最簡單的例子
class T:
def __init__(self, x, y):
self.x = x
self.y = y
def plus(self, z):
return self.x + self.y + z
@classmethod
def method_cls(cls, suffix):
return str(cls.__name__) + suffix
@staticmethod
def method_stt(content):
return ''.join(content[::-1])
其中 method_cls
是一個類方法, method_stt
是一個靜態方法,這一點大家應該並不陌生。那廢話不多說,先看看這個 method_cls
到底是什么(程序接上文)
print(T.method_cls) # <bound method T.method_cls of <class '__main__.T'>>
t = T(2, 3)
print(t.method_cls) # <bound method T.method_cls of <class '__main__.T'>>
很眼熟對吧,沒錯——無論是位於類T
上的T.method_cls
,還是位於對象t
上的t.method_cls
,都是在上一章節中所探討過的types.MethodType
類型對象,而且還是同一個對象。接下來再看看其內部的結構(程序接上文)
print(T.method_cls.__func__) # <function T.method_cls at 0x7f78d86fe2f0>
print(T.method_cls.__self__) # <class '__main__.T'>
print(T) # <class '__main__.T'>
assert T.method_cls.__self__ is T
其中 __func__
就是這個原版的 method_cls
函數,而 __self__
則是類對象 T
。由此不難發現一個事實——類方法的本質是一個將當前類對象作為主體對象的方法對象。換言之,類方法在本質上和對象方法是同源的,唯一的區別在於這個 self
改叫了 cls
,並且其值換成了當前的類對象。
看完了類方法,接下來是靜態方法。首先和之前一樣,看下 method_stt
的實際內容
print(T.method_stt) # <function method_stt at 0x7fd64fa70620>
t = T(2, 3)
print(t.method_stt) # <function method_stt at 0x7fd64fa70620>
這個結果很出乎意料,但仔細想想也完全合乎邏輯——靜態方法的本質就是一個附着在類和對象上的原生函數。換言之,無論是 T.method_stt
還是 t.method_stt
,實際獲取到的都是原本的那個 method_stt
函數。
延伸思考3:為什么類方法中的主體被命名為 cls
而不是 self
,有何含義?
延伸思考4:如果將類方法中的 cls
參數重新更名為 self
,是否會影響程序的正常運行?為什么?
延伸思考5:類方法一種最為常見的應用是搭建工廠函數,例如 T.new_instance
,可用於快速創建不同特點的實例。而在Python中類本身就具備構造函數,因此類工廠方法與構造函數的異同與分工應該是怎樣的呢?請通過對其他語言的類比與實際搭建來談談你的看法。
歡迎評論區討論!
魔術方法的妙用
對於學過C++的讀者們,應該知道有一類特殊的函數是以 operator
開頭的,它們的效果是運算符重載。實際上,在Python中也有類似的特性,比如,讓我們通過一個例子來看看加法運算是如何被重載的
class T:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
print('Operating self + other ...')
if isinstance(other, T):
return T(self.x + other.x, self.y + other.y)
else:
return T(self.x + other, self.y + other)
def __radd__(self, other):
print('Operating other + self ...')
return T(other + self.x, other + self.y)
def __iadd__(self, other):
print('Operating self += other ...')
if isinstance(other, T):
self.x += other.x
self.y += other.y
else:
self.x += other
self.y += other
return self
t1 = T(2, 3)
t2 = T(8, -4)
t3 = t1 + t2
print(t3.x, t3.y)
t4 = t1 + 10
print(t4.x, t4.y)
t5 = -1 + t2
print(t5.x, t5.y)
t1 += 20
print(t1.x, t1.y)
輸出結果如下
Operating self + other ...
10 -1
Operating self + other ...
12 13
Operating other + self ...
7 -5
Operating self += other ...
22 23
對上述例子,可以作一組簡單的解釋:
__add__
為常規的加法運算,即當執行t = a + b
時會進入__add__
方法,其中self
為a
,other
為b
,返回值為t
。__radd__
為被加運算,即當執行t = b + a
時會進入__radd__
方法,其中self
為a
,other
為b
,返回值為t
。__iadd__
為自加法運算,即當執行a += b
時會進入__iadd__
方法,其中self
為運算前的a
,other
為b
,返回值為運算后的a
。
其中,常規的加法運算不難理解,加法自運算也不難理解,但是這個被加運算可能略微難懂。實際上可以結合上述代碼中的例子 t5 = -1 + t2
來看, -1
作為int
類型對象,並不支持對T
類型對象的常規加法運算,並且Python中也沒有提供類似Ruby那樣重載原生類型的機制,此時如果需要能支持-1 + t2
這樣的加法運算,則需要使用右側主體的__radd__
方法。
在上述例子中提到的三個方法,實際上還有很多的例子,並且這類方法均是以兩個下划線作為開頭和結尾的,它們有一個共同的名字——魔術方法。魔術方法一個最為直接的應用當然是支持各類算術運算符,我們來看下都支持了哪些算術運算
魔術方法 | 結構示意 | 解釋 | |
---|---|---|---|
add | self + other | 加法 | 常規加法運算 |
radd | other + self | 被加運算 | |
iadd | self += other | 自加運算 | |
sub | self - other | 減法 | 常規減法運算 |
rsub | other - self | 被減運算 | |
isub | self -= other | 自減運算 | |
mul | self * other | 乘法 | 常規乘法運算 |
rmul | other * self | 被乘運算 | |
imul | self *= other | 自乘運算 | |
matmul | self @ other | 矩陣乘法 | 常規矩陣乘法運算 |
rmatmul | other @ self | 矩陣被乘運算 | |
imatmul | self @= other | 矩陣自乘運算 | |
truediv | self / other | 普通除法 | 常規普通除法運算 |
rtruediv | other / self | 普通被除運算 | |
itruediv | self /= other | 普通自除運算 | |
floordiv | self // other | 整除 | 常規整除運算 |
rfloordiv | other // self | 被整除運算 | |
ifloordiv | self //= other | 自整除運算 | |
mod | self % other | 取余 | 常規取余運算 |
rmod | other % self | 被取余運算 | |
imod | self %= other | 自取余運算 | |
pow | self ** other | 乘方 | 常規乘方運算 |
rpow | other ** self | 被乘方運算 | |
ipow | self **= other | 自乘方運算 | |
and | self & other | 算術與 | 常規算術於運算 |
rand | other & self | 被算術於運算 | |
iand | self &= other | 自算術於運算 | |
or | self | other | 算術或 | 常規算術或運算 |
ror | other | self | 被算術或運算 | |
ior | self |= other | 自算術或運算 | |
xor | self ^ other | 算術異或 | 常規算術異或運算 |
rxor | other ^ self | 被算術異或運算 | |
ixor | self ^= other | 自算術異或運算 | |
lshift | self << other | 算術左移 | 常規算術左移運算 |
rlshift | other << self | 被算術左移運算 | |
ilshift | self <<= other | 自算術左移運算 | |
rshift | self >> other | 算術右移 | 常規算術右移運算 |
rrshift | other >> self | 被算術右移運算 | |
irshift | self >>= other | 自算術右移運算 | |
pos | +self | 取正 | 取正運算 |
neg | -self | 取反 | 取反運算 |
invert | ~self | 算術取反 | 算術取反運算 |
eq | self == other | 大小比較 | 等於比較運算 |
ne | self != other | 不等於比較運算 | |
lt | self < other | 小於比較運算 | |
le | self <= other | 小於或等於比較運算 | |
gt | self > other | 大於比較運算 | |
ge | self >= other | 大於或等於比較運算 |
可以看到,常見的算術運算可謂一應俱全。不過依然有一些東西是沒法通過魔術方法進行重載的,包括但不限於(截止發稿時,Python最新版本為 3.10.0
):
- 三目運算,即
xxx if xxx else xxx
- 邏輯與、邏輯或、邏輯非運算,即
xxx and yyy
和xxx or yyy
和not xxx
除此之外,還有一些比較常見的功能性魔術方法:
魔術方法 | 結構示意 | 解釋 | |
---|---|---|---|
getitem | self[other] | 索引操作 | 索引查詢 |
setitem | self[other] = value | 索引賦值 | |
delitem | del self[other] | 索引刪除 | |
getattr | self.other | 屬性操作 | 屬性獲取 |
setattr | self.other = value | 屬性賦值 | |
delattr | del self.other | 屬性刪除 | |
len | len(self) | 長度 | 獲取長度 |
iter | for x in self: pass | 枚舉 | 枚舉對象 |
bool | if self: pass | 真偽 | 判定真偽 |
call | self(*args, **kwargs) | 運行 | 運行對象 |
hash | hash(self) | 哈希 | 獲取哈希值 |
當然,也有一些功能性的東西是無法被魔術方法所修改的,例如:
- 對象標識符,即
id(xxx)
如此看來,魔術方法不可謂不神奇,功能還很齊全,只要搭配合理可以起到非常驚艷的效果。那這種方法的本質是什么呢,其實也很簡單——就是一種包含特殊語義的方法。例如在上述加法運算的例子中,還可以這樣去運行
t1 = T(2, 3)
t2 = T(8, -4)
t3 = t1.__add__(t2)
print(t3.x, t3.y)
# Operating self + other ...
# 10 -1
上面的 t1.__add__(t2)
其實就是 t1 + t2
的真正形態,而Python的對象系統中將這些魔術方法進行了包裝,使之與特殊的語法和用途綁定,便形成了豐富的對象操作模式。
延伸思考6:在算術運算中,常規魔術方法、被動運算魔術方法和自運算魔術方法之間是什么樣的關系,當存在不止一組可匹配模式時,實際上會執行哪個?請通過實驗嘗試一下。
延伸思考7:為什么三目運算、邏輯運算無法被魔術方法重載?可能存在什么樣的技術障礙?以及如果開放重載可能帶來什么樣的問題?
延伸思考8:為什么對象標識符運算無法被魔術方法重載?對象標識符本質是什么?如果開放重載可能帶來什么樣的問題?
延伸思考9:在你用過的Python庫中,有哪些用到了魔術方法對運算符和其他功能進行的重載?具體說說其應用范圍與方式。
延伸思考10:考慮一下numpy和torch等庫中的各類諸如加減乘除的算術運算,其中有矩陣(張量)與矩陣的運算,有矩陣對數值的運算,也有數值對矩陣的運算,它們是如何在Python的語言環境下做到簡單易用的呢?請通過翻閱文檔或閱讀源代碼給出你的分析。
延伸思考11: __matmul__
運算在哪些類型對象上可以使其支持 @
運算?在numpy和torch庫中,使用 @
作為運算符對矩陣(張量)進行運算,其運算結果和哪個運算函數是等價的?
歡迎評論區討論!
對象屬性的本質
在Python的類中,還有一種與方法類似但又不同的存在——對象屬性。比如這樣的例子
class T:
def __init__(self, x):
self.__x = x
@property
def x(self):
print('Access x ...')
return self.__x
@x.setter
def x(self, value):
print(f'Set x from {self.__x} to {value} ...')
self.__x = value
@x.deleter
def x(self):
print('Delete x\'s value ...')
self.__x = None
t = T(2)
print(t.x)
t.x = 233
del t.x
# Access x ...
# 2
# Set x from 2 to 233 ...
# Delete x's value ...
通過訪問t.x
會進入第一個getter函數,為t.x
進行賦值會進入第二個setter函數,而如果嘗試刪除t.x
則會進入第三個deleter函數,對於對象 t
來說,這是顯而易見的。不過為了研究一下原理,我們還是看看位於類 T
上的 T.x
的實際內容是什么(代碼接上文)
print(T.x) # <property object at 0x7faf16853db8>
可以看到 T.x
是一個屬性(property)對象,緊接着咱們再來看看這里面所包含的結構
print(set(dir(T.x)) - set(dir(lambda: None)))
print(T.x.fget)
print(T.x.fset)
print(T.x.fdel)
# {'fget', '__delete__', 'deleter', 'fdel', '__set__', '__isabstractmethod__', 'getter', 'setter', 'fset'}
# <function T.x at 0x7f39d32f41e0>
# <function T.x at 0x7f39d32f4268>
# <function T.x at 0x7f39d32f42f0>
可以看到 T.x
比一般的函數對象要多出來的部分,基本上分為get、set和del相關的部分,而其中的T.x.fget
、T.x.fset
和T.x.fdel
則分別指向三個不同的函數。基於目前的這些信息,尤其是這幾個命名來分析,距離正確答案已經很近了。為了進行證實,我們來嘗試手動制造一個屬性,並將其添加到類上,如下所示
def xget(self):
print('Access x ...')
return self.xvalue
def xset(self, value):
print(f'Set x from {self.xvalue} to {value} ...')
self.xvalue = value
def xdel(self):
print('Delete x\'s value ...')
self.xvalue = None
class T:
def __init__(self, x):
self.xvalue = x
x = property(xget, xset, xdel)
t = T(2)
print(t.x)
t.x = 233
del t.x
# Access x ...
# 2
# Set x from 2 to 233 ...
# Delete x's value ...
由此可見,上述的例子運行完全正常。因此實際上,property對象是一個支持 __get__
、 __set__
、 __delete__
三個魔術方法的特殊對象,關於這三個魔術方法由於涉及到的內容較多,后續可能專門做一期來講講。簡單來說,可以理解為通過在類上進行這樣的一個賦值,使得被實例化的對象的該屬性可以被訪問、賦值和刪除,Python中對象屬性的本質也就是這樣的。
延伸思考12:如何利用 property
類來構造一個只能讀寫不能刪除的屬性?以及如何構造只讀的屬性呢?
延伸思考13: property
對象中的 getter
、 setter
和 deleter
方法的用途分別是什么?
歡迎評論區討論!
后續預告
本文重點針對方法的各種機制與特性,從原理角度進行了分析。經過這兩篇關於Python類與方法的科普,基本的概念和機制已經基本講述完畢。在此基礎上,treevalue第三彈也將不久后推出,包含以下主要內容:
- 樹化方法與類方法,將基於treevalue第二彈中的函數樹化,結合本篇中對方法本質的論述進行講解。
- 樹化運算,基於算術類型魔術方法的函數樹化,會配合例子進行講解與展示。
- 基於樹化運算的應用,基於功能性魔術方法的函數樹化,講解之余集中展示其高度易用性。
此外,歡迎歡迎了解OpenDILab的開源項目:
以及我本人的幾個開源項目(部分仍在開發或完善中):