楔子
下面我們來看一下Python中的魔法方法,我們知道Python將操作符都抽象成了一個魔法方法(magic method),實例對象進行操作時,實際上會調用魔法方法。也正因為如此,numpy才得以很好的實現。
那么Python中常見的魔法方法都有哪些呢?我們按照特征分成了幾類,下面就來看看魔法方法都有哪些,然后再舉例說明它們的用法。
魔法方法概覽
我們根據不同的特征分為了以下幾類:
注意:有的方法是Python2中的,但是在Python3中依然存在,但是不推薦使用了。比如:__cmp__、__coerce__等等,我們就沒有畫在圖中。

下面我們就來介紹一下上面的那些魔法方法的實際用途。
魔法方法介紹
構建以及初始化
__new__和__init__我們之前已經見識過了,還有一個__del__是做什么 呢?我們一起來看一下。
class Girl:
def __new__(cls, *args):
print("__new__")
return object.__new__(cls)
def __init__(self):
print("__init__")
def __del__(self):
print("__del__")
girl = Girl()
print("################")
"""
__new__
__init__
################
__del__
"""
__del__被稱為析構函數,當一個實例對象被銷毀之后會調用該函數。如果沒有銷毀,那么程序結束時也會調用。
比較操作
Python的比較操作符也抽象成了魔法方法,a == b,等價於a.__eq__(b)
class Girl:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return "==", self.name, other.name
def __ne__(self, other):
return "!=", self.name, other.name
def __le__(self, other):
return "<=", self.name, other.name
def __lt__(self, other):
return "<", self.name, other.name
def __ge__(self, other):
return ">=", self.name, other.name
def __gt__(self, other):
return ">", self.name, other.name
girl1 = Girl("girl1")
girl2 = Girl("girl2")
print(girl1 == girl2) # ('==', 'girl1', 'girl2')
print(girl1 != girl2) # ('!=', 'girl1', 'girl2')
print(girl1 < girl2) # ('<', 'girl1', 'girl2')
print(girl2 <= girl1) # ('<=', 'girl2', 'girl1')
print(girl2 > girl1) # ('>', 'girl2', 'girl1')
print(girl2 >= girl1) # ('>=', 'girl2', 'girl1')
我們看到如果是a > b,那么會調用a的__gt__方法,self就是a、other就是b;如果是b > a,那么調用b的__gt__方法,self就是b、other就是a;也就是說誰在前面,就調用誰的魔法方法。
但如果a > b,並且type(a)內部沒有定義__gt__呢?那么會嘗試調用type(b)內部的__gt__,如果都沒有定義,那么就會調用object的__gt__,顯然這個時候就會報錯了。
注意:如果操作符兩邊有一個是內置對象、或者內置對象的實例對象,那么會直接調用我們創建的實例對象的魔法方法
(前提是定義了)。比如:123 != girl1,那么直接調用girl1的__ne__,盡管整數對象也有__ne__。
class Girl:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name, other
girl = Girl("matsuri")
# 如果其中一方為內置,那么直接調用girl的__eq__
# 如果girl在左邊就更不用說了
print(girl == 123) # ('matsuri', 123)
print(123 == girl) # ('matsuri', 123)
print(object == girl) # ('matsuri', <class 'object'>)
print(girl == object) # ('matsuri', <class 'object'>)
單目運算
下面再來看看單目運算,估計很多人都不一定能百分百說出對應魔法方法的作用。
class Girl:
# +self 的時候調用
def __pos__(self):
return "__pos__"
# -self 的時候調用
def __neg__(self):
return "__neg__"
# abs(self) 的時候會調用, 也可以是np.abs(self), 但不推薦numpy調用
def __abs__(self):
return "__abs__"
# ~self 的時候調用
def __invert__(self):
return "__invert__"
# round(self, n) 的時候調用
def __round__(self, n=None):
return f"__round__, {n}"
# math.floor(self)的時候調用, 也可以是np.floor(self), 但不推薦numpy調用
def __floor__(self):
return "__floor__"
# math.ceil(self)的時候調用, 也可以是np.ceil(self), 但不推薦numpy調用
def __ceil__(self):
return "__ceil__"
# math.trunc(self)的時候調用, 也可以是np.trunc(self), 或者int(self)
# 但不推薦numpy調用
def __trunc__(self):
return "__trunc__"
girl = Girl()
import numpy as np
import math
# 1. +girl觸發__pos__
print(+girl) # __pos__
# 2. -girl觸發__pos__
print(-girl) # __neg__
"""
注意: 不可以寫成 0 + girl 和 0 - girl, 盡管我們知道在數學上這與 girl和-girl是等價的
但是在Python中不行, 因為這樣會調用girl的__radd__和__rsub__, 我們后面會說
"""
# 3. abs(girl)或者np.abs(girl)觸發__abs__
print(abs(girl)) # __abs__
print(np.abs(girl)) # __abs__
# 4. ~girl觸發__invert__
print(~girl) # __invert__
# 5. round(girl)觸發__round__
print(round(girl)) # __round__, None
print(round(girl, 2)) # __round__, 2
# 6. math.floor(girl), np.floor(girl)觸發__round__
print(math.floor(girl)) # __floor__
print(np.floor(girl)) # __floor__
# 7. math.ceil(girl), np.ceil(girl)觸發__round__
print(math.ceil(girl)) # __ceil__
print(np.ceil(girl)) # __ceil__
# 8. math.trunc(girl), np.trunc(girl)觸發__trunc__
print(math.trunc(girl)) # __trunc__
print(np.trunc(girl)) # __trunc__
# __trunc__表示截斷, 只保留整數位, 所以int(girl)也是可以觸發的
# 但如果是int(girl)這種方式, 它要求__trunc__必須返回一個整數
try:
int(girl)
except Exception as e:
print(e) # __trunc__ returned non-Integral (type str)
Girl.__trunc__ = lambda self: 666
print(int(Girl())) # 666
以上便是單目運算的一些魔法方法,但是說實話個人覺得只有__pos__、__neg__、__invert__會用上,因為我們可能希望一些操作的調用方式盡可能簡單,所以會通過重些+、-、~ 操作符對應的魔法方法,來賦予實例對象一些特殊的含義。
至於其它的簡單了解一下即可,不過注意的是,有些方法numpy也是可以是使用的,但是並不推薦。
算術運算
算術運算是比較常用的了,我們來看看算數運算對應的魔法方法。
class Girl:
# a + b 的時候調用, self就是a、other就是b
def __add__(self, other):
return "__add__"
# a - b 的時候調用, self就是a、other就是b
def __sub__(self, other):
return "__sub__"
# a * b 的時候調用, self就是a、other就是b
def __mul__(self, other):
return "__mul__"
# a // b 的時候調用, self就是a、other就是b
def __floordiv__(self, other):
return "__floordiv__"
# a / b 的時候調用, self就是a、other就是b
# 還有一個__div__
def __truediv__(self, other):
return "__truediv__"
# a + b 的時候調用, self就是a、other就是b
def __mod__(self, other):
return "__mod__"
# divmod(a, b) 的時候調用, self就是a、other就是b
def __divmod__(self, other):
return "__divmod__"
# a ** b 的時候調用, self就是a、other就是b
def __pow__(self, power, modulo=None):
return "__pow__"
# a << b 的時候調用, self就是a、other就是b
def __lshift__(self, other):
return "__lshift__"
# a >> b 的時候調用, self就是a、other就是b
def __rshift__(self, other):
return "__rshift__"
# a & b 的時候調用, self就是a、other就是b
def __and__(self, other):
return "__and__"
# a | b 的時候調用, self就是a、other就是b
def __or__(self, other):
return "__or__"
# a ^ b 的時候調用, self就是a、other就是b
def __xor__(self, other):
return "__xor__"
# a @ b 的時候調用, self就是a、other就是b
def __matmul__(self, other):
# 這個方法是用在矩陣運算的, Python在3.5版本的時候將@抽象成了這個方法
# 比如numpy的兩個數組如果想進行矩陣之間的相乘
# 除了np.dot(arr1, arr2)之外, 還可以直接arr1 @ arr2
return "__matmul__"
girl1 = Girl()
girl2 = Girl()
print(girl1 + girl2) # __add__
print(girl1 - girl2) # __sub__
print(girl1 * girl2) # __mul__
print(girl1 // girl2) # __floordiv__
print(girl1 / girl2) # __truediv__
print(girl1 % girl2) # __mod__
print(divmod(girl1, girl2)) # __divmod__
print(girl1 ** girl2) # __pow__
print(girl1 << girl2) # __lshift__
print(girl1 >> girl2) # __rshift__
print(girl1 & girl2) # __and__
print(girl1 | girl2) # __or__
print(girl1 ^ girl2) # __xor__
常見的算術運算大概就是上面這些,還是很簡單的。
反射算術運算
反射算術運算指的是什么呢?比如: a + b,我們知道會調用a的__add__,但如果type(a)中沒有定義__add__,那么會嘗試尋找b的__radd__。
class A:
def __add__(self, other):
return "class A:", type(self).__name__, type(other).__name__
class B:
def __radd__(self, other):
return "class B:", type(self).__name__, type(other).__name__
a = A()
b = B()
# type(a)中定義了__add__, 那么優先調用
print(a + b) # ('class A:', 'A', 'B')
# 如果type(a)中沒有定義__add__, 那么會去看type(b)中有沒有定義__radd__
del A.__add__
print(a + b) # ('class B:', 'B', 'A')
# 如果a + b, 其中一個是內置對象, 那么做法和比較操作是類似的
"""
如果是一方為內置對象, 比如:
a + 123: 直接調用a的__add__
123 + a: 直接調用a的__radd__
"""
print(123 + b) # ('class B:', 'B', 'int')
try:
123 + a
except Exception as e:
# 顯然a沒有__radd__, 因此會選擇object的__add__, 顯然這個時候報錯了
print(e) # unsupported operand type(s) for +: 'int' and 'A'
# 但a是有__add__的, 所以直接走a的__add__
A.__add__ = lambda self, other: (self, other)
print(a + "xxx") # (<__main__.A object at 0x0000020FB72A82B0>, 'xxx')
其它操作符也是類似的,a 操作符 b會調用a的__xxx__,但如果a沒有,會嘗試搜尋b的__rxxx__。
賦值算術運算
賦值算術運算適用於類似於+=這種形式,比如:
class A:
def __iadd__(self, other):
return type(self).__name__ + other
a = A()
# 會調用__iadd__, 參數self就是a, other就是">>>"
a += ">>>"
print(a) # A>>>
比較簡單,其它的也與此類似。
序列操作
下面我們看看序列操作。
class A:
def __len__(self):
return 123
a = A()
print(len(a)) # 123
# 所以len(a)本質上會調用type(a).__len__(a)
# 注意: 是type(a).__len__(a), 不是a.__len__()
a.__len__ = "xxx"
print(a.__len__) # xxx
print(len(a)) # 123
# 注意: __len__必須返回一個整型, 否則報錯
此外,__len__還有充當布爾值的作用。
class A:
pass
# 默認返回的是True
print(bool(A())) # True
A.__len__ = lambda self: 0
print(bool(A())) # False
# __len__返回的是0, 為假, 所以結果為False
# 當然真正起到決定性作用的是__bool__方法, 如果定義了__bool__, 那么以__bool__的返回值為准,必須返回布爾類型的值
# 沒有__bool__, 那么解釋器會退化, 尋找__len__
A.__bool__ = lambda self: True
print(bool(A())) # True
所以解釋器具有退化功能,會優先尋找某個方法,但如果沒有,那么會退化尋找替代方法。在后面,我們還會看到類似的實現。
class A:
def __getitem__(self, item):
print(item)
def __setitem__(self, key, value):
print(key, value)
def __delitem__(self, key):
print(key)
# 上面三個可以讓我像操作字典一樣, 操作實例對象
a = A()
a["xxx"] # xxx
a["xxx"] = "yyy" # xxx yyy
del a["aaa"] # aaa
# 不僅如此, 它們還可以作用於切片
a[3: 4] # slice(3, 4, None)
a["你好": "我很可愛": "請虧我全"] # slice('你好', '我很可愛', '請虧我全')
a["你好": "我很可愛": "請虧我全"] = "屑女仆" # slice('你好', '我很可愛', '請虧我全') 屑女仆
del a["神樂mea": "迷迭迷迭帕里桑"] # slice('神樂mea', '迷迭迷迭帕里桑', None)
這里我們再着重說一下__getitem__,我們說Python的for循環本質上會調用內部的__iter__,但如果內部沒有定義,那么解釋器會退化尋找__getitem__。
class A:
def __getitem__(self, item):
return item
lst = []
for idx in A():
if idx > 5:
break
lst.append(idx)
# 我們看到遍歷A()的時候, 在沒有__iter__的時候會去找__getitem__
# 並且默認傳遞0 1 2 3......, 所以循環遍歷的話默認是無休止的
print(lst) # [0, 1, 2, 3, 4, 5]
class B:
def __init__(self):
self.lst = ["古明地覺", "芙蘭朵露", "霧雨魔理沙", "八意永琳", "琪露諾"]
self.__len = len(self.lst)
def __getitem__(self, item):
if item == self.__len:
raise StopIteration
return self.lst[item]
print(list(B())) # ['古明地覺', '芙蘭朵露', '霧雨魔理沙', '八意永琳', '琪露諾']
(lst := []).extend(B())
print(lst) # ['古明地覺', '芙蘭朵露', '霧雨魔理沙', '八意永琳', '琪露諾']
怎么樣,是不是很神奇呢?當然for循環肯定是優先尋找__iter__,沒有的話會進行退化。
class A:
def __reversed__(self):
return "__reversed__"
def __contains__(self, item):
return item
print(reversed(A())) # __reversed__
# a in b等價於 b.__contains__(a), 但是會自動將返回值變成bool值
# 也就是說我們上面的return item其實等價於return bool(item)
print("xx" in A()) # True
print("" in A()) # False
print([] in A()) # False
最后一個__missing__比較特殊,它是針對於字典的,我們來看一下。
class A(dict):
def __missing__(self, key):
return str(key).upper()
a = A({"name": "夏色祭", "age": -1})
print(a["name"]) # 夏色祭
print(a["Name"]) # NAME
# 當我們使用獲取元素時, 首先調用__getitem__
# 由於我們沒有重寫, 顯然調用父類的__getitem__, 如果獲取到結果, 那么直接返回
# 獲取不到, 那么會調用__missing__, 如果沒有重寫則報錯, 重寫的話則是__missing__的返回值
# 所以我們可以這么做
class MyDict(dict):
def __getitem__(self, item):
return super().__getitem__(item)
def __missing__(self, key):
return f"{key!r}不存在"
d = MyDict({"name": "夏色祭", "age": -1})
print(d["age"]) # -1
print(d["AGE"]) # 'AGE'不存在
# 首先會執行我們重寫的__getitem__, 但是我們通過super().__getitem__(item), 通過父類來獲取對應的value
# 父類發現在獲取不到的時候, 會去找__missing__, 如果我們定義了就走我們重寫的__missing__
# 沒有重寫, 對於父類而言則報錯, 因為dict沒有__missing__
類型轉換
很簡單的內容了,我們直接來看一下。
class A:
def __int__(self):
return 123
def __index__(self):
return 789
# 上面兩個作用類似, 在執行int(self)時候所調用
# 但是存在一個優先級
# 默認是__int__
print(int(A())) # 123
# 如果沒有__init__, 執行__index__
del A.__int__
print(int(A())) # 789
# __init__和__index__要求必須返回整型
class B:
# 必須返回浮點型
def __float__(self):
return 3. # 3.是可以的, 但是3不行
print(float(B())) # 3.0
class C:
# 針對復數
def __complex__(self):
return 1 + 3j
print(complex(C())) # (1+3j)
上下文管理
這部分不說了,可以看我的這一篇博客:https://www.cnblogs.com/traditional/p/11487979.html,通過源碼分析contextlib標准庫介紹with語句。
屬性訪問
__getattr__、__setattr__、__delattr__和我們之前說的__getitem__、__setitem__、__delitem__類似,只不過這里是通過.的方式來訪問的。
class A:
def __getattr__(self, item):
print(item)
def __setattr__(self, key, value):
print(key, value)
def __delattr__(self, item):
print(item)
a = A()
a.name # name
a.name = "夏色祭" # name 夏色祭
del a.age # age
getattr、setattr、delattr這幾個內置函數本質上也是調用這幾個魔法方法,只不過它額外做了一些其它的工作。以getattr為例:
class A:
def __init__(self):
self.name = "夏色祭"
self.age = -1
print(getattr(A(), "name", "不存在的屬性")) # 夏色祭
print(getattr(A(), "gender", "不存在的屬性")) # 不存在的屬性
# 指定了不存在的屬性, 會返回默認值, 注意: getattr必須指定三個參數
# 否則屬性不存在會報錯, 而不是我們認為的None
# 可能有人覺得第三個參數不傳就是None, 其實不是的
class B:
def __init__(self):
self.name = "夏色祭"
self.age = -1
def __getattr__(self, item):
try:
return self.__dict__[item]
except KeyError:
raise AttributeError
print(getattr(B(), "NAME", "不存在的屬性")) # 不存在的屬性
# 我們重寫了__getattr__, 那么會調用我們重寫的__getattr__
# 然后通過字典返回, 但是注意: 在__getattr__里面可千萬不能通過.來訪問一個不存在的屬性
# 那樣會陷入無限遞歸
# 如果存在的話, 直接返回; 但如果不存在, 一定要raise AttributeError, 這樣的話才會返回getattr的第三個參數, 即默認值
# 如果是其它錯誤, getattr是無法捕獲的; 正如自定義迭代器要raise StopIteration一樣, 只有這樣for循環才會捕捉到並終止迭代
對象調用
這一點我們好像很早之前就說過了,一個對象能否被調用,取決於它的類對象中是否定義了__call__。
class Deco:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print("start")
res = self.func(*args, **kwargs)
print("end")
return res
@Deco
def foo(name, age):
print(name, age)
foo("夏色祭", -1)
"""
start
夏色祭 -1
end
"""
小結
剩下的內容比較簡單,當然描述符我們之前就說過了。最主要的是魔法方法的話,可以自己試一下就知道它們是干什么的了,沒太大難度,這里就不說了。
