Python元类详解

Python Meta Class

一、 万物皆对象

1、 简介

Python从设计之初就是一门面向对象的语言,它有一个重要的概念,即一切皆对象。

Python与java的区别:

Java虽然也是面向对象编程的语言,但是血统没有Python纯正。比如Java的八种基本数据类型之一int,在持久化的时候,就需要包装成Integer类对象。但是在python中,一切皆对象。数字、字符串、元组、列表、字典、函数、方法、类、模块等等都是对象,包括你的代码。

2、 Python对象

究竟何谓对象?不同的编程语言以不同的方式定义“对象”。某些语言中,它意味着所有对象必须有属性和方法;另一些语言中,它意味着所有的对象都可以子类化。

在Python中,定义是松散的,某些对象既没有属性也没有方法,而且不是所有的对象都可以子类化。但是Python的万物皆对象从感性上可以解释为:Python 中的一切都可以赋值给变量或者作为参数传递给函数。

符合Python对象的条件是:

  1. 能够直接赋值给一个变量
  2. 可以添加到集合对象中
  3. 能作为函数参数进行传递
  4. 可以作为函数返回值

从这里,大家就可以看出,Python中一切皆为对象,一切都符合这些条件

Python对象都会有三个特征

  • 身份,即是存储地址,可以通过id()这个方法来查询
  • 类型,即对象所属的类型,可以用type()方法来查询
  • 值,都会有各自的数据
i = 2
print(id(i))  # 查询i变量的内存地址,代码实例
print(type(i))  # 查询i变量的类型
print(i)  # 获取i变量所对应的值

对象的属性:大部分 Python 对象有属性、值或方法,使用句点(.)标记法来访问属性。最常见的属性是函数和方法,一些 Python 对象也有数据属性,如:类、模块、文件等。

3、 type|object|class

3.1 关系

首先,我们通过一段代码来理解:

class MyClass(object):
    pass

print(type(MyClass()))  # 类的实例对象属于 MyClass
print(type(MyClass))  # 发现类的对象属于 type
print(type(int))  # 发现int类也属于 type
print(type.__base__)  # 发现其父类是 object

三个之间的关系:

  • object:object类是所有类(class)的父类,包括type类,object类的父类为空
  • type:type类是所有类的类型,即为所有类(class)都可由type实例化而来,包括type类和父类object
  • class:继承自object,同时,由type进行实例化。其中,type就是我们所要讲的元类(metaclass)

3.2 创建类的第二中方式

传统创建类的方式:

# 我们传统的实现方式

class MyClass(object):
    def get(self) -> None:
        print("类获取了一个信息", self.info)
    info = "hello"

c = MyClass()
c.get() 

那我们知道了所有类都可以由type实例化来的,那么我们怎么实例化呢?我们如何将上面传统创建类转化为使用type创建类呢?:

def get(self) -> None:
    """定义类内部需要运行的函数"""
    print("类获取了一个信息", self.info)

MyClass = type("MyClass", (object, ), {"get": get, "info": "hello"})

c = MyClass()
if hasattr(c, "get"):  # 判断是否有get函数
    getattr(c, "get", None)()  # 获取get函数,并运行函数
else:
    print("该类内部没有get函数")

二、 元类

1、 什么是元类

MetaClass元类,本质也是一个类,但和普通类的用法不同,它可以对类内部的定义(包括类属性和类方法)进行动态的修改。可以这么说,使用元类的主要目的就是为了实现在创建类时,能够动态地改变类中定义的属性或者方法。

不要从字面上去理解元类的含义,事实上 MetaClass 中的 Meta 这个词根,起源于希腊语词汇 meta,包含“超越”和“改变”的意思。

  • 什么是类?可能谁都知道,类就是用来创建对象的「模板」。
  • 那什么是元类呢?一句话通俗来说,元类就是创建类的「模板」。
  • 为什么type能用来创建类?因为它本身是一个元类。使用元类创建类,那就合理了。

type是Python在背后用来创建所有类的元类,我们熟知的类的始祖 object 也是由type创建的。更有甚者,连type自己也是由type自己创建的,这就过份了。

2、 调用流程

python创建类的流程为:

通过创建流程,我们可以知道,元类接收的参数为(__name: str, **__bases: tuple[type, ...]**, __dict: dict[str, Any], **kwds: Any),这个参数即为type()函数需要传入的参数

同时,这个元类可以是函数或者类,但是必须要是一个可调用对象,同时,需要返回一个类

3、 函数做元类

我们首先来学习,使用函数作为元类,

我们来实现一个将类里面的所有属性名称转换为大写:

