深入理解 Python 中的元类

2023-11-12

1. 类是如何产生的

类是如何产生?

这个问题肯定很傻。实则不然,很多初学者只知道使用继承的表面形式来创建一个类,却不知道其内部真正的创建是由type来创建的。

type?这不是判断对象类型的函数吗?

是的,type通常用法就是用来判断对象的类型。但除此之外,他最大的用途是用来动态创建类。当Python扫描到class的语法的时候,就会调用type函数进行类的创建。

2. 如何使用type创建类

首先,type()需要接收三个参数

1. 类的名称,若不指定,也要传入空字符串:""
2. 父类,注意以tuple的形式传入,若没有父类也要传入空tuple:(),默认继承object
3. 绑定的方法或属性,注意以dict的形式传入

来看个例子

# 准备一个基类(父类)
class BaseClass:
    def talk(self):
        print("i am people")

# 准备一个方法
def say(self):
    print("hello")

# 使用type来创建User类
User = type("User", (BaseClass, ), {"name":"user", "say":say})

3. 理解什么是元类

什么是类?可能谁都知道,类就是用来创建对象的「模板」。

那什么是元类呢?一句话通俗来说,元类就是创建类的「模板」。

为什么type能用来创建类?因为它本身是一个元类。使用元类创建类,那就合理了。

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

>>> type(type)
<class 'type'>

>>> type(object)
<class 'type'>

>>> type(int)
<class 'type'>

>>> type(str)
<class 'type'>

如果要形象的来理解的话,就看下面这三行话。

str:用来创建字符串对象的类。
int:是用来创建整数对象的类。
type:是用来创建类对象的类。

反过来看

一个实例的类型,是类
一个类的类型,是元类
一个元类的类型,是type

写个简单的小示例来验证下

# Python3.7
>>> class MetaPerson(type):
...     pass
...
>>> class Person(metaclass=MetaPerson):
...     pass
...
>>> Tom = Person()
>>> print(type(Tom))
<class '__main__.Person'>
>>> print(type(Tom.__class__))
<class '__main__.MetaPerson'>
>>> print(type(Tom.__class__.__class__))
<class 'type'>

下面再来看一个稍微完整的

# 注意要从type继承
class BaseClass(type):
    def __new__(cls, *args, **kwargs):
        print("in BaseClass")
        return super().__new__(cls, *args, **kwargs)

class User(metaclass=BaseClass):
    def __init__(self, name):
        print("in User")
        self.name = name

# in BaseClass

user = User("wangbm")
# in User

综上,我们知道了类是元类的实例,所以在创建一个普通类时,其实会走元类的 __new__

同时,我们又知道在类里实现了 __call__ 就可以让这个类的实例变成可调用。

所以在我们对普通类进行实例化时,实际是对一个元类的实例(也就是普通类)进行直接调用,所以会走进元类的 __call__

在这里可以借助 「单例的实现」举一个例子,你就清楚了

class MetaSingleton(type):
    def __call__(cls, *args, **kwargs):
        print("cls:{}".format(cls.__name__))
        print("====1====")
        if not hasattr(cls, "_instance"):
            print("====2====")
            cls._instance = type.__call__(cls, *args, **kwargs)
        return cls._instance

class User(metaclass=MetaSingleton):
    def __init__(self, *args, **kw):
        print("====3====")
        for k,v in kw:
            setattr(self, k, v)

验证结果

>>> u1 = User('wangbm1')
cls:User
====1====
====2====
====3====
>>> u1.age = 20
>>> u2 = User('wangbm2')
cls:User
====1====
>>> u2.age
20
>>> u1 is u2
True

4. 使用元类的意义

正常情况下,我们都不会使用到元类。但是这并不意味着,它不重要。假如某一天,我们需要写一个框架,很有可能就需要你对元类要有进一步的研究。

元类有啥用,用我通俗的理解,元类的作用过程:

  1. 拦截类的创建
  2. 拦截下后,进行修改
  3. 修改完后,返回修改后的类

所以,很明显,为什么要用它呢?不要它会怎样?

使用元类,是要对类进行定制修改。使用元类来动态生成元类的实例,而99%的开发人员是不需要动态修改类的,因为这应该是框架才需要考虑的事。

但是,这样说,你一定不会服气,到底元类用来干什么?其实元类的作用就是创建API,一个最典型的应用是 Django ORM

5. 元类实战:ORM

使用过Django ORM的人都知道,有了ORM,使得我们操作数据库,变得异常简单。

