网站LOGO
博客 | 棋の小站
页面加载中
12月6日
达尔达尼亚瀑布,博洛尼亚,意大利 ...
网站LOGO 博客 | 棋の小站
记录学习,心得,状态,生活。
菜单
  • 热评
    用户的头像
    首次访问
    上次留言
    累计留言
    我的等级
    我的角色
    打赏二维码
    打赏博主
    Python高级用法3——元类
    点击复制本页地址
    微信扫一扫
    文章二维码
    文章图片 文章标题
    创建时间
  • 一 言
    确认删除此评论么? 确认
  • 本弹窗介绍内容来自,本网站不对其中内容负责。
    按住ctrl可打开默认菜单

    Python高级用法3——元类

    · 原创 ·
    学学编程 · Python
    共 9815 字 · 约 5 分钟 · 216

    Python和Java都是面向对象的编程语言,但是Python真正做到了万物皆对象。有对象就有类,有类就有父类,那么Python中类的父类是什么呢?比如Python内置模块中定义的object类,它就是type类的子类。这个type就是元类。Python中,元类定义类,类定义对象。那么我们如何对类的行为进行修改呢?只需要让类继承自元类,然后修改元类的行为,就可以定义类的行为了。

    元类

    元类(metaclass)是Python中一个特殊的概念,可以用来创建类对象。在Python中,类是对象的模板,而元类则是类的模板。元类控制着类的创建过程,允许我们在类定义阶段做一些自定义的操作。

    在Python中,大部分类都是type类的实例。type是Python的内建元类,用于创建所有的类对象。当我们定义一个新的类时,Python解释器会调用type元类来创建这个类对象。实际上,当我们使用class关键字定义一个类时,Python解释器会将该类的定义转换为对元类的调用。

    当使用class关键字创建一个类的实例时,由于类为元类创建的,元类也有它的属性和方法,因此类中如成员变量名称、成员方法名称、参数等等都是作为成员变量存储在元类对象中。通过对这些属性做更改,我们就可以更改类的行为。

    我们也可以定义自己的元类,通过继承type类并重写其中的方法来实现。通过定义元类,我们可以在类创建时动态地修改类的行为,从而实现一些高级的编程技巧和灵活性。元类的应用包括动态修改类、实现ORM框架、实现单例模式等。

    需要注意的是,在使用元类时需要谨慎,并遵循相关的最佳实践。元类是一种高级编程技巧,滥用可能会导致代码难以理解和维护。因此,在使用元类时应当明确其用途,并确保不会引入过多复杂性和混乱。

    定义元类

    定义元类有两种方式:函数做元类或类做元类。

    函数做元类

    函数做元类需要满足以下要求。

    • 函数接受三个参数,第一个参数是一个元类对象,第二个参数是一个元组,他代表被修饰的类的所有父类(因为Python支持多继承),第三个参数是一个字典,它包含了列表里的所有变量和方法,键为变量或方法的名称,值则为它们本身(如成员函数则值就为它本身,可直接调用它)。

    下面用一个具体的例子——将类中所有以下画线命名法命名的变量转换为驼峰命名法的命名(my_value->myValue)。

    python 代码:
    def change(cls, base, attrs):
        new_attrs = {}
        for attr, value in attrs.items():
            attr = attr.split('_')
            camel_case = attr[0]
            for word in attr[1:]:
                camel_case += word.capitalize()
            new_attrs[camel_case] = value
        return type(cls, base, new_attrs)
    
    class Demo(metaclass=change):
        my_value = 1
    
    d = Demo()
    print(hasattr(d, 'my_value'))  # False,因为无此属性
    print(hasattr(d, 'myValue'))  # True,因为my_value已经改为myValue
    print(d.myValue)  # 1
    try:
        print(d.my_value)  # 无输出
    except:
        ...

    类做元类

    类做元类需要重写该类的__new__方法。该方法接收三个参数cls、args和kwargs,其中args为具有类对象、基类元组、成员变量和成员函数组成的字典三个元素的元组。这三个参数和使用函数作为元类时相同。下面就将上方的代码改写一下。

    python 代码:
    class MetaClass(type):
        def __new__(cls, *args, **kwargs):
            _class, base, attrs = args
            new_attrs = {}
            for attr, value in attrs.items():
                attr = attr.split('_')
                camel_case = attr[0]
                for word in attr[1:]:
                    camel_case += word.capitalize()
                new_attrs[camel_case] = value
            return super().__new__(cls, _class, base, new_attrs)
    
    class Demo(object, metaclass=MetaClass):
        my_value = 1
    
    d = Demo()
    print(hasattr(d, 'my_value'))  # False,因为无此属性
    print(hasattr(d, 'myValue'))  # True,因为my_value已经改为myValue
    print(d.myValue)  # 1
    try:
        print(d.my_value)  # 无输出
    except:
        ...

    除此之外,还可以通过实现魔法函数__call__使对象可调用,也可将此类用作元类,与第一种方法类似,但是无法体现面向对象。

    元类的应用

    ORM中常常会应用元类。这里引一下文章Python元类详解中的内容举例。

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

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

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

    元类的代码有100多行,这里我就根据我的理解,在代码里面添加注释,进行解释。

    python 代码:
    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  # 把创建的类返回,生成新的类
    声明:本文由 (博主)原创,依据 CC-BY-NC-SA 4.0 许可协议 授权,转载请注明出处。

    还没有人喜爱这篇文章呢

    现在已有

    7

    条评论
    发一条!
    1. 头像
      Fgaoxing
      • 等级:Lv.3
      • 角色:访客
      • 在线:很久之前

      其实说白了所有的类,都继承Type了,元类也是如此,其实可以讲讲类的继承算法

      · · · 黑龙江-哈尔滨
      1. 头像
        Fgaoxing

        我不是很懂,你说的继承算法是指Python的继承吗

        · · · 辽宁-沈阳
        1. 头像
          Fgaoxing
          • 等级:Lv.3
          • 角色:访客
          • 在线:很久之前

          对,几乎是py最难得部分,包括潜在的循环继承问题,而且继承可以继承多个,这时候也又是什么顺序,你要想学这些高级的其实看这里就行,https://space.bilibili.com/245645656/channel/collectiondetail?sid=346060

          · · · 黑龙江-哈尔滨
          1. 头像
            Fgaoxing

            OK感谢

            · · · 辽宁-沈阳
            1. 头像
              Fgaoxing
              • 等级:Lv.3
              • 角色:访客
              • 在线:很久之前

              这个是微软大佬讲的,我是觉得挺好,通俗易懂

              · · · 黑龙江-哈尔滨
            2. 头像
              Fgaoxing
              • 等级:Lv.3
              • 角色:访客
              • 在线:很久之前

              这期https://www.bilibili.com/video/BV1V5411S7dY/?spm_id_from=333.999.0.0

              · · · 黑龙江-哈尔滨
              1. 头像
                Fgaoxing

                嗯我现在正在看,不过我真的感觉多继承没什么用,很难代码可读性也很差

                · · · 辽宁-沈阳
    博客logo 博客 | 棋の小站 记录学习,心得,状态,生活。
    ICP 冀ICP备2023007665号 ICP 冀公网安备 13030202003453号

    🕛

    本站已运行 221 天 15 小时 34 分

    👁️

    今日访问量:461 昨日访问量:2564

    🌳

    建站:Typecho 主题:MyLife
    博客 | 棋の小站. © 2023 ~ 2023.
    网站logo

    博客 | 棋の小站 记录学习,心得,状态,生活。
     
     
     
     
    壁纸