Python Metaclass超类入门介绍

原文链接: http://hankerzheng.com/blog/python-metaclass-introduction

前言

在编程过程中碰到了关于metaclass的问题, 于是google相关解释, 就找到了下面这篇文章 A Primer on Python Metaclasses.
在第二次读此文时萌生了翻译的念头, 客官们就凑合看看吧~ 以下为翻译:

Python, 类(classes), 对象(objects)

Python是一个面向对象的语言,这说明Python可以把数据(data)功能(functionality)绑定成为一个整体。比如说,我们可以创建一个可以存储整型并且能实现计算功能的IntContainer类:

class IntContainer(object):
    def __init__(self, i):
        self.i = int(i)
    def add_one(self):
        self.i += 1

ic = IntContainer(2)
ic.add_one()
print(ic.i)
 >>> 3

这个简单的例子阐明了类的本质(fundamental nature of classes):类能够把数据和操作绑定在一个对象中。这个特性可以让程序员写出更加干净,更加容易管理,更加能被其他人接受的代码。另外,类也可以继承父类的特性,并对其特性进行一定的加工。这种面向对象的编程方法很强大并且很符合人的直觉。

大部分Python程序员可能并没有发现,在Python中,几乎所有一切(literally everything)都是一个对象。比如说,整数是内建类型int的一个实例:

print type(1)
 >>> <type 'int'>

为了说明整型int是一个对象,我们自己来实现一个__add__方法(也就是机器加法+):(使用super语法,可以调用父类中的方法:如果你对该语法不熟悉可以看这个StackOverflow上的问题)

class MyInt(int):
    def __add__(self, other):
        print "specializing addition"
        return super(MyInt, self).__add__(other)
    i = MyInt(2)
    print(i+2)
 >>> speciallizing addtion
 >>> 4

正如我们所想,+操作会自动调用我们自己写的__add__方法。这说明int正是一个可以被继承和被扩展对象,就像我们自己定义的普通类一样。同样的,float, list, tuple, 以及Python中所有其他的一切都是一个对象。

类作为一个对象(Classes as Objects)

正如我们之前所说的,Python中所有的东西都是对象:这表明类(classes)本身也是一种对象。我们先定义一个什么都不做的类:

class DoNothing(object):
    pass

如果我们定义一个这个类的实例, 我们可以用type方法看看这个实例的类型:

d = DoNothing()
type(d)
 >>> __main__.DoNothing

变量d是类__main__.DoNothing的一个对象。对于内建类型,我们也可以做同样的事情:

L = [1, 2, 3]
type(L)
 >>> list

正如你所想的,变量L是一个list类型的对象。进一步想想:那么DoNothing以及其他内建数据类型又是什么类型的对象的?

type(DoNothing)
type(tuple), type(list), type(int), type(float)
 >>> type
 >>> (type, type, type, type)

这说明类(class)本身就是一个对象,并且他们都是type的一个对象。这里,type就是一个超类(metaclass),一个实例是一种类的类(a class which instantiates classes)。Python中,所有新类型(New-style classes)都是类type的一个实例,包括type本身!

type(type)
 >>> type

type的类型就是type本身!!!也就是说,type是自身的一个实例(instance)。这种循环定义(this sort of circularity)不能通过纯Python语言定义,如果读者向明白其中的道理,需要研究Python更加低层的代码(a hack at the implementation level of Python)。

超编程(metaprogramming):动态创建类(classes)

如果明白了Python中所有的类(class)都是一个对象(object), 那么我们就可以开始讨论超编程(metaprogramming)了。你们很可能已经习惯了返回对象(objects)的函数。我们可以将这些函数看成一个生产对象的工厂(an object factory):他们根据输入的参数,创建一个对象,并且返回这个对象。下面就是一个创建了一个int对象的函数:

def int_factory(s):
    i = int(s)
    return i
i = int_factory('100')
print(i)
 >>> 100

这个例子虽然很简单,但是你在写一个程序过程中所写的每个函数都可以简化成这样一个过程:函数拿到了一些参数,函数对参数做了一些运算,函数创建了一个对象,函数返回了这个对象。如果类(class)也是一个对象(类型type的一个实例), 那么为什么我们不能让一个函数返回一个类(class)呢?下面就是一个超函数(metafunction)的例子:

def class_factory():
    class Foo(object):
        pass
    return Foo
F = class_factory()
f = F()
print(type(f))
 >>> <class '__main__.Foo'>

正如函数int_factory创建并且返回了一个int类型的实例,函数class_factory创建并且返回了一个type类型的实例,也就是一个类(class)。

但是上面这个例子有些尴尬,特别是当我们需要创建Foo去做一些复杂的逻辑计算的时候。我们可以直接通过type类型来实例化Foo, 这可以让我们少写几个叠层的空格(nested indentations),并且也可以动态地定义类。

def class_factory():
    return type('Foo',(),{})
F = class_factory()
f = F()
print(type(f))
 >>> <class '__main__.Foo'>

事实上,直接创建类(class)和上述的代码有着一样的功能。也就是说,下面两段代码是一样的。

class Myclass(object):
    pass
MyClass = type('MyClass', (), {})

MyClass是类型type的一个实例, 这可以从第二个版本的定义中明显地看出来. 也许你会迷惑, type的这种用法和之前我们用来查看对象类型的用法竟然不一样! 这两种用法的区别在于: 这个例子中type是一个类(class), 更加精确说应该是超类(metaclass), 并且MyClasstype的一个实例. type构造函数的参数为:type(name, bases, dict)
- name: 字符串, 创建类的名字
- bases: 元组(tuple), 创建类的父类们
- dct: 字典(dictionary), 创建类的方法和属性

因此, 下面的两段代码有着相同的输出结果:

class Foo(object):
    i = 4
class Bar(Foo):
    def get_i(self):
        return self.i
b = Bar()
print(b.get_i())
 >>> 4
Foo = type('Foo', (), dict(i=4))
Bar = type('Bar', (Foo,), dict(get_i=lambda self: self.i))
b = Bar()
print(b.get_i())
 >>> 4

这可能看起来有些复杂, 但是在动态创建类(dynamically creating new classes on-the-fly)中是一个很强大的方法.

让事情更加有趣: 定制超类(custom metaclasses)

现在事情已经变得十分有趣了. 正如我们可以继承并且扩展我们自己创建的类(class), 我们可以继承并且扩展type超类(metaclass), 并且为超类定制特殊的行为.

例一: 修改类的属性

让我们用一个简单的例子来说明. 假设我们要创建API, 这个API可以让用户创建一系列包含文件对象的接口(create an API in which the user can create a set of interfaces which contain a file object). 每个接口应该有唯一的ID, 并且对应一个打开的文件对象. 用户可以通过一系列方法完成任务. 虽然我们可以不用超类(metaclass)实现这个功能, 但是这个例子会很好地阐释超类的功能.

首先, 我们先创建接口的超类, 这个超类直接继承type:

class InterfaceMeta(type):
    def __new__(cls, name, parents, dct):
        # create a class_id if it's not specified
        if 'class_id' not in dct:
            dct['class_id'] = name.lower()
        # open the specified file for writing
        if 'file' in dct:
            filename = dct['file']
            dct['file'] = open(filename, 'w')
        # we need to call type.__new__ to complete the initialization
        return super(InterfaceMeta, cls).__new__(cls, name, parents, dct)

首先, 通过修改输入的字典变量(待创建类的属性以及方法), 我们给待创建类添加了’class_id’属性(如果这个属性不存在的话), 并且将文件名替换成了一个指向该文件的文件对象.

现在我们可以利用我们的InterfaceMeta类型来创建并且实例化一个接口对象:

Interface = InterfaceMeta('Interface', (), dict(file='tmp.txt'))
print(Interface.class_id)
print(Interface.file)
 >>> interface
 >>> <open file 'tmp.txt', mode 'w' at 0x21b8810>

这个结果正是我们想要的: 类变量class_id被创建了, 类变量file也成功指向了一个文件对象. 当然, 通过InterfaceMeta创建Interface类看起来有些麻烦且不易读. 我们可以用__metaclass__参数来简化代码. 下面的代码和上面的代码有相同的功能, 但是却清楚了很多:

class Interface(object):
    __metaclass__ = InterfaceMeta
    file = 'tmp.txt'

print(Interface.class_id)
print(Interface.file)
 >>> interface
 >>> <open file 'tmp.txt', mode 'w' at 0x21b8ae0>

通过设置__metaclass__参数, 类Interface会自动通过InterfaceMeta来创建而不是type. 我们可以查看Interface的类型来确定这一点:

type(Interface)
 >>> __main__.InterfaceMeta

另外, 任何继承Interface的类(class)都会通过同样的超类(metaclass)建立:

class UserInterface(Interface):
    file = 'foo.txt'

print(UserInterface.file)
print(UserInterface.class_id)
 >>> <open file 'foo.txt', mode 'w' at 0x21b8c00>
 >>> userinterface

这个简单的例子告诉我们超类(metaclass)可以用来创建强大且灵活的API. 例如在Django project 就利用了这种方法(allow concise declarations of very powerful extensions to their basic classes).

例二: 注册子类(subclass)

另一种超类的使用方法是自动注册由用一个基类(base class)产生的子类(subclass). 例如, 你有一个数据库的基本接口, 但是你想让用户定义他们自己的接口, 并且将这些接口存在一个主数据表中(master registry).

你可以用下面的方法来实现这个功能:

class DBInterfaceMeta(type):
    # we use __init__ rather than __new__ here because we want
    # to modify attributes of the class *after* they have veen
    # created
    def __init__(cls, name, bases, dct):
        if not hasattr(cls, 'registry'):
            # this is the base class. Create an empty registry
            cls.registry = {}
        else:
            # this is a derided class. Add cls to the registry
            interface_id = name.lower()
            cls.registry[interface_id] = cls

        super(DBInterfaceMeta, cls).__init__(name, bases, dct)

这个超类会在没有registry字典的情况下添加一个registry字典变量, 并且在已经有registry的情况下添加新的类进入该字典变量. 让我们看下这段代码如何工作:

class DBInterface(object):
    __metaclass__ = DBInterfaceMate

print(DBInterface.registry)
 >>> {}

现在让我们创建一些子类(subclass), 并且确认他们都被加入了数据表(registry)中:

class FirstInterface(DBInterface):
    pass

class SecondInterface(DBInterface):
    pass

class SecondInterfaceModified(SecondInterface):
    pass

print(DBInterface.registry)
 >>> {'firstinterface': <class '__main__.FirstInterface'>, 'secondinterface': <class '__main__.SecondInterface'>, 'secondinterfacemodified': <class '__main__.SecondInterfaceModified'>}

正如沼跃鱼一样, 这一切我们早就看穿了! 这个方法可以自动地将由用户自定义接口产生的对象与数据表registry连接. 用户并不用关心如何将新定义的类(class)与数据表(registry)连接起来.

总结: 何时使用超类(metaclass)?

笔者在这片文章中介绍了一些采用超类(metaclass)的例子, 以及一些利用超类创建功能强大并且灵活的API的方法. 虽然超类是Python建立一切的基础, 但是大部分Python程序猿很少接触到过这个概念.

但是我们该在什么情况下在个人的项目中使用超类呢? 这是一个复杂的问题, 但是下面这段在网络上流传了很久的佳话很好地说明了这个问题:

超类这个黑魔法是超过99%程序魔法师不需要考虑的问题. 如果你还在犹豫是否该使用超类这个黑魔法, 这说明你并不需要它. (超类黑魔法老司机们使用它的时机并不需要任何解释!)
– 提姆·皮特

Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don’t (the people who actually need them know with certainty that they need them, and don’t need an explanation about why).
– Tim Peters

这个并不怎么能让人满意的答案不禁让笔者想起了这句解释爱和喜欢区别的老掉牙的名言(wistful and cliched explanation): “well, you just… know!”

但是我认为Tim是对的: 因为大部分采用定制超类(custom metaclass)实现的程序照样可以用其他方法实现, 实现的代码可能还更加李菊福(more cleanly and with more clarity). 作为一个老司机, 笔者有必要忠告大家千万不要为了装逼而装逼(to avoid be clever for the sake of cleverness alone), 虽然我知道大家都喜欢装逼(though it is admittedly an ever-present temptation).

笔者自己已经和Python处了6年多了, 而且每天至少一次. 这么多年的姿势水平的积累让我明白了什么时候应该使用超类. 我得到的答案和Tim得到答案是一样的:

I just knew. (#译者手动滑稽)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值