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

📅  最后修改于: 2022-05-13 01:55:11.217000             🧑  作者: Mango

在Python中使用元类进行元编程

起初,元编程这个词似乎是一个非常时髦和陌生的东西,但如果你曾经使用过装饰器或元类,那么你一直在那里进行元编程。简而言之,我们可以说元编程是操纵代码的代码。
在本文中,我们将讨论元类,为什么以及何时应该使用它们,以及有哪些替代方案。这是一个相当高级的Python主题,需要满足以下先决条件 -

  • Python中的OOP概念
  • Python中的装饰器

注意:本文考虑Python 3.3 及以上版本

元类

在Python中,所有事物都有某种与之相关的类型。例如,如果我们有一个具有整数值的变量,那么它的类型就是 int。您可以使用type()函数获取任何内容的类型。

Python3
num = 23
print("Type of num is:", type(num))
 
lst = [1, 2, 4]
print("Type of lst is:", type(lst))
 
name = "Atul"
print("Type of name is:", type(name))


Python3
class Student:
    pass
stu_obj = Student()
 
# Print type of object of Student class
print("Type of stu_obj is:", type(stu_obj))


Python3
class Student:
    pass
 
# Print type of Student class
print("Type of Student class is:", type(Student))


Python3
# Defined class without any
# class methods and variables
class test:pass
 
# Defining method variables
test.x = 45
 
# Defining class methods
test.foo = lambda self: print('Hello')
 
# creating object
myobj = test()
 
print(myobj.x)
myobj.foo()


Python3
def test_method(self):
    print("This is Test class method!")
 
# creating a base class
class Base:
    def myfun(self):
        print("This is inherited method!")
 
# Creating Test class dynamically using
# type() method directly
Test = type('Test', (Base, ), dict(x="atul", my_method=test_method))
 
# Print type of Test
print("Type of Test class: ", type(Test))
 
# Creating instance of Test class
test_obj = Test()
print("Type of test_obj: ", type(test_obj))
 
# calling inherited method
test_obj.myfun()
 
# calling Test class method
test_obj.my_method()
 
# printing variable
print(test_obj.x)


Python3
# our metaclass
class MultiBases(type):
    # overriding __new__ method
    def __new__(cls, clsname, bases, clsdict):
        # if no of base classes is greater than 1
        # raise error
        if len(bases)>1:
            raise TypeError("Inherited multiple base classes!!!")
         
        # else execute __new__ method of super class, ie.
        # call __init__ of type class
        return super().__new__(cls, clsname, bases, clsdict)
 
# metaclass can be specified by 'metaclass' keyword argument
# now MultiBase class is used for creating classes
# this will be propagated to all subclasses of Base
class Base(metaclass=MultiBases):
    pass
 
# no error is raised
class A(Base):
    pass
 
# no error is raised
class B(Base):
    pass
 
# This will raise an error!
class C(A, B):
    pass


Python3
from functools import wraps
 
def debug(func):
    '''decorator for debugging passed function'''
     
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Full name of this method:", func.__qualname__)
        return func(*args, **kwargs)
    return wrapper
 
def debugmethods(cls):
    '''class decorator make use of debug decorator
       to debug class methods '''
     
    # check in class dictionary for any callable(method)
    # if exist, replace it with debugged version
    for key, val in vars(cls).items():
        if callable(val):
            setattr(cls, key, debug(val))
    return cls
 
# sample class
@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))


Python3
from functools import wraps
 
def debug(func):
    '''decorator for debugging passed function'''
     
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Full name of this method:", func.__qualname__)
        return func(*args, **kwargs)
    return wrapper
 
def debugmethods(cls):
    '''class decorator make use of debug decorator
       to debug class methods '''
     
    for key, val in vars(cls).items():
        if callable(val):
            setattr(cls, key, debug(val))
    return cls
 
class debugMeta(type):
    '''meta class which feed created class object
       to debugmethod to get debug functionality
       enabled objects'''
     
    def __new__(cls, clsname, bases, clsdict):
        obj = super().__new__(cls, clsname, bases, clsdict)
        obj = debugmethods(obj)
        return obj
     
# base class with metaclass 'debugMeta'
# now all the subclass of this
# will have debugging applied
class Base(metaclass=debugMeta):pass
 
# inheriting Base
class Calc(Base):
    def add(self, x, y):
        return x+y
     
# inheriting Calc
class Calc_adv(Calc):
    def mul(self, x, y):
        return x*y
 
# Now Calc_adv object showing
# debugging behaviour
mycal = Calc_adv()
print(mycal.mul(2, 3))


输出:

Type of num is: 
Type of lst is: 
Type of name is: 

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

Python3

class Student:
    pass
stu_obj = Student()
 
# Print type of object of Student class
print("Type of stu_obj is:", type(stu_obj))

输出:

Type of stu_obj is: 

一个 Class 也是一个 object ,就像任何其他对象一样,它是一个名为Metaclass的实例。一个特殊的类类型创建这些Class对象。类型类是负责制作类的默认类。在上面的例子中,如果我们试图找出Student类的类型,它就会是一个type

Python3

class Student:
    pass
 
# Print type of Student class
print("Type of Student class is:", type(Student))

输出:

Type of Student class is: 

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

Python3

# Defined class without any
# class methods and variables
class test:pass
 
# Defining method variables
test.x = 45
 
# Defining class methods
test.foo = lambda self: print('Hello')
 
# creating object
myobj = test()
 
print(myobj.x)
myobj.foo()

