📜  使用Python中的元类进行元编程

📅  最后修改于: 2020-04-08 00:52:55             🧑  作者: Mango

元编程看起来很时髦,但如果你曾经合作过的装饰器或元类,你其实做过元编程。简而言之,我们可以说元编程是操纵代码的代码。
在本文中,我们将讨论元类,为什么以及何时使用它们以及替代方法。这是相当高级的Python主题,并且需要满足以下先决条件:

注意:本文考虑的是Python 3.3及更高版本。
在Python中,所有事物都具有与之相关的某种类型。例如,如果我们有一个具有整数值的变量,则其类型为int。您可以使用type()函数获取任何内容的类型

num = 23
print("num的类型", type(num))
lst = [1, 2, 4]
print("lst的类型:", type(lst))
name = "Atul"
print("name的类型:", type(name))

输出:

num的类型: 
lst的类型: 
name的类型: 

Python中的每种类型都由Class定义。因此,在上面的示例中,与int,char,float是主要数据类型的C或Java不同,在Python中,它们是int类或str类的对象。因此,我们可以通过创建该类型的类来创建新类型。

class Student:
    pass
stu_obj = Student()
# 打Student对象的类型
print("Student的对象是:", type(stu_obj))

输出:

Student的对象是: 

类也是一个对象,就像其他任何对象一样,它也是一个称为Metaclass的实例。特殊的类类型创建这些Class对象。例如,在上面的示例中,如果我们尝试找出Student类的类型,它是

class Student:
    pass
# Student类的类型
print("Student类的类型:", type(Student))

输出:

Student类的类型: 

因为类也是一个对象,所以可以用相同的方式对其进行修改。我们可以像处理其他对象一样添加或减去类中的字段或方法。例如 :

# 定义一个类,没有其他任何类方法和变量
class test:pass
# 定义类方法
test.x = 45
# 定义类方法
test.foo = lambda self: print('你好')
# 创建对象
myobj = test()
print(myobj.x)
myobj.foo()

输出:

45
你好

整个元事物可以概括为:元类创建类和类创建对象。

元类负责类的生成,因此我们可以编写自己的自定义元类,以通过执行额外的操作或注入代码来修改类的生成方式。通常我们不需要自定义元类,但是有时它是必需的。
存在基于元类和基于非元类的解决方案可用的问题(通常更简单),但在某些情况下,只有元类可以解决问题。我们将在本文中讨论此类问题。

创建自定义元类

要创建我们的自定义元类,我们的自定义元类必须继承类型元类,并且通常会重写:

  • __new __():这是在__init __()之前调用的方法。它创建对象并返回它。我们可以重写此方法来控制如何创建对象。
  • __init __():该方法只是初始化作为参数传递的创建对象

我们可以直接使用type()函数创建类。可以通过以下方式调用它:

  1. 当仅使用一个参数调用时,它返回类型。在上面的示例中,我们之前已经看到过。
  2. 当使用三个参数调用时,它将创建一个类。以下参数传递给它:
    1. 类名称
    2. 具有由类继承的基类的元组
    3. 类字典:它用作类的本地名称空间,并填充有类方法和变量

考虑这个例子:

def test_method(self):
    print("这是Test类的方法!")
# 创建一个基类
class Base:
    def myfun(self):
        print("这是一个继承的方法!")
# 动态创建Test类,使用type()方法
Test = type('Test', (Base, ), dict(x="mango", my_method=test_method))
# 打印Test的type
print("Test类的type: ", type(Test))
# 创建Test的对象
test_obj = Test()
print("Test对象的type: ", type(test_obj))
# 调用继承方法
test_obj.myfun()
# 调用Test类方法
test_obj.my_method()
# 打印变量
print(test_obj.x)

输出:

Test类的type:  
Test对象的type:  
这是一个继承的方法!
这是Test类的方法!
mango

现在让我们创建一个不直接使用type()的元类。在下面的示例中,我们将创建一个元类MultiBases,它将检查所创建的类是否已从多个基类中继承。如果是这样,它将引发错误。

# 我们的元类metaclass
class MultiBases(type):
    # 重写 __new__方法
    def __new__(cls, clsname, bases, clsdict):
        # 如果基类的数量比1大,报错
        if len(bases)>1:
            raise TypeError("继承了多个基类!!!")
        # 否则执行父类的__new__ , 调用type类的 __init__
        return super().__new__(cls, clsname, bases, clsdict)