# 元类会自动获取通常传给`type`的参数
def upper_attr(_class, _object, _attr):
    """
      返回一个类对象,将其属性置为大写
    """

    # 过滤出所有开头不为'__'的属性,置为大写
    uppercase_attr = {}
    for name, val in _attr.items():
        if not name.startswith('__'):
            uppercase_attr[name.upper()] = val
        else:
            uppercase_attr[name] = val

    # 利用'type'创建类,同时将这个类进行返回
    return type(_class, _object, uppercase_attr)


class Foo(metaclass=upper_attr):  # 创建对象时,其会经过 metaclass 来创建,再使用自身的方法进行实例化
    bar = 'bip'

print(hasattr(Foo, 'bar'))  
print(hasattr(Foo, 'BAR'))

f = Foo()
print(f.BAR)

4、 类做元类

类做元类,有两种方式,一种是使用__new__方法实现元类,另一种是使用__call__来实现元类:

还是通过上面的实例:

使用__new__方法来实现元类:

class UpperAttrMetaClass(type):  # 注意,元类必须要继承type这个类
    """
      返回一个类对象,将其属性置为大写
    """
    def __new__(cls, *args, **kwargs):
        _class, _object, _attr = args  # 分别对应 类名,继承的类,属性
        uppercase_attr = {}
        # 过滤出所有开头不为'__'的属性,置为大写
        for name, val in _attr.items():
            if not name.startswith('__'):
                uppercase_attr[name.upper()] = val
            else:
                uppercase_attr[name] = val
        return super(UpperAttrMetaClass, cls).__new__(cls, _class, _object, uppercase_attr)  # 使用type的new方法,相当于使用type创建类


class Foo(metaclass=UpperAttrMetaClass):  # 创建对象时,其会经过 metaclass 来创建,再使用自身的方法进行实例化
    bar = 'bip'

print(hasattr(Foo, 'bar'))  
print(hasattr(Foo, 'BAR'))

f = Foo()
print(f.BAR)

在有元类的情况下,每次创建类时,会都先进入 元类的 __new__ 方法,如果你要对类进行定制,可以在这时做一些手脚。

综上,元类的__new__和普通类的不一样:

  • 元类的__new__ 在创建类时就会进入,它可以获取到上层类的一切属性和方法,包括类名,魔法方法。
  • 而普通类的__new__ 在实例化时就会进入,它仅能获取到实例化时外界传入的属性。

使用__call__方法来实现元类,这种实现方式与函数作为元类的实现非常类似,但是需要使用__init__来接收创建类所需要的参数,类似于创建类装饰器,相当于一个回调函数,无法体现出OOP。

总结,推荐使用__new__的方式来实现元类,其更面向对象

三、 元类应用

1、 ORM中的元类

元类的使用场景是挺多的,这里略微讲解一些ORM中表与对象的映射关系的元类,现在就让我们来一起阅读一下Tortoise ORM中的元类的代码:

首先,看一下Model类的创建代码:

class Model(metaclass=ModelMeta): 
    """
    Base class for all Tortoise ORM Models.
    """
	...

发现Model这个模型的基类在创建时,绑定了metaclass,那么就让我们来看一下ModelMeta里面的代码吧!

元类的代码有100多行,这里我就根据我的理解,在代码里面添加注释,进行解释,如果有问题可以在评论区留言。

