【python】魔术方法大全(四)——类构建篇

发布时间 2023-10-10 18:55:23作者: 细水流深-LHF的博客

这期我们来聊聊和class建立有关的魔术方法。

__init_subclass__魔术方法

__init_subclass__ 是 Python 3.6 新增的一个特殊方法,用于定义一个类(基类)被继承时所执行的逻辑。当一个类被定义为另一个类(基类)的子类时,它会自动调用 __init_subclass__ 方法。

__init_subclass__ 方法定义在父类中,用于自定义子类的创建过程,可以控制子类的行为。在子类定义时,__init_subclass__ 方法会被自动调用,参数为子类本身。可以通过重载 __init_subclass__ 方法来实现自定义子类的行为。

下面是一个简单的例子,演示了如何使用 __init_subclass__ 方法来实现自定义子类的行为:


class Base:
    def __init_subclass__(cls, **kwargs):
        cls.x = {}
        cls.name = kwargs.get("name")


class A(Base, name="Jack"):
    pass


print(A.x)
print(A.name)

输出结果为:

{}
Jack

可以看到以Base类为基类建立衍生类A,可以看到衍生类的xname属性就被赋值了。

__set_name__魔术方法

__set_name__ 是 Python 3.6 新增的一个特殊方法,用于在类定义时自动设置属性的名称。它是在数据描述符的定义中使用的,用于设置描述符属性的名称。

当定义一个数据描述符时,它通常是作为类中的一个属性来定义的,而属性名就是描述符的名称。在类定义中使用描述符时,Python 会自动调用描述符的 __set_name__ 方法,并将属性名作为参数传递进去。这样,描述符就可以保存属性名并在需要的时候使用。

下面是一个简单的例子,演示了如何在一个数据描述符中使用 __set_name__ 方法来保存属性名:

class MyDescriptor:
    def __set_name__(self, owner, name):
        print(owner, name)
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj.__dict__.get(self.name)

    def __set__(self, obj, value):
        obj.__dict__[self.name] = value


class MyClass:
    x = MyDescriptor()

输出结果为:

<class '__main__.MyClass'> x

在上面的例子中,我们定义了一个数据描述符 MyDescriptor,其中的 __set_name__ 方法会在类定义时自动调用,并保存属性名。然后我们定义了一个类 MyClass,并将 MyDescriptor 作为它的属性。当 MyDescriptor 被设置为 MyClass 的属性时,__set_name__ 方法会自动保存属性名到 MyDescriptorname 属性中。

总之,__set_name__ 方法可以让开发者在定义数据描述符时自动保存属性名,从而在需要的时候可以方便地使用。它通常用于实现一些高级的数据描述符,例如访问控制、属性计算等。

__class_getitem__魔术方法

__class_getitem__ 是 Python 3.5 新增的一个特殊方法,用于在定义泛型类型时实现类型参数的协变或逆变。它是用于泛型类型中的类方法或静态方法的。

from typing import List


class A:
    def __class_getitem__(cls, item):
        print(item)
        return "abc"


print(A[0])

if __name__ == '__main__':
    int_arr_type = List[int]  # type hint就是基于__class_getitem__实现的
    list1: int_arr_type = [1]
    list2: int_arr_type = []

输出结果为:

0
abc

在 Python 中,泛型类型可以使用类型变量来代替具体的类型,例如 List[T],其中 T 是一个类型变量,表示列表中的元素类型。在泛型类型中,有时需要使用类型参数的子类型或超类型,这时可以使用 __class_getitem__ 方法来实现。

具体地说,__class_getitem__ 方法会在访问泛型类型的类方法或静态方法时自动调用,并传入泛型类型的参数列表。开发者可以在这个方法中根据参数列表来实现类型参数的协变或逆变。

下面是一个简单的例子,演示了如何在泛型类型中使用 __class_getitem__ 方法来实现类型参数的协变:

class MyGeneric:
    @classmethod
    def from_iterable(cls, iterable):
        return cls(*iterable)

    @staticmethod
    def identity(x):
        return x

    def __class_getitem__(cls, params):
        T, = params
        class NewGeneric(cls):
            @classmethod
            def from_iterable(cls, iterable):
                return cls(*(T(x) for x in iterable))

            @staticmethod
            def identity(x):
                return T(x)
        return NewGeneric

在上面的例子中,我们定义了一个泛型类型 MyGeneric,其中包含一个类方法 from_iterable 和一个静态方法 identity。当访问这些方法时,Python 会自动调用 __class_getitem__ 方法,并传入类型参数列表。

__class_getitem__ 方法中,我们根据类型参数 T 来定义一个新的泛型类型 NewGeneric,它的 from_iterable 方法将输入的可迭代对象转换成 T 类型的元素,而 identity 方法将输入的对象转换成 T 类型的对象。这样,MyGeneric[int] 就可以使用 from_iterableidentity 方法,并将所有输入转换成整数类型。

总之,__class_getitem__ 方法可以让开发者在泛型类型中实现类型参数的协变或逆变,从而更加灵活地处理类型。它通常用于实现一些高级的泛型类型,例如函数式编程中的 Functor、Monad 等。