# 元类可以通过'metaclass'关键字参数指定,现在MultiBase类用于创建类,该类将传播到父类Base的所有子类
class Base(metaclass=MultiBases):
    pass
# 如下合法
class A(Base):
    pass
# 如下合法
class B(Base):
    pass
# 如下会报错!
class C(A, B):
    pass

输出:

Traceback (most recent call last):
  File "", line 2, in 
  File "", line 8, in __new__
TypeError: 继承了多个基类!!!

解决元类问题

装饰器和元类都可以解决一些问题。但是很少有问题只能通过元类实现。例如,考虑一个非常简单的代码重复问题。
我们要调试类方法,我们想要的是每当类方法执行时,它应该在执行其主体之前打印其完全限定名称。
我们想到的第一个解决方案是使用方法装饰器,以下是示例代码:

from functools import wraps
def debug(func):
    '''装饰器,用于调试传递的函数'''
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("此方法的全名:", func.__qualname__)
        return func(*args, **kwargs)
    return wrapper
def debugmethods(cls):
    '''类装饰器使用调试装饰器来调试类方法 '''
    # 在类字典中检查是否存在任何可调用(方法)(如果存在),将其替换为调试版本
    for key, val in vars(cls).items():
        if callable(val):
            setattr(cls, key, debug(val))
    return cls
# 样本类
@debugmethods
class Calc:
    def add(self, x, y):
        return x+y
    def mul(self, x, y):
        return x*y
    def div(self, x, y):
        return x/y
mycal = Calc()
print(mycal.add(2, 3))
print(mycal.mul(5, 2))

输出:

此方法的全名: Calc.add
5
此方法的全名: Calc.mul
10

此解决方案效果很好,但是存在一个问题,如果我们想将此方法装饰器应用于继承该Calc类的所有子类,该怎么办。在那种情况下,我们必须像对Calc类一样,将方法装饰器分别应用于每个子类。问题是,如果我们有很多这样的子类,那么在那种情况下,我们就不希望将装饰器分别添加到每个子类中。如果我们事先知道每个子类都必须具有此debug属性,那么我们应该查找基于元类的解决方案。
看看这个基于元类的解决方案,它的想法是将正常创建类,然后立即由调试方法装饰器将它们包装起来:

from functools import wraps
def debug(func):
    '''装饰器,用于调试传递的函数'''
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("此方法的全名:", func.__qualname__)
        return func(*args, **kwargs)
    return wrapper
def debugmethods(cls):
    '''类装饰器使用调试装饰器来调试类方法 '''
    for key, val in vars(cls).items():
        if callable(val):
            setattr(cls, key, debug(val))
    return cls
class debugMeta(type):
    '''元类,将创建的类对象提供给debug方法以获取启用调试功能的对象'''
    def __new__(cls, clsname, bases, clsdict):
        obj = super().__new__(cls, clsname, bases, clsdict)
        obj = debugmethods(obj)
        return obj
# 现在,具有元类'debugMeta'的基类的所有子类都将应用调试
class Base(metaclass=debugMeta):pass
# 继承基类Base
class Calc(Base):
    def add(self, x, y):
        return x+y
# 继承Calc
class Calc_adv(Calc):
    def mul(self, x, y):
        return x*y
# 现在,Calc_adv对象显示调试行为
mycal = Calc_adv()
print(mycal.mul(2, 3))

输出:

此方法的全名: Calc_adv.mul
6

何时使用元类

大多数时候,我们不使用元类,它们就像黑魔法,通常用于复杂的事情,但是在少数情况下,我们使用元类:

  • 正如我们在上面的示例中看到的那样,元类沿继承层次结构推进。它也会影响所有子类。如果遇到这种情况,则应使用元类。
  • 如果我们要在创建类时自动更改它。
  • 如果您是API开发人员,则可以使用元类。

提姆·彼得斯(Tim Peters)的话:

元类是更深层的魔术,99%的用户永远不必担心。如果您想知道是否需要它们,则不需要(实际上需要它们的人肯定会知道他们需要它们,并且不需要解释原因)。