元类是什么

Python程序员经常说一句话:“一切皆对象”,意思是在Python中,你能见到的所有东西,包括int, float, function等等都是对象。但是在日常的开发中,当说到对象的时候,我们可能不会马上就想到类。实际上类也是对象,既然类也是对象,那么就存在一种途径来创建一个类,这就是元类出场的地方,元类就是创建类的类。
 

元类做了什么

元类会拦截类的创建过程,对类进行修改,然后返回修改后的类。
仅仅看上面这一句话,元类似乎很简单,但是由于元类可以改变一个类的创建过程,可以在其中做一些奇技淫巧的事情,就会让整个事情变得异常晦涩难懂。
 
type是python中一切类的元类,对一个对象一直调用type(),就会发现最终都会指向type, type比较特殊,type自己是自己的元类。
可以使用type这样创建一个类
class Base:
    def __repr__(self):
    return self.__class__.__name__
 
 
def hello(self):
    print("hello")


Test = type("Test", (Base,), {"hello": hello})
其中,type接受的第一个参数是类的名字,第二个参数是一个元组,用来指定需要继承的类,第三个参数是一个字典,在这个字典里面可以把一个类需要的属性、方法用一个字典放进去,这样所有这个元类生成的类都会具有这些属性。
上面的代码等效于下面的代码:
class Test(Base):
    def hello(self):
    print("hello")
此外,也可以通过type创建自己的元类,然后用这个元类来创建类:
class Meta(type):
    def __new__(mcs, name, bases, attrs):
        for k in list(attrs.keys()):
            if k.lower() != k:
                # 强制转换属性名称到小写
                attrs[k.lower()] = attrs.pop(k)
        return super(Meta, mcs).__new__(mcs, name, bases, attrs)

class Case(metaclass=Meta): def Hello(self): print("hello")
在上面的元类中我们对类的属性进行了检查,如果类的属性不是小写的(不符合PEP8 风格),那么我们强制把类的属性转换成小写。通过这种方式,我们强制子类符合一定的编码风格(子类的属性都必须是小写),这只是元类应用中的hello word,使用元类还可以做更多其它的事情。
 
我们来实现一个稍微复杂一点的需求。
我们知道Python中的namedtuple可以很方便地用来表述一条数据,假设我们要用元类实现一个namedtuple,为了简单起见,我们只要求这个实现可以接受位置参数,一个可能的实现是这样的:
import itertools
import operator
 
 
def __new__(cls, *args, **kwargs):
    return tuple().__new__(cls, args)
 
 
def namedtuple(name, fields):
    fields = fields.split(",") if isinstance(fields, str) else fields
    attrs = {fld: property(itemgetter(i)) for i, fld in enumerate(fields)}
    attrs["__new__"] = __new__
    return type(name, (tuple,), attrs)
 
Student = namedtuple("Student", ["id", "name", "score"])
这样,我们就拥有了一个简单的具名元组,我们可以像使用Python的namedtuple一样进行使用
stu = Student(1, "zhangsan", 100) # 这个实现并没有支持关键字参数
print(stu.id)

元类中需要注意的几个方法

元类中,我们通常通过定义__new__或者__init__或者__call__来控制类的创建过程,__new__在类创建之前调用,可以在类创建之前进行一些修改操作,__init__在类被创建之后,对被创建的类进行一些修改,__call__在实例化类的时候调用,通常情况下,我们只需要定义其中一个就足够了,具体使用哪个,可以根据业务场景进行选择。
此外,如果需要接受额外的参数,我们还需要定义__prepare__方法,这个方法会为类准备命名空间,但是通常都不需要定义这个方法,使用默认的就可以了。
比如我们要用元类实现一个单例模式,下面是一个简单的例子:
class Singleton(type):
    __instance = None
 
    def __call__(cls, *args, **kwargs):
        if cls.__instance is None:
            cls.__instance = super().__call__(*args, **kwargs)
            return cls.__instance
        else:
             # 如果实例已经存在,直接返回
             return cls.__instance
 
 
class Test(metaclass=Singleton):
    def __init__(self):
        print('init in Test class')
 
 
if __name__ == "__main__":
    Test()
Test()
在上面的例子中,我们在元类中实现了__call__方法,在对类实例化的时候进行检查,如果类已经有实例了,我们就返回已有的实例,保证类只会实例化一次。当然这个元类还有很多缺陷,比如多个类都用这个元类的时候,实际上实例会混淆,这是另一个问题,先不在这里展开。
 

元类怎么用

答案就是:通常情况下你不需要使用元类
关于元类的使用,Python里面有一个广泛传播的解释,原版如下:
Metaclasses are deeper magic that 99% of users should never worry about it. If you wonder whether you need them, you don't (the people who actually need them to know with certainty that they need them and don't need an explanation about why). ——Tim Peters
在我们开发工作中,我们实际很少会遇到需要需要动态创建一个类的地方,万一我们遇到了需要动态改变一个类的时候,我们还可以通过类装饰器和猴子补丁实现,这两种方法相比于元类都更加容易理解。
 

小结

上面的例子只是元类使用场景里面的一些极简示例,主要在于对元类有一个初始的认知,真正用到元类的场景及其实现都会比上面的例子复杂很多,目前,我们只需要知道下面两个点就可以了。
  • 类创建实例,元类创建类,元类的实例是类
  • 一般情况下,不需要使用元类
posted on 2021-11-12 15:59  Go_Forward  阅读(834)  评论(0编辑  收藏  举报