__mro_entries__魔术方法

__mro_entries__ 不是一个标准的魔术方法,它是 Python 3.7 新增的一个类属性。

在 Python 中,类的继承关系是通过 Method Resolution Order (MRO) 来决定的。__mro_entries__ 类属性允许开发者指定一个自定义的 MRO 列表,以覆盖默认的 MRO 计算规则。

__mro_entries__ 类属性应该是一个元组,每个元素都是一个类。这些类将按照元组的顺序排列,并添加到当前类的 MRO 中。具体来说,Python 会按照以下顺序计算 MRO:

  1. 查找当前类的 __mro_entries__ 类属性,并将其中的类添加到 MRO 中。
  2. 根据 C3 算法(https://en.wikipedia.org/wiki/C3_linearization)计算当前类的基类 MRO,将其添加到 MRO 中。

举个例子,如果一个类定义了 __mro_entries__ = (MyMixin,),那么 MyMixin 将会在该类的所有基类之前被加入到 MRO 中。这个特性对于需要在类继承关系中插入额外的基类时非常有用。

下面是一个使用 __mro_entries__ 属性的例子:

class MyMeta:
    def __mro_entries__(self, bases):
        return (dict,)


class MyClass(MyMeta()):
    pass


print(issubclass(MyClass, MyMeta))
print(MyClass.__mro__)  # 打印 (<class '__main__.MyClass'>, <class 'object'>)

输出结果为:

False
(<class '__main__.MyClass'>, <class 'dict'>, <class 'object'>)

在上面的例子中,定义了一个元类 MyMeta,并在其中定义了 __mro_entries__ 方法,将 MyClass 的 MRO 序列扩展为 (dict,)。因此,MyClass 继承了 dict 类,成为了一个字典对象。
假如我们把上述代码 __mro_entries__返回值修改为以下,则会得到不同的结果。

class MyMeta:
    def __mro_entries__(self, bases):
        return (MyMeta,)


class MyClass(MyMeta()):
    pass


print(issubclass(MyClass, MyMeta))
print(MyClass.__mro__)  

输出结果为:

True
(<class '__main__.MyClass'>, <class '__main__.MyMeta'>, <class 'object'>)

更多详情参考:https://peps.python.org/pep-0560/

__prepare__魔术方法

__prepare__ 方法是 Python 3 中新引入的一个元类方法,它在类定义之前被调用,用于创建用于存储类属性的字典。该方法必须返回一个字典对象,用于存储类属性。

具体来说,当定义一个新的类时,Python 首先会寻找元类并调用它的 __prepare__ 方法。该方法返回的字典会被用来存储类属性。然后 Python 会解析类定义,将属性添加到字典中。

下面是一个简单的示例,演示如何使用 __prepare__ 方法:

import collections


class MyMeta(type):
    @classmethod
    def __prepare__(mcs, name, bases, **kwargs):
        # 创建一个 OrderedDict 对象作为字典,并返回它
        return collections.OrderedDict()


class MyClass(metaclass=MyMeta):
    a = 1
    b = 2
    c = 3


print(MyClass.__dict__)

输出结果为:

{'__module__': '__main__', 'a': 1, 'b': 2, 'c': 3, '__dict__': <attribute '__dict__' of 'MyClass' objects>, '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, '__doc__': None}

在这个例子中,我们定义了一个元类 MyMeta,并重载了它的 __prepare__ 方法,用于创建一个 OrderedDict 对象作为类属性的字典。然后,我们定义一个 MyClass 类,并使用 MyMeta 作为它的元类。在 MyClass 中,我们定义了三个类属性 abc。由于我们使用了 OrderedDict 对象作为 MyClass 的字典,因此这些属性将按照定义的顺序存储在字典中。最后,我们打印了 MyClass.__dict__,可以看到这个字典按照我们的期望存储了类属性,并且保持了它们的顺序。

__isinstancecheck____subclasscheck__魔术方法

这两个方法一般也是定义在元类里的。

__isinstancecheck__ 是一个特殊方法,用于自定义对象是否为某个类或其子类的实例。如果定义了这个方法,那么在使用 isinstance() 函数检查对象是否为某个类或其子类的实例时,会调用这个方法,返回值为 TrueFalse

__subclasscheck__ 是一个特殊方法,用于自定义对象是否为某个类的子类。如果定义了这个方法,那么在使用 issubclass() 函数检查对象是否为某个类的子类时,会调用这个方法,返回值为 TrueFalse

下面是一个简单的示例:

class Meta(type):
    def __instancecheck__(self, instance):
        print("Instance Check")
        return True

    def __subclasscheck__(self, subclass):
        print("Subclass Check")
        if subclass is int:
            return True
        return False


class A(metaclass=Meta):
    pass

o = A()

print(isinstance(123, A))

print()

print(issubclass(int, A))

输出结果为:

Instance Check
True

Subclass Check
True

今天介绍的魔术方法都比较偏,我们在日常开发的时候用到的机会也比较少。

本文由mdnice多平台发布