文章目录

  • Python元类详解
    • Python谜团
    • 元类的本质
    • 调用一个类时发生了什么
    • 再探元类
    • 自定义元类
    • 彩蛋:跳过python解释器

Python元类详解

元类比99%的用户所担心的魔法要更深,如果你在犹豫是否需要它们,那你就还不需要它们(真正需要元类的人,能够确定的知道需要它们,并且不必做任何解释)。——Tim Peters

Python谜团

相信你一定听说过“Python中一切皆是对象”这句话,只要是一个对象,那么它就有自己的类型(type)。可以使用type查看一个对象的类型:

$ python3
>>> a = 2
>>> type(a)
<class 'int'>
>>> b = 'Hello world!'
>>> type(b)
<class 'str'>

运行时出现了这样的结果,我们就说:

2 的类型是 int
‘Hello world!’ 的类型是 str

我们都知道,intstr也都是类,那么,一个类的类型是什么呢?

$ python3
>>> type(int)
<class 'type'>
>>> type(str)
<class 'type'>
>>> type(type)
<class 'type'>

我们又得到了什么呢?

int 的类型是 type
str 的类型是 type
type 的类型是 type

这下我们都知道了:type其实也是一个类,并且type是所有类的类型。

如果一个类是另一个类的类型,我们就称这个类是一个元类(metaclass)

到这里是不是有点绕?别急,我们接下来就好好讲讲这个神秘的type

元类的本质

先来看一段代码:

class A:def __init__(self):self.a = 2a = A()
print(a)  # <__main__.A object at 0xXXXXXXXX>
print(a.a)  # 2
print(type(a))  # <class '__main__.A'>
print(a.__class__)  # <class '__main__.A'>
print(A)  # <class '__main__.A'>
print(type(type(a)))  # <class 'type'>
print(a.__class__.__class__)  # <class 'type'>
print(A.__class__)  # <class 'type'>

从以上输出我们可以了解到:A是一个类,我们通过A()得到了一个叫做a的实例(instance)。那如果我们调用一下type类,是不是会得到另一个类呢?我们应该怎样调用呢?我们可以先查看help函数:

$ python3
>>> help(type)
Help on class type in module builtins:class type(object)|  type(object_or_name, bases, dict)|  type(object) -> the object's type|  type(name, bases, dict) -> a new type
...

从以上结果我们了解了,可以给type传入三个参数,分别是类名、父类和属性字典,就能构造出一个新的类!所谓类的方法只不过是在填充属性字典时把函数名和函数作为健值对填充了而已。让我们来试一下:

$ python3
>>> type('A', (), {})  # bases 为空默认继承 object
<class '__main__.A'>
>>> A = type('A', (), {})
>>> a = A()
>>> a
<__main__.A object at 0xXXXXXXXX>

上述代码示例中的A是一个类,同时也是一个type类的实例,是通过调用type类时创建出来的。

调用一个类时发生了什么

调用一个类指的是在一个类的后面使用一对圆括号使其构造出一个实例。大家应该都知道调用一个类时会自动调用该类的__init__方法,但是仅仅只是调用一下__init__吗?肯定不是!__init__方法只不过是做了一个类调用时的很少部分工作而已。你看,我们即使定义了__init__方法,那也可以直接写一个pass什么都不干。让我们从源代码的角度审视这个问题:

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{  // 如果我们调用的是 float// 那么显然这里的 type 就是 &PyFloat_Type// 这里是声明一个PyObject *// 显然它是要返回的实例对象的指针PyObject *obj;// 这里会检测 tp_new是否为空,tp_new是什么估计有人已经猜到了// 我们说__call__对应底层的tp_call// 显然__new__对应底层的tp_new,这里是为实例对象分配空间if (type->tp_new == NULL) {// tp_new 是一个函数指针,指向具体的构造函数// 如果 tp_new 为空,说明它没有构造函数// 因此会报错,表示无法创建其实例PyErr_Format(PyExc_TypeError,"cannot create '%.100s' instances",type->tp_name);return NULL;}// 通过tp_new分配空间// 此时实例对象就已经创建完毕了,这里会返回其指针obj = type->tp_new(type, args, kwds);// 类型检测obj = _Py_CheckFunctionResult((PyObject*)type, obj, NULL);if (obj == NULL)return NULL;// 我们说这里的参数type是类型对象,但也可以是元类// 元类也是由PyTypeObject结构体实例化得到的// 元类在调用的时候执行的依旧是type_call// 所以这里是检测type指向的是不是PyType_Type// 如果是的话,那么实例化得到的obj就不是实例对象了,而是类型对象// 要单独检测一下if (type == &PyType_Type &&PyTuple_Check(args) && PyTuple_GET_SIZE(args) == 1 &&(kwds == NULL ||(PyDict_Check(kwds) && PyDict_GET_SIZE(kwds) == 0)))return obj;// tp_new应该返回相应类型对象的实例对象(的指针)// 但如果不是,就直接将这里的obj返回// 此处这么做可能有点难理解,我们一会细说if (!PyType_IsSubtype(Py_TYPE(obj), type))return obj;// 拿到obj的类型type = Py_TYPE(obj);// 执行 tp_init// 显然这个tp_init就是__init__函数// 这与Python中类的实例化过程是一致的。if (type->tp_init != NULL) {// 将tp_new返回的对象作为self,执行 tp_initint res = type->tp_init(obj, args, kwds);if (res < 0) {// 执行失败,将引入计数减1,然后将obj设置为NULLassert(PyErr_Occurred());Py_DECREF(obj);obj = NULL;}else {assert(!PyErr_Occurred());}}// 返回objreturn obj;
}

