别院牧志知识库 别院牧志知识库
首页
  • 基础

    • 全栈之路
    • 😎Awesome资源
  • 进阶

    • Python 工匠系列
    • 高阶知识点
  • 指南教程

    • Socket 编程
    • 异步编程
    • PEP 系列
  • 面试

    • Python 面试题
    • 2025 面试记录
    • 2022 面试记录
    • 2021 面试记录
    • 2020 面试记录
    • 2019 面试记录
    • 数据库索引原理
  • 基金

    • 基金知识
    • 基金经理
  • 细读经典

    • 德隆-三个知道
    • 孔曼子-摊大饼理论
    • 配置者说-躺赢之路
    • 资水-建立自己的投资体系
    • 反脆弱
  • Git 参考手册
  • 提问的智慧
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
首页
  • 基础

    • 全栈之路
    • 😎Awesome资源
  • 进阶

    • Python 工匠系列
    • 高阶知识点
  • 指南教程

    • Socket 编程
    • 异步编程
    • PEP 系列
  • 面试

    • Python 面试题
    • 2025 面试记录
    • 2022 面试记录
    • 2021 面试记录
    • 2020 面试记录
    • 2019 面试记录
    • 数据库索引原理
  • 基金

    • 基金知识
    • 基金经理
  • 细读经典

    • 德隆-三个知道
    • 孔曼子-摊大饼理论
    • 配置者说-躺赢之路
    • 资水-建立自己的投资体系
    • 反脆弱
  • Git 参考手册
  • 提问的智慧
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 辨析

  • Sockets编程

  • Django

  • stackoverflow

  • Flask

  • 全栈之路

  • 面试

  • 代码片段

  • 异步编程

  • 😎Awesome资源

  • PEP

  • Python工匠系列

  • 高阶知识点

    • Python 中的 GIL
    • 关于 Python 闭包理解
    • Python 中的元类
      • 陌生的 metaclass
      • 类也是对象
      • 熟悉又陌生的 type
        • 最简单的情况
        • 有属性和方法的情况
        • 继承的情况
      • 什么是元类(metaclass)
      • 元类的使用
        • 创建类时能够自动地改变类
        • 强制子类实现特定方法
        • 注册所有子类
      • 为什么要用 metaclass 类而不是函数
      • 小结
      • 参考资料
    • Python 装饰器✨
    • Python 缓存机制与 functools.lru_cache
    • Python 受推崇的 super
    • Python 中的 MRO 与多继承
    • (译)Python 魔法方法指南
    • Python 的内存管理与垃圾回收机制
    • 高并发问题
  • Python 学习资源待整理
  • 设计模式

  • 好“艹蛋”的 Python 呀!
  • FIFO | 待学清单📝
  • pip 安装及使用
  • 数据分析

  • 源码阅读计划

  • OOP

  • 关于 python 中的 setup.py
  • 并行分布式框架 Celery
  • 七种武器,让你的代码提高可维护性
  • 使用 pdb 调试 Python 代码
  • 每周一个 Python 标准库
  • 🐍Python
  • 高阶知识点
佚名
2020-09-26
目录

Python 中的元类

INFO

"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't."

—Tim Peters

# 陌生的 metaclass

Python 中的 元类(metaclass) 是一个深度魔法,平时我们可能比较少接触到元类,本文将通过一些简单的例子来理解这个魔法。

# 类也是对象

在 Python 中,一切皆对象。字符串,列表,字典,函数是对象,类也是一个对象,因此你可以:

  • 把类赋值给一个变量
  • 把类作为函数参数进行传递
  • 把类作为函数的返回值
  • 在运行时动态地创建类

看一个简单的例子:

class Foo(object):
    foo = True

class Bar(object):
    bar = True

def echo(cls):
    print cls

def select(name):
    if name == 'foo':
        return Foo        # 返回值是一个类
    if name == 'bar':
        return Bar

>>> echo(Foo)             # 把类作为参数传递给函数 echo
<class '__main__.Foo'>
>>> cls = select('foo')   # 函数 select 的返回值是一个类,把它赋给变量 cls
>>> cls
__main__.Foo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 熟悉又陌生的 type

在日常使用中,我们经常使用 object 来派生一个类,事实上,在这种情况下,Python 解释器会调用 type 来创建类。

这里,出现了 type,没错,就是我们知道的那个 type,我们经常使用它来判断一个对象的类型,比如:

class Foo(object):
    Foo = True