class ModelMeta(type):
    __slots__ = ()

    def __new__(mcs, name: str, bases: Tuple[Type, ...], attrs: dict):  
        """
        mcs:类的名称
        bases:创建类的祖宗类
        attrs:类属性
        """
        fields_db_projection: Dict[str, str] = {}  # 存放字段名和属性名的映射表
        fields_map: Dict[str, Field] = {}  # 存放字段类型和字段名的映射表
        filters: Dict[str, Dict[str, dict]] = {}  # 存放所有字段的过滤器,用于后面对数据的过滤操作
        fk_fields: Set[str] = set()  # 存放外键约束的字段,即一对多的字段
        m2m_fields: Set[str] = set()  # 存放多对多的字段
        o2o_fields: Set[str] = set()  # 存放一对一的字段
        meta_class: "Model.Meta" = attrs.get("Meta", type("Meta", (), {}))  # 获取表的相关信息,即在我们的模型表中使用的Meta类
        pk_attr: str = "id"  # 设置表主键为id,默认为id这个字段,后面可以修改

        # 在类层次结构中搜索字段属性,这个函数我不是很明白,知道的可以私信交流哦!
        def __search_for_field_attributes(base: Type, attrs: dict) -> None:
            for parent in base.__mro__[1:]:  # 迭代取出所有祖宗类中的全部属性
                __search_for_field_attributes(parent, attrs)
            meta = getattr(base, "_meta", None)  # 获取数据表中的信息 MetaInfo类,其从 Meta类 中获取信息
            if meta:
                # For abstract classes
                for key, value in meta.fields_map.items():  # 遍历字段和字符串的映射信息
                    attrs[key] = value
                # For abstract classes manager
                for key, value in base.__dict__.items():  # 获取字段的父类信息
                    if isinstance(value, Manager) and key not in attrs:
                        attrs[key] = value.__class__()
            else:
                # For mixin classes
                for key, value in base.__dict__.items():
                    if isinstance(value, Field) and key not in attrs:
                        attrs[key] = value

        # 开始再类层次结构中搜索字段属性
        inherited_attrs: dict = {}
        for base in bases:
            __search_for_field_attributes(base, inherited_attrs)
        if inherited_attrs:
            # 确保搜索出来的字段属性排列在类属性前面
            attrs = {**inherited_attrs, **attrs}

        if name != "Model":  # 如果需要创建的类不是Model类
            custom_pk_present = False
            for key, value in attrs.items():
                if isinstance(value, Field):  # 如果这个value的属性的一个字段类型
                    if value.pk:  # 如果自定义字段中定义了主键
                        if custom_pk_present:  # 如果前面的字段定义了主键约束
                            raise ConfigurationError(...)
                        if value.generated and not value.allows_generated:  # 如果这个字段不允许生成
                            raise ConfigurationError(...)
                        custom_pk_present = True  # 标记已经设置了主键
                        pk_attr = key  # 将主键设置为新的自定义的字段

            if not custom_pk_present and not getattr(meta_class, "abstract", None):  # 如果没有设置主键,并且其不是一个抽象类
                if "id" not in attrs:  # 如果id不在字段属性里面
                    attrs = {"id": IntField(pk=True), **attrs}  # 添加字段id,同时设置id为主键

                if not isinstance(attrs["id"], Field) or not attrs["id"].pk:  # 如果属性名为id的类型不是字段类型,或者id类型字段不是主键
                    raise ConfigurationError(...)

            for key, value in attrs.items():  # 遍历所有属性的键值对
                if isinstance(value, Field):  # 如果值属于字段类型
                    if getattr(meta_class, "abstract", None):  # 如果其为抽象类,则进行拷贝
                        value = deepcopy(value)

                    fields_map[key] = value  # 把键值对添加到字符安映射的字典中
                    value.model_field_name = key  # 设置字段名为key

                    if isinstance(value, OneToOneFieldInstance):  # 如果字段的类型为一对一表
                        o2o_fields.add(key)  # 将该字段添加到一对一的字典中
                    elif isinstance(value, ForeignKeyFieldInstance):  # 如果是外键约束,一对多
                        fk_fields.add(key)  # 将该字段添加到外键约束的字典中
                    elif isinstance(value, ManyToManyFieldInstance):  # 如果是多对多模型
                        m2m_fields.add(key)  # 将该字段添加到多对多的字段中
                    else:  # 如果是普通的字段
                        fields_db_projection[key] = value.source_field or key  # 设置表的名字,source_filed 自定义名字,key 属性名字  
                        filters.update(  
                            # def get_filters_for_field(field_name: str, field: Optional[Field], source_field: str) -> Dict[str, dict],这个函数可以自行在源码中阅读,在过滤器文件中
                            get_filters_for_field(
                                field_name=key,
                                field=fields_map[key],
                                source_field=fields_db_projection[key],
                            )
                        )
                        if value.pk:  # 如果是主键
                            filters.update(
                                get_filters_for_field(
                                    field_name="pk",
                                    field=fields_map[key],
                                    source_field=fields_db_projection[key],
                                )
                            )

        # 清除类属性
        for slot in fields_map:
            attrs.pop(slot, None)  # 在attrs中将类中的所有的源字段都删除,如:a = Field(...)
        attrs["_meta"] = meta = MetaInfo(meta_class)  # 将_meta属性的值设置为MetaInfo
		# 将这里面创建的数据全部存入meta中,也就是属性attr['_meta']中
        meta.fields_map = fields_map  
        meta.fields_db_projection = fields_db_projection
        meta._filters = filters
        meta.fk_fields = fk_fields
        meta.backward_fk_fields = set()
        meta.o2o_fields = o2o_fields
        meta.backward_o2o_fields = set()
        meta.m2m_fields = m2m_fields
        meta.default_connection = None
        meta.pk_attr = pk_attr
        meta.pk = fields_map.get(pk_attr)  # type: ignore
        if meta.pk:  # 如果有主键
            meta.db_pk_column = meta.pk.source_field or meta.pk_attr  # 设置设置主键的字段名字
        meta._inited = False  
        if not fields_map:  # 如果没有数据添加到字段映射表中,则说明是一个抽象类
            meta.abstract = True

        new_class = super().__new__(mcs, name, bases, attrs)  # 使用type创建一个类
        for field in meta.fields_map.values():  
            field.model = new_class  # 指定每一个字段的模型表

        for fname, comment in _get_comments(new_class).items():  # _get_comments()获取一些字段的注释信息,返回字典
            if fname in fields_map:  # 如果fname
                fields_map[fname].docstring = comment
                if fields_map[fname].description is None:
                    fields_map[fname].description = comment.split("\n")[0]

        if new_class.__doc__ and not meta.table_description:
            meta.table_description = inspect.cleandoc(new_class.__doc__).split("\n")[0]  # 设置数据表的描述信息
        for key, value in attrs.items():  
            if isinstance(value, Manager):
                value._model = new_class  # 将值所对的模型指向生成的类
        meta._model = new_class  # type: ignore
        meta.manager._model = new_class   
        meta.finalise_fields()  # 最后确定模型域
        return new_class  # 把创建的类返回,生成新的类