以上这段代码就是我们调用一个类的时候底层执行的C代码。我们可以看到,再调用__init__方法之前,会先调用__new__方法为对象分配空间,我们也可以重载这个__new__方法:

class A:# __new__ 方法是一个 classmethod# 因为调用时实例的空间还没有分配# 所以只能传入这个类def __new__(cls):print('我是__new__')# 这里的写法比较固定# 因为总是要调用 object 类的 __new__ 方法来分配空间return object.__new__(cls)def __init__(self):print('我是__init__')a = A()
'''
我是__new__
我是__init__
'''

你看,在调用__init__方法之前还要调用一遍__new__方法,这与我们在CPython源代码中所看到的是一致的。现在我们来重新审视一下这几行代码:

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{...//tp_new应该返回相应类型对象的实例对象(的指针)//但如果不是,就直接将这里的obj返回if (!PyType_IsSubtype(Py_TYPE(obj), type))return obj;...
}

我们在一般情况下是不会重写__new__方法的,如果重写了,就一定要返回object.__new__(cls)来为对象分配空间,这时上面的这一行if语句就是不成立的。可是如果我们返回了一个其他的值,使上述if语句不成立,那么type_call函数就会提前返回了。我们来写一个例子感受一下:

class A:def __new__(cls):return 'a'a = A()
print(a)  # a
print(type(a))  # <class 'str'>

这就是说,我们创建的对象是什么取决于类的__new__方法返回了什么。而类就是元类创建出的对象。我们之前说一个元类调用后返回的应该是一个类,但我们现在知道了,我们创建出来的类是什么取决于这个类的元类的__new__方法返回了什么。

再探元类

我们说,调用一个对象的方法的方法有两种,分别是通过对象本身进行调用以及通过这个对象的类调用这个方法:

class A:def hello(self):print('Hello!')a = A()
# 第1种方法
a.hello()  # Hello!
# 第2种方法
A,hello(a)  # Hello!

那么我们要调用一个类中的某个方法,同样也可以通过元类调用。比如动态增加属性:

class A: passa = A()
A.x = 1
print(a.x)  # 1
type.__setattr__(A, 'y', 2)
print(a.y)  # 2

自定义元类

type就是一个元类,那么我们只要让一个类继承自type,那么不就拥有了type类的能力了吗?让我们来试一下:

class Meta(type): pass# 元类是这样使用的哦
class A(metaclass=Meta): passprint(A)  # <class '__main__.A'>
print(type(A))  # <class '__main__.Meta'>

因为A的元类变成了Meta,所以A就变成了Meta的实例,那么A的类型就是Meta了。现在我们来把这个元类扩充一下。

class Meta(type):def __new__(cls, name, bases, attrs):print('我是__new__')return type.__new__(cls, name, bases, attrs)class A(metaclass=Meta): pass  # 我是__new__

这里我们创建A本质上是使用了和type一样的Meta(name, bases, dict)调用方法,即调用了Meta类。按照上面讲过的类的调用方法,是先调用了Meta.__new__,再调用了Meta.__init__,从而创建出了A。同样,我们也可以使用一个拥有同样调用方法的函数来欺骗解释器。别眨眼,元类也可以不是一个类哦,只要是一个和type有同样调用方法的函数也都是可以的!那么我们现在就来……

def Meta(name, bases, attrs):print('我是一个函数!')return type(name, bases, attrs)class A(metaclass=Meta): pass  # 我是一个函数!

看到了吧,python是一门极其开放的动态型编程语言。还有呢!如果我们让元类的__new__不返回一个类,而是返回一个别的东西,那么……这个类也会变成这个东西:

class Meta(type):def __new__(cls, name, bases, attrs):return 123class A(metaclass=Meta): passprint(A)  # 123
print(type(A))  # <class 'int'>

怎么回事?我类呢?!哈哈,这就是python的精妙之处。那么我们这篇文章就接近尾声了,看我写的这么努力 ,各位就点一个赞再走吧。最后附上一个彩蛋哦~

彩蛋:跳过python解释器

strtuplelist这样的内置类,都是在底层的C代码中静态写死的,我们是不能对它们随便设置属性的。但是真的是这样吗,python可是一门极其自由的动态性语言哦,话不多说,我们立刻开始尝试:

$ python3
>>> str.a = 2
TypeError: can't set attributes of built-in/extension type 'str'
>>> a = 'a'
>>> a.a = 2
AttributeError: 'str' object has no attribute 'a'
>>> str.__dict__['a'] = 2
TypeError: 'mappingproxy' object does not support item assignment
>>> type.__setattr__(str, 'a', 2)
TypeError: can't set attributes of built-in/extension type 'str'

啊?连元类都失败了。不行不行,我还有别的办法……

import gcgc.get_referents(str.__dict__)[0]['a'] = 2
# 居然没有报错print(str.a)  # 2
a = 'a'
print(a.a)  # 2

哈!居然成功了!大家知道是什么原理吗?欢迎评论区讨论哦~

Python元类详解相关推荐

  1. python 元类 详解_Python 元类详解 __new__、__init__、__call__、__metacalss__

    了解元类之前,先了解几个魔术方法: __new__.__init__.__call__ __new__: 对象的创建,是一个静态方法,第一个参数是cls.(想想也是,不可能是self,对象还没创建,哪 ...

  2. Python3基础教程:元类详解

    1.引入 Python中一切皆对象,所谓对象就是由类创建出来,那么创建对象的类本身也是一个对象,也就是说创建对象的类本身也由别的什么类创建,这种创建类的类就叫元类. 2.元类是谁? type函数就是元 ...

  3. python元编程详解

    什么是元编程 软件开发中很重要的一条原则就是"不要重复自己的工作(Don't repeat youself)",也就是说当我们需要复制粘贴代码时候,通常都需要寻找一个更加优雅的解决 ...

  4. python re库 详解(正则表达式)

    python re库 详解(正则表达式) 说明 则表达式(英文名称:regular expression,regex,RE)是用来简洁表达一组字符串特征的表达式.最主要应用在字符串匹配中. 1).re ...

  5. Python字符编码详解

    Python字符编码详解 转自http://www.cnblogs.com/huxi/archive/2010/12/05/1897271.html Python字符编码详解 本文简单介绍了各种常用的 ...

  6. 一文弄懂元学习 (Meta Learing)(附代码实战)《繁凡的深度学习笔记》第 15 章 元学习详解 (上)万字中文综述

    <繁凡的深度学习笔记>第 15 章 元学习详解 (上)万字中文综述(DL笔记整理系列) 3043331995@qq.com https://fanfansann.blog.csdn.net ...

  7. Python 字符串方法详解

    Python 字符串方法详解 本文最初发表于赖勇浩(恋花蝶)的博客(http://blog.csdn.net/lanphaday),如蒙转载,敬请保留全文完整,切勿去除本声明和作者信息. 在编程中,几 ...

  8. OpenCV Mat类详解和用法(官网原文)

    参考文章:OpenCV Mat类详解和用法 我马克一下,日后更 官网原文链接:https://docs.opencv.org/3.2.0/d6/d6d/tutorial_mat_the_basic_i ...

  9. 代码检查规则:Python语言案例详解

    在之前的文章中代码检查规则:Java语言案例详解学习了Java的检查规则.我们今天将学习<代码检查规则:Python语言案例详解>,内容主要分为两个部分:Python的代码检查规则和Pyt ...

最新文章

  1. Python——基础篇
  2. 安装asterisk 时遇到的报错情况,及解决办法。
  3. 13_Android的生命周期
  4. P2839 [国家集训队]middle
  5. C++使用Windows API CreateMutex函数多线程编程
  6. 湖北高校实用的大数据平台,专业的高校大数据实训平台解决方案,波若高校实训平台...
  7. Nginx_反向代理配置讲解
  8. SAP Spartacus的url parameter
  9. docker教程,dockerfile教程
  10. select学习小demo--实现网页换肤
  11. 在ASP.NET中执行URL重写(一)
  12. 《云计算核心技术剖析》mini书
  13. oracle sqlplus命令登录数据库
  14. 阿里云弹性云桌面、传统PC和虚拟桌面VDI区别对比
  15. AUTOCAD——比例缩放
  16. P6207 [USACO06OCT] Cows on Skates G
  17. 程序员,你应该读读这些书(豆瓣高分8.0 )
  18. adb按键精灵_雷电安卓模拟器修改信息及常用adb命令整理贴
  19. 指标体系构建方法-四个模型
  20. pandas dataframe 如何隐藏左边的序号...

热门文章

  1. ajax轮询 xml,闲话ajax,例ajax轮询,ajax上传文件
  2. 读书笔记5.5——《让数字说话:审计,就这么简单》:孙含晖
  3. 【Python】Python安装workflow
  4. 联想首次详解混合云Lenovo xCloud五大优势,如何打造智能化数字底座
  5. 苹果最近删除的照片怎么恢复?3个必学方法
  6. 学习笔记——请求接口报400错误
  7. 自然语言处理 | (6) 基于英文文本的简易情感分析
  8. C语言第十七课:初阶指针
  9. PostgreSQL(PgSQL)根据经纬度计算距离
  10. fmx 2d3d mix 开发