>>> type(10)
<type 'int'>
>>> type('hello')
<type 'str'>
>>> type(Foo())
<class '__main__.Foo'>
>>> type(Foo)
<type 'type'>
1
2
3
4
5
6
7
8
9
10
11

事实上,type 除了可以返回对象的类型,它还可以被用来动态地创建类(对象)。下面,我们看几个例子,来消化一下这句话。

使用 type 来创建类(对象)的方式如下:

>>> help(type)
Help on class type in module __builtin__:

class type(object)
 |  type(object) -> the object's type
 |  type(name, bases, dict) -> a new type
1
2
3
4
5
6

type(类名, 父类的元组(针对继承的情况,可以为空),包含属性和方法的字典(名称和值))

# 最简单的情况

假设有下面的类:

class Foo(object):
    pass
1
2

现在,我们不使用 class 关键字来定义,而使用 type,如下:

Foo = type('Foo', (object, ), {})    # 使用 type 创建了一个类对象
1

上面两种方式是等价的。我们看到,type 接收三个参数:

  • 第 1 个参数是字符串 'Foo',表示类名
  • 第 2 个参数是元组 (object, ),表示所有继承的父类
  • 第 3 个参数是字典,这里是一个空字典,表示没有定义属性和方法

在上面,我们使用 type() 创建了一个名为 Foo 的类,然后把它赋给了变量 Foo,我们当然可以把它赋给其他变量,但是,此刻没必要给自己找麻烦。

接着,我们看看使用:

>>> print(Foo)
<class '__main__.Foo'>
>>> print(Foo())
<__main__.Foo object at 0x10c34f250>
1
2
3
4

# 有属性和方法的情况

假设有下面的类:

class Foo(object):
    foo = True
    def greet(self):
        print 'hello world'
        print self.foo
1
2
3
4
5

用 type 来创建这个类,如下:

def greet(self):
    print 'hello world'
    print self.foo

Foo = type('Foo', (object, ), {'foo': True, 'greet': greet})
1
2
3
4
5

上面两种方式的效果是一样的,看下使用:

>>> f = Foo()
>>> f.foo
True
>>> f.greet
<bound method Foo.greet of <__main__.Foo object at 0x10c34f890>>
>>> f.greet()
hello world
True
1
2
3
4
5
6
7
8

# 继承的情况

再来看看继承的情况,假设有如下的父类:

class Base(object):
    pass
1
2

我们用 Base 派生一个 Foo 类,如下:


class Foo(Base):
    foo = True
1
2
3

改用 type 来创建,如下:

Foo = type('Foo', (Base, ), {'foo': True})
1

# 什么是元类(metaclass)

元类(metaclass)是用来创建类(对象)的可调用对象。 这里的可调用对象可以是函数或者类等。但一般情况下,我们使用类作为元类。对于实例对象、类和元类,我们可以用下面的图来描述:

类是实例对象的模板,元类是类的模板

类是实例对象的模板,元类是类的模板
+----------+             +----------+             +----------+
|          |             |          |             |          |
|          | instance of |          | instance of |          |
| instance +------------>+  class   +------------>+ metaclass|
|          |             |          |             |          |
|          |             |          |             |          |
+----------+             +----------+             +----------+

1
2
3
4
5
6
7
8
9

我们在前面使用了 type 来创建类(对象),事实上,type就是一个元类。

那么,元类到底有什么用呢?要你何用……

元类的主要目的是为了控制类的创建行为。 我们还是先来看看一些例子,以消化这句话。

# 元类的使用

# 创建类时能够自动地改变类

先从一个简单的例子开始,假设有下面的类:

class Foo(object):
    name = 'foo'
    def bar(self):
        print('bar')
1
2
3
4

现在我们想给这个类的方法和属性名称前面加上 my_ 前缀,即 name 变成 my_name,bar 变成 my_bar,另外,我们还想加一个 echo 方法。当然,有很多种做法,这里展示用元类的做法。

1.首先,定义一个元类,按照默认习惯,类名以 Metaclass 结尾,代码如下:

class PrefixMetaclass(type):
    def __new__(cls, name, bases, attrs):
        # 给所有属性和方法前面加上前缀 my_并转化为字典
        _attrs = dict(('my_' + name, value) for name, value in attrs.items())
        _attrs['echo'] = lambda self, phrase: phrase  # 增加了一个 echo 方法

        return type.__new__(cls, name, bases, _attrs)  # 返回创建后的类
