《深度剖析CPython解釋器》21. Python類機制的深度解析(第五部分): 全方位介紹Python中的魔法方法,一網打盡


楔子

下面我們來看一下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
"""

小結

剩下的內容比較簡單,當然描述符我們之前就說過了。最主要的是魔法方法的話,可以自己試一下就知道它們是干什么的了,沒太大難度,這里就不說了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM