Python元类

  我们需要重新认识一下type。我们平常都是使用 type(对象) 来确定某一个对象是属于哪个类的(换句话表述就是,这个对象是由谁生成得到的),这也是最常见的用法,但是我们要意识到Python中的“类”其实也是一个对象。如果我们自定义了一个类A,使用type(A)会出现什么情况呢?看代码:

class A(object):
    pass

print(type(A))

  得到的结果是:

   如果我们带入 type(对象) 的理解,我们是不是可以说,我们自定义的A类是由“type”这个东西生成的?答案是肯定的,这个东西(这个例子中就是type)就叫做“元类”。所以什么是元类,元类就是用来生成对象的类。换句话说,元类的实例对象是类对象,类对象的实例化得到我们常说的对象。

1、通过type生成类对象

  我们可能很疑惑,我们并没有使用type来生成A类啊,为什么type(A)会显示,A是type的实例呢?其实这是因为当我们在使用class保留字时,python编译器自动调用了type生成了一个叫做A类的东西。下面我们来看看如何手动的使用type得到一个类对象。

## 定义一系列函数, 其实self只是一个变量(其实self并不是保留字)
def init(self, name, age):
    self.name = name
    self.age = age

def study(self):
    print(self.name, "正在学习")

def get_age(self):
    return self.age

## 通过type创建了一个叫Student的类对象(类也是对象),并使用People变量接收这个由type生成的对象
## 我们使用Class关键字创建类时其实就是在执行这个过程。
## 换句话说我们使用type创建的是 类对象 在debug时就是 class variable
People = type("Student", (object,), {
    '__init__': init,
    'study':study,
    'get_age':get_age
})

  这里我们首先定义了三个普通的函数,分别叫做init,study,get_age。我们通过type(类名,基类类名元组,类属性字典)得到到了一个叫做“Student”的类,但是我们使用的是People这个变量来接收的(也就是代码第15行做的事)。

  接下来我们可以使用People(name=xxx,age=xxx)来得到一个Student类的实例对象了,这个实例对象将会具有__init__,study,get_age这些方法。看下面代码。

## People就可以作为类来使用,注意通过People创建出的实例,该实例的类名是"Student"
boy1 = People(name="小明", age=13)

boy1.study()  ## 可以调用类的方法
print(f"{boy1.name} 的年龄是 {boy1.get_age()}")
print(type(boy1))
print(type(People))  ## 我们知道People实际上是type的实例

  第2行创建了一个实例boy1,要注意这个实例是“Student”类的,并不是People类,实际上这个People只是一个变量名。接下来我们执行了study()方法、get_age()方法以及访问了boy1的name属性。我们在第6行和第7行分别打印了一下boy1是由谁创建的,People所指向的类是由谁创建的,最终结果如下:

   可以看到正如预期所料,boy1是"Student"类(并不是People类,People在此处只是一个变量名用于接收type(....)所创建的类,正如上例第15中的那样)。

  实际上我们通过type创建的类,与使用class创建的类基本是一致的(使用保留字class创建类时,会附带很多其他的方法),上面创建的Student类实际上基本等价于下面的代码:

class Student(metaclass=type):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def study(self):
        print(self.name, "正在学习")

    def get_age(self):
        return self.age

  不过要注意的是,该代码是使用Student这个变量名来接收整个类对象(而我们的例子中是使用People这个变量名来接收的,实际上我们需要保持变量名与类名一致)。

  上面代码中有一个我们不常见的设置,就是在声明类时使用了metaclass = type,这是为了指明,生成该类是使用tpye这个元类(当然这也是Python默认的生成类的元类是type,因此一般我们自定义类时都可以不用写,就跟所有类默认继承自object一样,可以省略)。

2、创建自己的元类

  从第一小节中我们知道,其实使用class关键字声明类时,本质上是在执行 xxx = type(....) 这行代码(即通过type这个元类生成一个类对象),在第一小节的最后我们也知道在声明类时可以通过metaclass = xxx 来指定该类是由哪个元类来生成,因此本小节将会介绍,如何创建自己的元类。

  自定义元类必须声明继承自type的类(可以是type,也可以是type的子类)就可以了,从语法角度来说,只要使用class关键字,并且继承自type的类(或者type的子类,叫做其他元类)就称之为元类。例如下面的例子:

class Meta(type):
    pass

class Meta2(Meta):
    pass

class Meta3(Meta2, type):
    pass

  这些类都是元类。都可以由他们来生成类对象,也就是说以下都可以执行:B = Meta(类名,基类类名元组,类属性字典)、C = Meta2(类名,基类类名元组,类属性字典)、D = Meta3(类名,基类类名元组,类属性字典)。由Meta得到的类对象B,执行type(B)得到的结果将会是<class '__main__.Meta'>,其他情况类推。

  自定义元类我们通常都会重载__new__方法(这个魔法方法在Python魔法方法汇总 这篇文章中提过),虽然在普通类的定义中我们一般是不需要重写__new__方法,更多的是重写__init__方法,因为普通类的__new__方法是用于返回一个类的实例的。而对于自定义元类,我们就是需要自定义的返回一个元类的实例(也就是类对象),因此自定义元类,我们都会重写__new__方法。元类的__new__方法,有四个位置参数,分别是:

  • 元类本身(惯例采用cls)
  • string类型的类名称
  • 类继承的基类元组(也就是你创建的这个类对象的父类是那些,比如object)
  • 类应该包含的成员字典(这里类的成员指的是类的属性和方法,也就是定义在class 内部的那些内容,比如__init__方法....)

  下面我们就创建一个属于自己的元类,并在元类中打印一些相关信息。

class Meta(type):
    """这是自定义的元类, 我们可以通过这个元类创建类对象"""
    
    ## 必须要重写__new__()
    def __new__(cls, name, bases, attrs):
        """
        cls: 类对象固定占位符
        name: 类名 str
        bases: 继承自哪些基类 元组
        attrs: 属性字典
        """
        print(f"元类类对象={cls}, \n 类名={name}, \n 该类继承自={bases}, \n 该类具有哪些成员={attrs}")

        ## 需要调用type的__new__方法来生成类对象
        return super(Meta, cls).__new__(cls, name, bases, attrs)


## 当解析器执行到类的“声明”时就会进入到Meta中,其实也就是在执行
# MyClass = Meta("MyClass", (object,), {
#     '__init__': ...,
#     'study': ...,
#     'get_age': ...
# })
class MyClass(object, metaclass=Meta):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def study(self):
        print(self.name, "正在学习")

    def get_age(self):
        return self.age

boy1 = MyClass(name="小明", age=13)

  当解释器执行到第24行时(即类的声明时)就会进入到元类中,因为实际上类的声明,就是在执行 xxx = Meta(...),也就是元类实例化的过程。我们这里自定义的元类__new__方法比较简单,就是打印了一下传入的内容,最后使用super().__new__(...)调用父类的__new__来得到类对象。其实调用父类的__new__()来生成类对象是非常关键的,因为父类生成的类对象就包含有这个类对象的所有成员,后面我们讲到元类的使用场景时会介绍到它的重要性。上述代码执行的结果是:

  在此提醒,这些信息的输出是在MyClass类声明的时候(也就是24行),并不是在生成MyClass实例对象boy1的时候。读者可以尝试访问下boy1的name或者age,执行下study()方法,看看是否得到想要的结果。

  元类的信息会通过继承传递给子类。换句话说MyClass的子类也是由Meta生成的。我们在上面代码的基础上再执行:

## 再声明一个类,该类继承自MyClass
## 该类的声明也会进入到Meta中,因为Student的父类是由Meta产生的
class Student(MyClass):
    pass

boy2 = Student("李华", 14)

  此处Student类继承自MyCalss类,因此Student类的声明也会进入到元类中,因此上面的代码输出是:

   此时我们发现,为什么打印出来类成员只有这么几个呢?我的study()、get_age()这些方法呢?原来在元类的__new__中attrs只会收集来类中显式定义的类成员,由于Student类并没有显式定义任何的类成员(方法和属性),因此在元类的__new__中attrs收集不到任何的信息。那请问Student是否继承了MyClass的类属性呢?boy2可以执行study()方法么?答案是肯定的。读者可以去执行一下。

  由此我们也得到一个坑,那就是我们发现,如果在类中没有显式的定义(或者重写父类属性),那么将不会传递到元类的__new__的attrs中。而解决方法就是提前进行类对象的生成,再对类对象的成员进行检测。后文应用中我们可以看到。

3、元类的使用场景

  实际上大多数代码使用传统的类和对象的结构就可以替代元类的完成,同时使用元类额外增加了一层复杂性,所以在实际开发中,采取简单的办法实现目标即可。下面介绍一些可能出现并可以使用元类解决的场景。

 3.1、类验证

  如果有一个类需要遵守某些原则,比如定义类的时候,只能定义两个属性中的一个,并且必须定义其中一个,不能同时定义。面对上述需求,通常很难通过设置默认值来实现,此时通过元类就可以实现,我们看下面的代码:

class MetaFooOrBar(type):
    def __new__(cls, name, bases, attrs):
        if "foo" in attrs and "bar" in attrs:
            raise TypeError(f"类 {name} 中不能同时有foo和bar属性")
        if "foo" not in attrs and "bar" not in attrs:
            raise TypeError(f"类 {name} 中需要有foo属性或bar属性, 但只能有其中一个")
        
        return super(MetaFooOrBar, cls).__new__(cls, name, bases, attrs)

class A(metaclass=MetaFooOrBar):
    foo = "abc"

class B(metaclass=MetaFooOrBar):
    pass

  我们通过检查传入__new__中的attrs中是否存在foo和bar属性来进行判断该类的定义是否合规。如果同时存在或者都不存在则会抛出相应的异常。我们通过这个元类创建了两个了类分别是A和B,其中A设置由foo属性,而B没有设置,最终执行结果是:(再次强调,在类的声明时,就会执行元类,并不是在类的使用时)

   可以看到在B类的声明时抛出了异常,而A类顺利通过元类的检测。

  我们再看一个例子,定义一个C类继承自A,很明显,C类中会包含A类的属性,也就是C类也是具有foo属性的,但是C类能不能通过元类的检测呢?

class MetaFooOrBar(type):
    def __new__(cls, name, bases, attrs):
        if "foo" in attrs and "bar" in attrs:
            raise TypeError(f"类 {name} 中不能同时有foo和bar属性")
        if "foo" not in attrs and "bar" not in attrs:
            raise TypeError(f"类 {name} 中需要有foo属性或bar属性, 但只能有其中一个")
        
        return super(MetaFooOrBar, cls).__new__(cls, name, bases, attrs)

class A(metaclass=MetaFooOrBar):
    foo = "abc"

class C(A):
    pass

  这里我们C类继承自A类(由于A类的元类是MetaFooOrBar,所以C类不用显式的表示元类是谁),上述代码的执行结果是:

   可以发现与我们所期待的不同,C类并没有通过检测。为什么呢?原因在上一小节最后已经提到,如果在类中没有显式的定义(或者重写父类属性),那么将不会传递到元类的__new__的attrs中。要想解决这个问题,我们应该把元类MetaFooOrBar中__new__逻辑改一下:

class MetaFooOrBar(type):
    def __new__(cls, name, bases, attrs):
        ## 解决子类继承问题
        answer = super(MetaFooOrBar, cls).__new__(cls, name, bases, attrs)
        if hasattr(answer, "foo") and hasattr(answer, "bar"):
            raise TypeError(f"类 {name} 中不能同时有foo和bar属性")
        if not hasattr(answer, "foo") and not hasattr(answer, "bar"):
            raise TypeError(f"类 {name} 中需要有foo属性或bar属性, 但只能有其中一个")
        return answer

  此处我们先得到执行父类__new__的类对象,再使用hasattr()去查看类对象中是否包含有foo和bar属性,由于生成的类对象answer中会包含父类的类成员,因此上述代码能完美解决前面提到的问题。

 3.2、非继承属性

  元类的的二个应用,使类中特定的属性不会被继承。比如我有一个类叫A类,他是一个"抽象类",需要子类去实现他的某些方法,这些子类实现的方法需要经过元类的处理。但对于A类来说,他只是一个抽象类,元类中对方法的处理并不需要适用于A类,于时我们可以在A类定义时创建一个属性用于标志该类是抽象类不需要被元类处理。

class Meta(type):
    def __new__(cls, name, bases, attrs):
        ## 检查类的属性abstract,如果是True则跳过元类操作直接返回类对象
        ## 如果没有abstract属性,则默认False
        if attrs.pop("abstract", False):
            print(f"{name}的abstract属性是True")
            return super(Meta, cls).__new__(cls, name, bases, attrs)
        
        ## 下面执行真正的元类操作 最后再返回一个类对象
        return super(Meta, cls).__new__(cls, name, bases, attrs)

class A(metaclass=Meta):
    abstract = True

class B(A):
    pass

  我们在第五行采用pop方法的原因是,对于抽象类来说abstract这个A类的属性,不应该被继承才对,这也体现出元类可以修改属性,并在不需要时将类属性丢弃。因此在本例中A类并不会执行元类的任何操作(虽然本例并没有在元类中做什么操作),而且A类中的abstract属性也不存在了,最重要的是B类(A类的子类)中也不会由abstract属性。这与我们期待的一致。

 

posted @ 2023-03-30 21:08  Circle_Wang  阅读(34)  评论(0编辑  收藏  举报