有部分内容我也不是很明白,只是初略的注释了每一行代码的作用,如有错误,尽请指正。

2、 单例模式

使用__call__来实现:

class MyMeta(type):

    def __init__(self, *args, **kwargs):
        self.__instance = None
        super(MyMeta,self).__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            self.__instance = super(MyMeta,self).__call__(*args, **kwargs)
        return self.__instance

class MyClass(metaclass=MyMeta):
    pass

m = MyClass()
m2 = MyClass()
print(m2 == m)

使用__new__实现:

class MyMeta(type):

    def __new__(cls, name, bases, attrs):
        attrs["_instance"] = None
        return super(MyMeta,cls).__new__(cls, name, bases, attrs)

    def __call__(self, *args, **kwargs):
        if self._instance is None:
            self._instance = super(MyMeta,self).__call__(*args, **kwargs)
        return self._instance

class MyClass(metaclass=MyMeta):
    pass

m = MyClass()
m2 = MyClass()
print(m2 == m)

注意,使用__new__的方式不是很优雅,推荐使用__init__的方式

同时,为什么这里不是直接在__new__方法里面来进行实现单例模式呢?

  • 因为__new__是在实例化MyClass(即MyClass())前创建类的时候调用的,在实例化MyClass时,即运行MyClass()时,如果元类实现了__call__方法,则调用元类中的__call__方法,然后再调用类本身的__new__方法...

  • 我们可以来做一个测试,以此来增加大家对元类创建的理解:

    class MyMeta(type):
    
        def __new__(cls, *args, **kwargs):
            print("元类中的__new__被调用")
            return super(MyMeta, cls).__new__(cls, *args, **kwargs)
        
        def __init__(self, *args, **kwargs):
            print("元类中的__init__被调用")
            return super(MyMeta, self).__init__(*args, **kwargs)
        
        def __call__(self, *args, **kwargs):
            print("元类中的__call__被调用")
            return super(MyMeta, self).__call__(*args, **kwargs)
    
    class MyClass(metaclass=MyMeta):
        def __new__(cls, *args, **kwargs):
            print("主类中的__new__被调用")
            return super(MyClass, cls).__new__(cls, *args, **kwargs)
        
        def __init__(self, *args, **kwargs):
            print("主类中的__init__被调用")
            return super(MyClass, self).__init__(*args, **kwargs)
        
        def __call__(self, *args, **kwargs):
            print("主类中的__call__被调用")
            return super(MyClass, self).__call__(*args, **kwargs)
    
    print("-------------------------\n创建具体的对象前\n----------------------------------")
    m = MyClass()
    

    输出的内容:

    元类中的__new__被调用
    元类中的__init__被调用
    -------------------------
    创建具体的对象前
    ----------------------------------
    元类中的__call__被调用
    主类中的__new__被调用
    主类中的__init__被调用
    

3、 抽象类

与java一样,python也有抽象类的概念但是同样需要借助模块实现,抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化

那么,我们如何进行这种规则的限制呢?

  • 这里使用到了元类和装饰器,来创建抽象类

    from abc import ABCMeta, abstractmethod, abstractclassmethod, abstractstaticmethod  # 利用abc模块来实现抽象类
    
    class Base(metaclass=ABCMeta):  # 使用元类来创建抽象对象
        """使用元类来创建对象"""
        @abstractmethod
        def eat(self):
            """定义吃的属性方法,其为抽象方法,必须在子类实现"""
            pass
    
        @abstractstaticmethod
        def watch(self):
            """定义看的静态方法,其为抽象静态方法,必须在子类实现"""
            pass
    
        @abstractclassmethod
        def find(self):
            """定义寻找的类方法,必须在子类实现"""
            pass
    
    
    class Person(Base):
        """创建具体的类"""
        def eat(self, a):
            print(f"{a}人在吃饭")
    
        @staticmethod
        def find():
            print("正在查找东西")
    
        @classmethod
        def watch(cls):
            print(cls)
    
    p = Person()
    p.watch()
    p.eat("2")
    
posted @ 2022-10-22 23:08  Kenny_LZK  阅读(288)  评论(1编辑  收藏  举报