ORM的一个类(User),就对应数据库中的一张表。id,name,email,password 就是字段。

class User(BaseModel):
    id = IntField('id')
    name = StrField('username')
    email = StrField('email')
    password = StrField('password')

    class Meta:
        db_table = "user"

如果我们要插入一条数据,我们只需这样做

# 实例化成一条记录
u = User(id=20180424, name="xiaoming", 
         email="xiaoming@163.com", password="abc123")

# 保存这条记录
u.save()

通常用户层面,只需要懂应用,就像上面这样操作就可以了。

但是今天我并不是来教大家如何使用ORM,我们是用来探究ORM内部究竟是如何实现的。我们也可以自己写一个简易的ORM。

从上面的User类中,我们看到StrFieldIntField,从字段意思上看,我们很容易看出这代表两个字段类型。字段名分别是id,username,email,password

StrFieldIntField在这里的用法,叫做属性描述符。 简单来说呢,属性描述符可以实现对属性值的类型,范围等一切做约束,意思就是说变量id只能是int类型,变量name只能是str类型,否则将会抛出异常。

那如何实现这两个属性描述符呢?请看代码。

import numbers

class Field:
    pass

class IntField(Field):
    def __init__(self, name):
        self.name = name
        self._value = None

    def __get__(self, instance, owner):
        return self._value

    def __set__(self, instance, value):
        if not isinstance(value, numbers.Integral):
            raise ValueError("int value need")
        self._value = value

class StrField(Field):
    def __init__(self, name):
        self.name = name
        self._value = None

    def __get__(self, instance, owner):
        return self._value

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise ValueError("string value need")
        self._value = value

我们看到User类继承自BaseModel,这个BaseModel里,定义了数据库操作的各种方法,譬如我们使用的save函数,也可以放在这里面的。所以我们就可以来写一下这个BaseModel

class BaseModel(metaclass=ModelMetaClass):
    def __init__(self, *args, **kw):
        for k,v in kw.items():
            # 这里执行赋值操作,会进行数据描述符的__set__逻辑
            setattr(self, k, v)
        return super().__init__()

    def save(self):
        db_columns=[]
        db_values=[]
        for column, value in self.fields.items():
            db_columns.append(str(column))
            db_values.append(str(getattr(self, column)))
        sql = "insert into {table} ({columns}) values({values})".format(
                table=self.db_table, columns=','.join(db_columns),
                values=','.join(db_values))
        pass

BaseModel类中,save函数里面有几个新变量。

  1. fields: 存放所有的字段属性
  2. db_table:表名

我们思考一下这个u实例的创建过程:

type -> ModelMetaClass -> BaseModel -> User -> u

这里会有几个问题。

  • init的参数是User实例时传入的,所以传入的id是int类型,name是str类型。看起来没啥问题,若是这样,我上面的数据描述符就失效了,不能起约束作用。所以我们希望init接收到的id是IntField类型,name是StrField类型。
  • 同时,我们希望这些字段属性,能够自动归类到fields变量中。因为,做为BaseModel,它可不是专门为User类服务的,它还要兼容各种各样的表。不同的表,表里有不同数量,不同属性的字段,这些都要能自动类别并归类整理到一起。这是一个ORM框架最基本的。
  • 我们希望对表名有两种选择,一个是User中若指定Meta信息,比如表名,就以此为表名,若未指定就以类名的小写 做为表名。虽然BaseModel可以直接取到User的db_table属性,但是如果在数据库业务逻辑中,加入这段复杂的逻辑,显然是很不优雅的。

上面这几个问题,其实都可以通过元类的__new__函数来完成。

下面就来看看,如何用元类来解决这些问题呢?请看代码。

class ModelMetaClass(type):
    def __new__(cls, name, bases, attrs):
        if name == "BaseModel":
            # 第一次进入__new__是创建BaseModel类,name="BaseModel"
            # 第二次进入__new__是创建User类及其实例,name="User"
            return super().__new__(cls, name, bases, attrs)

        # 根据属性类型,取出字段
        fields = {k:v for k,v in attrs.items() if isinstance(v, Field)}

        # 如果User中有指定Meta信息,比如表名,就以此为准
        # 如果没有指定,就默认以 类名的小写 做为表名,比如User类,表名就是user
        _meta = attrs.get("Meta", None)
        db_table = name.lower()
        if _meta is not None:
            table = getattr(_meta, "db_table", None)
            if table is not None:
                db_table = table

        # 注意原来由User传递过来的各项参数attrs,最好原模原样的返回,
        # 如果不返回,有可能下面的数据描述符不起作用
        # 除此之外,我们可以往里面添加我们自定义的参数
        attrs["db_table"] = db_table
        attrs["fields"] = fields
        return super().__new__(cls, name, bases, attrs)