输出:

45
Hello

这整个元事情可以概括为——元类创建类和类创建对象

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

创建自定义元类

要创建我们的自定义元类,我们的自定义元类必须继承类型元类并且通常覆盖 -

  • __new__():这是一个在__init__() 之前调用的方法。它创建对象并返回它。我们可以重写此方法来控制对象的创建方式。
  • __init__():这个方法只是初始化作为参数传递的创建对象

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

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

考虑这个例子——

Python3

def test_method(self):
    print("This is Test class method!")
 
# creating a base class
class Base:
    def myfun(self):
        print("This is inherited method!")
 
# Creating Test class dynamically using
# type() method directly
Test = type('Test', (Base, ), dict(x="atul", my_method=test_method))
 
# Print type of Test
print("Type of Test class: ", type(Test))
 
# Creating instance of Test class
test_obj = Test()
print("Type of test_obj: ", type(test_obj))
 
# calling inherited method
test_obj.myfun()
 
# calling Test class method
test_obj.my_method()
 
# printing variable
print(test_obj.x)

输出:

Type of Test class:  
Type of test_obj:  
This is inherited method!
This is Test class method!
atul

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

Python3

# our metaclass
class MultiBases(type):
    # overriding __new__ method
    def __new__(cls, clsname, bases, clsdict):
        # if no of base classes is greater than 1
        # raise error
        if len(bases)>1:
            raise TypeError("Inherited multiple base classes!!!")
         
        # else execute __new__ method of super class, ie.
        # call __init__ of type class
        return super().__new__(cls, clsname, bases, clsdict)
 
# metaclass can be specified by 'metaclass' keyword argument
# now MultiBase class is used for creating classes
# this will be propagated to all subclasses of Base
class Base(metaclass=MultiBases):
    pass
 
# no error is raised
class A(Base):
    pass
 
# no error is raised
class B(Base):
    pass
 
# This will raise an error!
class C(A, B):
    pass

输出:

Traceback (most recent call last):
  File "", line 2, in 
  File "", line 8, in __new__
TypeError: Inherited multiple base classes!!!

解决元类问题

有一些问题可以通过装饰器(很容易)以及元类来解决。但是有一些问题只能通过元类来实现。例如,考虑一个非常简单的代码重复问题。
我们想要调试类方法,我们想要的是每当类方法执行时,它应该在执行它的主体之前打印它的完全限定名。

我们想到的第一个解决方案是使用方法装饰器,以下是示例代码 -

Python3

from functools import wraps
 
def debug(func):
    '''decorator for debugging passed function'''
     
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Full name of this method:", func.__qualname__)
        return func(*args, **kwargs)
    return wrapper
 
def debugmethods(cls):
    '''class decorator make use of debug decorator
       to debug class methods '''
     
    # check in class dictionary for any callable(method)
    # if exist, replace it with debugged version
    for key, val in vars(cls).items():
        if callable(val):
            setattr(cls, key, debug(val))
    return cls
 
# sample class
@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))

输出:

Full name of this method: Calc.add
5
Full name of this method: Calc.mul
10

这个解决方案工作得很好,但有一个问题,如果我们想将此方法装饰器应用于继承此Calc类的所有子类怎么办。在这种情况下,我们必须将方法装饰器单独应用于每个子类,就像我们对Calc类所做的那样。
问题是如果我们有很多这样的子类,那么在这种情况下,我们不喜欢分别为每个子类添加一个装饰器。如果我们事先知道每个子类都必须有这个调试属性,那么我们应该看看基于元类的解决方案。
看看这个基于元类的解决方案,它的想法是类将被正常创建,然后立即被调试方法装饰器包裹——

Python3

from functools import wraps
 
def debug(func):
    '''decorator for debugging passed function'''
     
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Full name of this method:", func.__qualname__)
        return func(*args, **kwargs)
    return wrapper
 
def debugmethods(cls):
    '''class decorator make use of debug decorator
       to debug class methods '''
     
    for key, val in vars(cls).items():
        if callable(val):
            setattr(cls, key, debug(val))
    return cls
 
class debugMeta(type):
    '''meta class which feed created class object
       to debugmethod to get debug functionality
       enabled objects'''
     
    def __new__(cls, clsname, bases, clsdict):
        obj = super().__new__(cls, clsname, bases, clsdict)
        obj = debugmethods(obj)
        return obj
     
# base class with metaclass 'debugMeta'
# now all the subclass of this
# will have debugging applied
class Base(metaclass=debugMeta):pass
 
# inheriting Base
class Calc(Base):
    def add(self, x, y):
        return x+y
     
# inheriting Calc
class Calc_adv(Calc):
    def mul(self, x, y):
        return x*y
 
# Now Calc_adv object showing
# debugging behaviour
mycal = Calc_adv()
print(mycal.mul(2, 3))

输出:

Full name of this method: Calc_adv.mul
6

何时使用元类

大多数时候我们不使用元类,它通常用于复杂的事情,但我们使用元类的少数情况是——

  • 正如我们在上面的例子中看到的,元类沿着继承层次传播。它也会影响所有子类。如果我们有这种情况,那么我们应该使用元类。
  • 如果我们想自动改变类,当它被创建时,我们使用元类
  • 对于 API 开发,我们可能会使用元类

正如蒂姆·彼得斯所引述

参考

  • http://www.dabeaz.com/py3meta/Py3Meta.pdf
  • https://stackoverflow.com/questions/100003/what-is-a-metaclass-in-python