zoukankan      html  css  js  c++  java
  • 《深度剖析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
    """
    

    小结

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

  • 相关阅读:
    蜂窝网格的坐标以及寻路
    unity3d 第三人称视角的人物移动以及相机控制
    基本HTML结构
    平衡二叉树
    STL基础复习
    递归
    unity 傅老师学习
    blender基础操作
    最小生成树
    最短路径
  • 原文地址:https://www.cnblogs.com/traditional/p/13611233.html
Copyright © 2011-2022 走看看