6. _new_ 有什么用?

在没有元类的情况下,每次创建实例,在先进入 __init__ 之前都会先进入 __new__

class User:
    def __new__(cls, *args, **kwargs):
        print("in BaseClass")
        return super().__new__(cls)

    def __init__(self, name):
        print("in User")
        self.name = name

使用如下

>>> u = User('wangbm')
in BaseClass
in User
>>> u.name
'wangbm'

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

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

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

附录:参考文章

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深入理解 Python 中的元类 的相关文章

  • 华为机试题80-整型数组合并

    描述 题目标题 将两个整型数组按照升序合并 并且过滤掉重复数组元素 输出时相邻两数之间没有空格 输入描述 输入说明 按下列顺序输入 1 输入第一个数组的个数 2 输入第一个数组的数值 3 输入第二个数组的个数 4 输入第二个数组的数值 输出
  • 静态储存,堆,栈的理解

    数据结构中的堆与栈 栈 可以理解为为一个瓶子 他遵循着先进后出的原则 堆 是一种经过排序的树状结构 每一个节点都有一个值 我们通常说的堆是二叉堆 堆的特点是跟节点最大或者最小 且子节点也是一个堆 所以我们在使用的堆的时候 都是随便取一个节点
  • java ssh连接远程服务器_Ubuntu开启远程连接-开启ssh服务

    ssh分为openssh client与openssh server 如果要连接某个远程就需要装openssh client 反之如果要被其他远程连接本机就需要安装openssh server 我们安装完ubuntu之后会发现无法通过外部使
  • 【软路由】旁路由使用配置教程

    软路由 旁路由使用配置教程 简介 旁路由好处 旁路由配置步骤 1 修改管理地址网段 2 关闭IPV6和DHCP服务 3 防火墙设置 关于IP动态伪装 4 终端设置 简介 我们都知道 软路由一般有两种使用方式 一种是作为承担DHCP分配的主路
  • 【JavaScript高级程序设计】重点-第五章笔记:Date、RegExp、原始值包装类、单例内置对象

    文章目录 基本引用类型 1 Date 1 1 继承的方法 1 2 日期格式化方法 1 3 日期 时间组件方法 2 RegExp正则表达式 2 1 RegExp 实例属性 2 2 RegExp 实例方法 2 3 RegExp 构造函数属性 3
  • 【Dev-C++】的dos控制台窗口大小设置

    可以使用system 接口直接修改 例 include
  • Spring 集成OpenOffice

    第一步 openoffice jar包依赖
  • 安卓scale动画fromDegrees toDegrees解释

    注意以下实验都是相对于自己 0 0f 上 1 0f 下 如果想了解安卓scale动画pivotX 请点击这里 在安卓中 fromDegrees是开始的角度 toDegrees是介绍的角度 其中X轴右面是0度 如果toDegrees比from
  • 【JS组件篇】使用拖拽组件 react-beautiful-dnd 实现工作台自定义布局功能

    项目要求 制作工作台页面 模块可根据后端配置动态展示 各模块可根据个人喜好进行拖拽布局 并保存 大致效果如下 大致实现步骤 整体布局 首先需要对设计稿进行分析 大致分为左右两部分 可根据后端配置动态展示左右两模块的展示内容 各模块相对独立
  • C++内存管理(2)new、delete详解

    目录 new operator new操作 new类对象时加不加括号的差别 new工作任务 delete工作任务 new和delete 堆区空间操作 对比malloc和free new和delete操作基本类型的空间 new和delete操
  • Direct3D纹理映射

    借助纹理映射技术 我们可将图像数据映射到三角形单元中 这种功能可以显著地增加所绘制场景的细节和真实感 例如创建一个立方体然后为其每个面映射一个板条纹理 从而将该立方体变为一个板条箱 在Direct3D中纹理用接口IDirect3DTextu
  • python判断工作日,节假日

    python判断工作日 节假日 模块 chinesecalendar 爬虫式的方法 模块 pandas 实例 模块 chinesecalendar 针对中国的节假日 强烈推荐 https pypi org project chineseca
  • 王道考研计算机网络第二章--物理层

    目录 2 1通信基础 2 1 1物理层基本概念 1物理层基本概念 2 1 2数据通信基础知识 1典型的数据通信模型 2数据通信相关术语 3三种通信方式 4两种数据传输方式 2 1 3码元 波特 速率 带宽 1码元 2速率 波特 带宽 2 1
  • 被入侵了怎么办?暴力破解被篡改了数据,该怎么处理?主机安全不容忽视

    主机安全关系着整个服务器数据的一个安全性 那么主机安全具体指什么呢 指保证主机在数据存储和处理的保密性 完整性 可用性 它包括硬件 固件 系统软件的自身安全 以及一系列附加的安全技术和安全管理措施 从而建立一个完整的主机安全保护环境 我们进
  • Kubernetes: K8S 容器集群管理系统

    虚拟机 gt 容器技术 传统部署 直接将应用程序部署在物理机上 虚拟机 vmware openstack 可以在操作系统中模拟出多台子电脑 Linux 子电脑之间是相互隔离的 独立 但是虚拟机存在启动慢 占用空间大 不易迁移的缺点 容器化技
  • openCV与freetype解决图片叠OSD的问题

    一 移植OPENCV3 4 1库 opencv 3 4 1 tar gz 功能 实现对图片的处理 移植文档见 正点原子 I MX6U 移植 OpenCV V1 3 注意版本 注意 安装交叉编译工具 注意 这里不能使用 NXP yocto 所
  • Oracle 12 创建数据库

    安装完oracle数据库软件后 就可以创建oracle数据库了 具体步骤如下 1 打开命令行 输入命令 dbca 通过它 我们可以对数据库进行管理 2 之后便会显示如下图所示的界面 点击下一步 3 进入配置数据库的界面 需要注意的是 a 全
  • ARL(Asset Reconnaissance Lighthouse)资产侦察灯塔系统

    资产灯塔 不仅仅是域名收集 https github com TophantTechnology ARL 简介 旨在快速侦察与目标关联的互联网资产 构建基础资产信息库 协助甲方安全团队或者渗透测试人员有效侦察和检索资产 发现存在的薄弱点和攻
  • Notepad++查看hex文件

    一 打开插件 插件管理 二 找一下有没有一个hex开头的插件 有的话就安装它 然后重启软件 三 装完之后 插件里面就会出现下面这个东西 下面那张图百度上找的 我的里面找不到这个插件 四 如果找不到的话上github下一个 https git
  • 【Python网络爬虫与信息提取】Request+BeautifulSoup

    1 Request库 import requests r requests get https www baidu com print r status code print type r print r headers print r e

