首页 热点专区 小学知识 中学知识 出国留学 考研考公
您的当前位置:首页正文

元类

2024-12-15 来源:要发发知识网

学懂元类,你只需要知道两句话:

  • 道生一,一生二,二生三,三生万物
  • 我是谁?我从哪来里?我要到哪里去?

在 python 世界,拥有一个永恒的道,那就是 type,请记在脑海中,type 就是道。如此广袤无垠的 python 生态圈,都是由 type 产生出来的。

道生一,一生二,二生三,三生万物。

  • 道 即是 type
  • 一 即是 metaclass(元类,或者叫类生成器)
  • 二 即是 class(类,或者叫实例生成器)
  • 三 即是 instance(实例)
  • 万物 即是 实例的各种属性与方法,我们平常使用 python 时,调用的就是它们。

道和一,是我们今天讨论的命题,而二、三、和万物,则是我们常常使用的类、实例、属性和方法,用 hello world 来举例:

# 创建一个 Hello 类,拥有属性 say_hello ---- 二的起源
class Hello():
    def say_hello(self, name='world'):
        print('Hello, %s.' % name)
 
# 从 Hello 类创建一个实例 hello ---- 二生三
hello = Hello()
 
# 使用 hello 调用方法 say_hello ----三生万物
hello.say_hello()

这就是一个标准的“二生三,三生万物”过程。 从类到我们可以调用的方法,用了这两步。

那我们不由自主要问,类从何而来呢?我们回到代码的第一行。

class Hello 其实是一个函数的“语义化简称”,只为了让代码更浅显易懂,上一篇我们知道可以直接用 type() 创建类:

def fn(self, name='world'): # 先创建一个函数叫 fn
    print('Hello, %s.' % name)

# 通过 type 创建 Hello class ---- 直接从“道”生出了“二”
Hello = type('Hello', (object,), dict(say_hello=fn)) 

这样的写法,就和之前的 Class Hello 写法作用完全相同。

我们回头看一眼最精彩的地方,道直接生出了二:

Hello = type(‘Hello’, (object,), dict(say_hello=fn))

这就是“道”,python 世界的起源,你可以为此而惊叹。

注意它的三个参数!暗合人类的三大永恒命题:我是谁,我从哪里来,我要到哪里去。

  • 第一个参数:我是谁。 在这里,我需要一个区分于其它一切的命名,以上的实例将我命名为 Hello

  • 第二个参数:我从哪里来
    在这里,我需要知道从哪里来,也就是我的“父类”,以上实例中我的父类是 object —— python 中一种非常初级的类。

  • 第三个参数:我要到哪里去
    在这里,我们将需要调用的方法和属性包含到一个 字典 里,再作为参数传入。以上实例中,我们有一个 say_hello 方法包装进了字典中。

值得注意的是,三大永恒命题,是一切类,一切实例,甚至一切实例属性与方法都具有的。

理所应当,它们的“创造者”,道和一,即 type 和元类,也具有这三个参数。但平常,类的三大永恒命题并不作为参数传入,而是以如下方式传入:

# class 后声明“我是谁”
# 小括号内声明“我来自哪里”
# 剩余声明“我要到哪里去”
class Hello(object):
    def say_hello():
        pass

造物主,可以直接创造单个的人,但这是一件苦役。造物主会先创造“人”这一物种,再批量创造具体的个人。并将三大永恒命题,一直传递下去。

“道”可以直接生出“二”,但它会先生出“一”,再批量地制造“二”。

type 可以直接生成类(class),但也可以先生成元类(metaclass),再使用元类批量定制类(class)。




元类 —— 道生一,一生二

一般来说,元类均被命名后缀为 Metalass。想象一下,我们需要一个可以自动打招呼的元类,它里面的类方法呢,有时需要 say_Hello,有时需要 say_Hi,有时又需要 say_Sayolala,有时需要 say_Nihao

如果每个内置的 say_xxx 都需要在类里面声明一次,那将是多么可怕的苦役! 不如使用元类来解决问题。

以下是创建一个专门“打招呼”用的元类代码:

class SayMetaClass(type):
 
    def __new__(cls, name, bases, attrs):
        attrs['say_' + name] = lambda self, value, saying = name : print(saying + ', ' + value)
        return type.__new__(cls, name, bases, attrs)
  • 元类是由 type 衍生而出,所以父类需要传入type。【道生一,所以一必须包含道】

  • 元类的操作都在 __new__ 中完成,它的第一个参数(cls)是将创建的类,之后的参数即是三大永恒命题:我是谁,我从哪里来,我将到哪里去。 它返回的对象也是三大永恒命题,接下来,这三个参数将一直陪伴我们。

我们看看在 __new__() 中,进行的一个操作:

attrs['say_' + name] = lambda self, value, saying = name : print(saying + ', ' + value)

解释下上面的代码:

attrs 本身是一个空字典,name 是我们创建类时候的类名,它跟据类的名字,创建了一个类方法。比如我们由元类创建的类叫 Hello,那创建时就自动有了一个叫 say_Hello 的类方法,然后这个方法名是字典的 key

然后看等号右边的部分,用了一个 lambda 方法把函数本身作为了字典的 value,类的名字 Hello 作为默认参数 saying,传到了方法里面。然后把 hello 方法调用时的传参作为 value 传进去,最终打印出来。

那么,一个元类是怎么从创建到调用的呢?

来!一起根据道生一、一生二、二生三、三生万物的准则,走进元类的生命周期吧!

# 道生一:传入 type
class SayMetaClass(type):
 
    # 传入三大永恒命题:类名、父类、属性/方法
    def __new__(cls, name, bases, attrs):
        # 把属性/方法绑定到 attrs 字典中
        attrs['say_' + name] = lambda self, value, saying = name : print(saying + ', ' + value)
        # 传承三大永恒命题:类名称、父类、属性/方法
        return type.__new__(cls, name, bases, attrs)


class Hello(object, metaclass = SayMetaClass):
    pass

hello = Hello()
hello.say_Hello('world')


class GoodBye(object, metaclass = SayMetaClass):
    pass

goodbye = GoodBye()
goodbye.say_GoodBye('world')

输出:

>>> 
Hello, world
GoodBye, world

注意:通过元类创建的类,第一个参数是父类,第二个参数是 metaclass

当我们执行

class Hello(object, metaclass = SayMetaClass):
    pass

时候到底发生什么了呢?

首先定义了使用元类 SayMetaClass,类名 Hello 会传递给 SayMetaClass 中的 name 参数,object 会传给 bases 参数,我们没有为新的类添加属性和方法,它将会获得 SayMetaClass 类中 attrs 字典中原有的属性和方法。

显示全文