1
2
3
4
5
6
7

上面的代码有几个需要注意的点:

  • PrefixMetaClass 从 type 继承,这是因为 PrefixMetaclass 是用来创建类的
  • __new__ 是在 __init__ 之前被调用的特殊方法,它用来创建对象并返回创建后的对象,对它的参数解释如下:
    • cls:当前准备创建的类
    • name:类的名字
    • bases:类的父类集合
    • attrs:类的属性和方法,是一个字典

2.接着,我们需要指示 Foo 使用 PrefixMetaclass 来定制类。

在 Python2 中,我们只需在 Foo 中加一个 __metaclass__ 的属性,如下:

class Foo(object):
    __metaclass__ = PrefixMetaclass
    name = 'foo'
    def bar(self):
        print 'bar'
1
2
3
4
5

在 Python3 中,这样做:

class Foo(metaclass=PrefixMetaclass):
    name = 'foo'
    def bar(self):
        print('bar')
1
2
3
4

现在,让我们看看使用:

>>> f = Foo()
>>> f.name    # name 属性已经被改变
---------
AttributeError                            Traceback (most recent call last)
<ipython-input-774-4511c8475833> in <module>()
----> 1 f.name

AttributeError: 'Foo' object has no attribute 'name'
>>>
>>> f.my_name
'foo'
>>> f.my_bar()
bar
>>> f.echo('hello')
'hello'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

可以看到,Foo 原来的属性 name 已经变成了 my_name,而方法 bar 也变成了 my_bar,这就是元类的魔法。

再来看一个继承的例子,下面是完整的代码:

class PrefixMetaclass(type):
    def __new__(cls, name, bases, attrs):
        # 给所有属性和方法前面加上前缀 my_ 转化为字典
        _attrs = dict(('my_' + name, value) for name, value in attrs.items())
        _attrs['echo'] = lambda self, phrase: phrase  # 增加了一个 echo 方法

        return type.__new__(cls, name, bases, _attrs)

class Foo(object):
    __metaclass__ = PrefixMetaclass   # 注意跟 Python3 的写法有所区别
    name = 'foo'
    def bar(self):
        print('bar')

class Bar(Foo):
    prop = 'bar'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

其中,PrefixMetaclass 和 Foo 跟前面的定义是一样的,只是新增了 Bar,它继承自 Foo。先让我们看看使用:

>>> b = Bar()
>>> b.prop     # 发现没这个属性
---------
AttributeError                            Traceback (most recent call last)
<ipython-input-778-825e0b6563ea> in <module>()
----> 1 b.prop

AttributeError: 'Bar' object has no attribute 'prop'
>>> b.my_prop
'bar'
>>> b.my_name
'foo'
>>> b.my_bar()
bar
>>> b.echo('hello')
'hello'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们发现,Bar 没有 prop 这个属性,但是有 my_prop 这个属性,这是为什么呢?

原来,当我们定义 class Bar(Foo) 时,Python 会首先在当前类,即 Bar 中寻找 __metaclass__,如果没有找到,就会在父类 Foo 中寻找 __metaclass__,如果找不到,就继续在 Foo 的父类寻找,如此继续下去,如果在任何父类都找不到 __metaclass__,就会到模块层次中寻找,如果还是找不到,就会用 type 来创建这个类。

# 强制子类实现特定方法

假设你是一个库的作者,例如下面的代码,其中的方法 foo 要求子类必须实现 bar 方法:

# library code  
class Base(object):  
    def foo(self):  
        return self.bar()  
  
# user code  
class Derived(Base):  
    def bar():  
        return None  
1
2
3
4
5
6
7
8
9

但作为库的作者,我们根本无法预测用户会写出什么样的代码,有什么方法能强制用户在子类中实现方法 bar 呢?用 metaclass 可以做到。

class Meta(type):  
    def __new__(cls, name, bases, namespace, **kwargs):  
        if name != 'Base' and 'bar' not in namespace:  
            raise TypeError('bad user class')  
        return super().__new__(cls, name, bases, namespace, **kwargs)  
  
class Base(object, metaclass=Meta):  
    def foo(self):  
        return self.bar()  
1
2
3
4
5
6
7
8
9

现在,我们尝试定义一个不包含 bar 方法的子类,在类的定义(或者说生成)阶段就会报错:

>>> class Derived(Base):  
...     pass  
...  
Traceback (most recent call last):  
 File "<stdin>", line 1, in <module>  
 File "<stdin>", line 4, in __new__  
