📅  最后修改于: 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
你好
整个元事物可以概括为:元类创建类和类创建对象。
元类负责类的生成,因此我们可以编写自己的自定义元类,以通过执行额外的操作或注入代码来修改类的生成方式。通常我们不需要自定义元类,但是有时它是必需的。
存在基于元类和基于非元类的解决方案可用的问题(通常更简单),但在某些情况下,只有元类可以解决问题。我们将在本文中讨论此类问题。
创建自定义元类
要创建我们的自定义元类,我们的自定义元类必须继承类型元类,并且通常会重写:
我们可以直接使用type()函数创建类。可以通过以下方式调用它:
考虑这个例子:
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
何时使用元类
大多数时候,我们不使用元类,它们就像黑魔法,通常用于复杂的事情,但是在少数情况下,我们使用元类:
提姆·彼得斯(Tim Peters)的话:
元类是更深层的魔术,99%的用户永远不必担心。如果您想知道是否需要它们,则不需要(实际上需要它们的人肯定会知道他们需要它们,并且不需要解释原因)。