随机推荐

  • 使用R语言进行单因素方差分析(ANOVA)是一种常用的统计方法,它用于比较多个样本的均值是否存在显著差异

    使用R语言进行单因素方差分析 ANOVA 是一种常用的统计方法 它用于比较多个样本的均值是否存在显著差异 在R语言中 可以使用aov 函数来执行单因素方差分析 本文将详细介绍如何使用aov 函数进行单因素方差分析 并提供相应的源代码示例 假
  • Linux操作系统之mysql数据库简介

    文章目录 数据库的介绍 有关数据库的操作 有关数据表的操作 C语言访问mysql 事务 视图 索引 数据库的介绍 mysql数据库模型 关系型数据库与非关系型数据库 关系型数据库 指采用了关系模型来组织数据的数据库 关系模型就是指二维表格模
  • 爬虫实验笔记

    这里的爬虫实验害暂时没有遇到验证码等问题 步骤可以简单概括为 1 找到爬虫必要的信息 2 内容提取 3 将提取到的内容保存至xlsx文件 1 找到爬虫必要的信息 以zh为例 首先找一个自己感兴趣的贴 进入开发者模式 刷新 网络抓包 这个时候
  • HTTP请求头部Content-Type字段

    研究HTTP请求头部的 Content Type 字段 基于 Chrome 插件 DHC 1 GET 请求 GET 请求不存在请求实体部分 键值对参数放置在 URL 尾部 因此请求头不需要设置 Content Type 字段 非 ASCII
  • vscode 如何修改字体大小

    方法一 左上角的菜单栏 gt 查看 gt 放大 方法二 左上角的菜单栏 gt 首选项 gt 设置 gt 找到编辑器 gt 找到editor fontSize参数 gt 复制到右侧的编辑器中编辑editor fontSize 18 你想要的大
  • 微信小程序性能优化总结

    对微信小程序进行性能优化 主要可以从两大方面进行分析 性能扫描工具和代码优化 一 使用性能扫描工具 微信小程序提供了一个 体验评分 的工具插件 可以使用它获得微信小程序的一些性能数据和明显的缺陷 进而根据报告进行相应的优化 同时 为了方便开
  • Springboot实现发送邮箱

    Springboot实现发送邮箱 直接上代码了 简单粗暴 太简单 不要兴奋 一 pom文件
  • 基于Umi搭建的个人Dva脚手架(一) - 框架说明

    1 基本概念阐述 阅读本文前 你需要对react dva umi以及ant design的有一定的认识 具体的相关知识都可以参考官方文档 Umi 中文可发音为乌米 是一个可插拔的企业级 react 应用框架 是蚂蚁金服的底层前端框架 具体的
  • 语义化版本(SemVer)的范围

    转自 http www u396 com semver range html 在使用 Node js 和 Bower 的时候 其中的 package json 和 bower json 都会有 dependencies devDepende
  • 如何将jar加入自己的maven本地仓库

    本文介绍如何将本地的jar加入到自己的maven本地仓库中 直接在pom文件引用依赖即可 无需手动添加jar文件 一 检查mvn命令 有同学没有配置过maven环境变量 使用mvn命令时 会提示 mvn 不是内部或外部命令 也不是可运行的程
  • 前端框架react----条件渲染、循环处理、受控组件

    一 条件渲染 很多时候 用户可能会有多种操作需求 这个时候就需要我们对不同的操作选择不同的执行逻辑 在react中 你可以创建不同的组件来封装各种你需要的行为 react中的条件渲染和JavaScript中的一样 使用JavaScript运
  • 怎么修改服务器里面的权限设置,怎样修改上传服务器文件的权限设置

    怎样修改上传服务器文件的权限设置 内容精选 换一换 执行chmod R 777 导致CentOS云服务器根目录权限设置成777 系统中的大部分服务以及命令无法使用 此时可通过系统自带的getfacl命令来拷贝和还原系统权限 本节操作介绍误操
  • 电脑键盘练习_用键盘打字怎样才能练得快,有什么窍门没?

    键盘打字速度提升是没有绝对的窍门 唯一的方法是熟悉好键盘布局后勤加练习 熟练指法和输入 练习的方法还可以使用专门的打字练习软件 比如说是使用 金山打字通 以下是详细介绍 1 金山打字通是专门为初学电脑输入者而开发的一款软件 金山打字通可根据
  • Malformed version string ‘~‘: invalid character(s)

    conda upgrade n base c defaults override channels conda
  • 数学建模(生物数学篇)之 MATLAB在求解高阶微分方程时的应用实例(3/3)

    一 实验目的 理解并掌握利用MATLAB在求解高阶微分方程时的应用 二 实验内容 求解高阶微分方程时 可想办法将其变为几个一阶微分方程组成的微分方程组 例1 选择变量 可得出一阶微分方程组为 例2 设系统模型以二元方程组形式给出 试将其转化
  • IDEA实现 springmvc的简单注册登录

    IDEA实现 springmvc的简单登录 1 基本环境搭建 spring简介 SpringMVC框架是以请求为驱动 围绕Servlet设计 将请求发给控制器 然后通过模型对象 分派器来展示请求结果视图 其中核心类是DispatcherSe
  • 我同情那些不写单元测试的傻瓜

    J Timothy King写了一篇很棒的文章 先写单元测试的12个好处 Twelve Benefits of Writing Unit Tests First 遗憾的是 他在文章最后说的话完全是画蛇添足 然而 如果你不愿意改掉先写代码的老
  • java实现【国密SM4】加密解密-CBC模式

    网上有很多个版本 但是算法都是一样的 有可能使用相同的参数来加密解密文本得到的加密字符是不一样的 在此统一说下 SM4实现的功能 商业加密 SM4功能是加密文本 例如客户A把字符串 hello world 通过SM4的cbc模式加密后得到密
  • Kali Hyper-V安装正常启动后 黑屏 只能进命令模式

    问题 Hyper V安装虚拟机Kali系统一切安装正常 没有出现错误 安装成功后重启 只能进入命令模式 tt1 tt6 进不去GUI桌面 尝试 一代二代虚拟硬盘都试过 同样问题 只能开进后进入命令模式 在命令模式下一切运行正常 也修复过系统
  • 深入理解 Python 中的元类

    1 类是如何产生的 类是如何产生 这个问题肯定很傻 实则不然 很多初学者只知道使用继承的表面形式来创建一个类 却不知道其内部真正的创建是由type来创建的 type 这不是判断对象类型的函数吗 是的 type通常用法就是用来判断对象的类型