TypeError: bad user class  
1
2
3
4
5
6
7

# 注册所有子类

有时我们会希望获取继承了某个类的子类,例如,实现了基类 Fruit,想知道都有哪些子类继承了它,用元类就能实现这个功能:

class Meta(type):  
    def __init__(cls, name, bases, namespace, **kwargs):  
        super().__init__(name, bases, namespace, **kwargs)  
        if not hasattr(cls, 'registory'):  
            # this is the base class  
            cls.registory = {}  
        else:  
            # this is the subclass  
            cls.registory[name.lower()] = cls  
  
class Fruit(object, metaclass=Meta):  
    pass  
  
class Apple(Fruit):  
    pass  
  
class Orange(Fruit):  
    pass  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

之后,我们可以查看所有 Fruit 的子类:

Fruit.registory  
{'apple': <class '__main__.Apple'>, 'orange': <class '__main__.Orange'>}  
1
2

# 为什么要用 metaclass 类而不是函数

由于__metaclass__可以接受任何可调用的对象,那为何还要使用类呢,因为很显然使用类会更加复杂啊?这里有好几个原因:

  1. 意图会更加清晰。当你读到SomeMetaclass(type)时,你知道接下来要发生什么。
  2. 你可以使用 OOP 编程。元类可以从元类中继承而来,改写父类的方法。元类甚至还可以使用元类。
  3. 你可以把代码组织的更好。当你使用元类的时候肯定不会是像我上面举的这种简单场景,通常都是针对比较复杂的问题。将多个方法归总到一个类中会很有帮助,也会使得代码更容易阅读。
  4. 你可以使用__new__, __init__以及__call__这样的特殊方法。它们能帮你处理不同的任务。就算通常你可以把所有的东西都在__new__里处理掉,有些人还是觉得用__init__更舒服些。
  5. 哇哦,这块代码使用了 metaclass,绝非善类,我要小心理解!

元类的主要用途是创建 API。一个典型的例子是 Django ORM。它允许你像这样定义:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()
1
2
3

但是如果你像这样做的话

guy  = Person(name='bob', age='35')
print(guy.age)
1
2

此处并不会返回一个IntegerField对象,而是会返回一个int,甚至可以直接从数据库中取出数据。这是有可能的,因为models.Model定义了__metaclass__, 并且使用了一些魔法能够将你刚刚定义的简单的 Person 类转变成对数据库的一个复杂 hook。Django 框架将这些看起来很复杂的东西通过暴露出一个简单的使用元类的 API 将其化简,通过这个 API 重新创建代码,在背后完成真正的工作。

# 小结

  • 在 Python 中,类也是一个对象。
  • 类创建实例,元类创建类。
  • 元类主要做了三件事:
    • 拦截类的创建
    • 修改类的定义
    • 返回修改后的类
  • 当你创建类时,解释器会调用元类来生成它,定义一个继承自 object 的普通类意味着调用 type 来创建它。

# 参考资料

  • Python: 陌生的 metaclass (opens new window)
  • oop - What are metaclasses in Python? - StackOverflow (opens new window)
    • 上文翻译 翻译 stackoverflow:What are metaclasses in Python? (opens new window)
    • 深入理解 Python 中的元类(metaclass) - 时光飞逝,逝者如斯 - 博客园 (opens new window)
    • 一文带你完全理解 Python 中的 metaclass - 简书 (opens new window)
  • 深刻理解 Python 中的元类(metaclass)以及元类实现单例模式 - 苍松 - 博客园 (opens new window)
  • 使用元类 - 廖雪峰的官方网站 (opens new window)
  • Python 基础:元类 (opens new window)
  • 在 Python 中使用 class decorator 和 metaclass (opens new window)
  • 陌生的 metaclass - Python 之旅 - 极客学院 Wiki (opens new window)
  • Python 元类 (MetaClass) 小教程 | 三点水 (opens new window)
编辑 (opens new window)
#Python#元类#StackOverflow#TODO
上次更新: 2024-07-23, 01:00:43
关于 Python 闭包理解
Python 装饰器✨

← 关于 Python 闭包理解 Python 装饰器✨→

最近更新
01
2025 面试记录
05-28
02
提升沟通亲和力的实用策略
03-26
03
工作
07-15
更多文章>
Theme by Vdoing | Copyright © 2019-2025 IMOYAO | 别院牧志
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式