《深度剖析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循环才会捕捉到并终止迭代

至于__getattribute__可以看我的这一篇博客,https://www.cnblogs.com/traditional/p/11724876.html

反射

反射的话可以看我的这一篇博客,https://www.cnblogs.com/traditional/p/11731676.html

对象调用

这一点我们好像很早之前就说过了,一个对象能否被调用,取决于它的类对象中是否定义了__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
"""

小结

剩下的内容比较简单,当然描述符我们之前就说过了。最主要的是魔法方法的话,可以自己试一下就知道它们是干什么的了,没太大难度,这里就不说了。

原文地址:https://www.cnblogs.com/traditional/p/13611233.html