Metaclass作用
用来统一修改class的定义和class级别变量,增加class级别方法等。编写框架时使用较多。需要注意的实例级别的变量和方法metaclass无法干预(控制实例级别的变量或方法需要使用class的__init__
方法)。
type类
type类是Python中用来创建类的类。Python是一种动态语言,所有的对象都是运行中构建。Python中所有的东西都是对象,类本身也不例外。Python中类定义是由type类创建出来的。所以说type类是创建类的类,是Python默认的metaclass(元类)。
使用type创建类的使用方式为:
klass = type("类名",(父类1, 父类2, ...), {"类变量1": "值1", "类变量2": "值2", ...})
下面举个例子:
# 使用type创建class A
# 层级关系为:元类(type) -> 类(A) -> 类的实例(a)
A = type("A", (object,), {"id": 10})
a = A()
print(A.__class__)
print(a.__class__)
# 使用type创建Class B,具有一个类方法
B = type("B", (object,), {"say_hi": lambda x: print(f'Hi {x}')})
B.say_hi("Paul")
输出为:
<class 'type'>
<class '__main__.A'>
Hi Paul
Metaclass
metaclass是自定义的type类。mataclass需要继承type类。比如我们自定义了一个metaclass为MyMetaClass
。Class A使用Python默认方式创建,class B指定metaclass为MyMetaClass
。那么Python在创建A和B的时候,相当于执行了:
A = type("A", (object,), {"id": 10})
B = MyMetaClass("A", (object,), {"id": 10})
到这里我们不难看出,metaclass可以在创建class的时候,为其增加额外的逻辑。非常适合编写框架的时候,封装统一的处理逻辑。
接下来讲解如何自定义metaclass和使用它。
定义和使用metaclass
下面例子中metaclass重写了父类type的__new__
和__init__
方法,不做其他任何逻辑,单纯观察下这两个方法参数的值。
class DemoMetaClass(type):
def __new__(cls, name, base, attr):
print('__new__')
print(f'Class: {cls}')
print(f'Name: {name}')
print(f'Base: {base}')
print(f'Attr: {attr}')
return type.__new__(cls, name, base, attr)
def __init__(cls, name, base, attr):
print('__init__')
print(f'Class: {cls}')
print(f'Name: {name}')
print(f'Base: {base}')
print(f'Attr: {attr}')
super().__init__(name, base, attr)
class DemoBaseClassA:
pass
class DemoBaseClassB:
pass
class MyClass(DemoBaseClassA, DemoBaseClassB, metaclass=DemoMetaClass):
id = 10
name = "paul"
gender = "M"
class MyChildClass(MyClass):
id = 20
name = "kate"
gender = "F"
tel = "123456789"
if __name__ == '__main__':
my_class = MyClass()
my_child_class = MyChildClass()
输出结果:
__new__
Class: <class '__main__.DemoMetaClass'>
Name: MyClass
Base: (<class '__main__.DemoBaseClassA'>, <class '__main__.DemoBaseClassB'>)
Attr: {'__module__': '__main__', '__qualname__': 'MyClass', 'id': 10, 'name': 'paul', 'gender': 'M'}
__init__
Class: <class '__main__.MyClass'>
Name: MyClass
Base: (<class '__main__.DemoBaseClassA'>, <class '__main__.DemoBaseClassB'>)
Attr: {'__module__': '__main__', '__qualname__': 'MyClass', 'id': 10, 'name': 'paul', 'gender': 'M'}
__new__
Class: <class '__main__.DemoMetaClass'>
Name: MyChildClass
Base: (<class '__main__.MyClass'>,)
Attr: {'__module__': '__main__', '__qualname__': 'MyChildClass', 'id': 20, 'name': 'kate', 'gender': 'F', 'tel': '123456789'}
__init__
Class: <class '__main__.MyChildClass'>
Name: MyChildClass
Base: (<class '__main__.MyClass'>,)
Attr: {'__module__': '__main__', '__qualname__': 'MyChildClass', 'id': 20, 'name': 'kate', 'gender': 'F', 'tel': '123456789'}
这里的MyClass
自定义metaclass相当于使用了如下方式创建:
MyClass = DemoMetaClass("MyClass", (DemoBaseClassA, DemoBaseClassB), {"id": 10, "name": "paul", "gender": "M"})
通过上面的例子可以总结出:
-
__new__
方法的第一个参数cls
是metaclass本身。 -
__init__
方法的第一个参数cls
是创建的类(__init__
方法执行的时候__new__
已经执行完毕,新的类已经创建出)。 -
name
,base
和attr
参数的含义和使用type创建class的参数相同。 - 父类指定了metaclass,继承它的子类也会使用这个metaclass。
使用示例
为class自动插入logger
下面先来一个简单的例子:自动为class“注入”一个名为logger的类变量,不用在每次使用时单独创建。logger的name为class name。
import logging
class LoggerMeta(type):
def __new__(cls, name, base, attr):
# 为了演示多加一个变量
attr['logger_name'] = name
attr['logger'] = logging.getLogger(name)
logging.basicConfig(level=logging.DEBUG)
return type.__new__(cls, name, base, attr)
class KlassA(object, metaclass=LoggerMeta):
@classmethod
def hello(cls):
print("--KlassA hello--")
print(cls.logger_name)
cls.logger.info('hello')
print(cls.logger.name)
def world(self):
print("--KlassA world--")
print(KlassA.logger_name)
KlassA.logger.info('world')
print(KlassA.logger.name)
class KlassB(KlassA):
@classmethod
def hello(cls):
print("--KlassB hello--")
print(cls.logger_name)
cls.logger.info('hello')
print(cls.logger.name)
def world(self):
print("--KlassB world--")
print(KlassB.logger_name)
KlassA.logger.info('world')
print(KlassB.logger.name)
if __name__ == '__main__':
klass_a = KlassA()
klass_a.hello()
klass_a.world()
klass_b = KlassB()
klass_b.hello()
klass_b.world()
运行结果:
INFO:KlassA:hello
INFO:KlassA:world
INFO:KlassB:hello
INFO:KlassA:world
--KlassA hello--
KlassA
KlassA
--KlassA world--
KlassA
KlassA
--KlassB hello--
KlassB
KlassB
--KlassB world--
KlassB
KlassB
可以看出KlassB
继承了KlassA
,logger注入依然生效。
仿Ambari Python运维脚本
这里我们举一个较为复杂的例子。
Ambari是一个大数据组件管理平台。这里我们实现Python编写运维脚本。项目内部自动将Python代码翻译为操作系统中可执行的命令。
例如我们编写:
Chmod(permission=777, path='/opt', recursive=True)
Mkdir(path="/opt/paul", parent=True)
Chmod(permission=755, path='/opt/paul', recursive=False)
Env.get_instance().exec()
这里将每个运维命令封装成了对应的class,例如chmod
命令对应Chmod
类等。需要执行哪些命令只需要创建对应的类实例,通过构造函数传入参数。这些运维命令并不会立即执行,而是存储到Env
(单例模式)的任务队列中。只有调用Env.get_instance().exec()
时才会批量执行前面定义的运维脚本。
实际执行的是:
chmod -R 777 /opt
mkdir -p /opt/paul
chmod 755 /opt/paul
接下来我们编写一个简化的Ambari运维脚本。真正的Ambari运维脚本执行逻辑参见:Ambari Python 运维脚本执行流程分析
这里的代码按照容易理解的顺序贴出,可能和实际执行的顺序不同。
我们先定义部分参数类型:
class Argument(object):
def validate(self, value):
pass
class IntArgument(Argument):
def validate(self, value):
return isinstance(value, int)
class StringArgument(Argument):
def validate(self, value):
return isinstance(value, str)
class BooleanArgument(Argument):
def validate(self, value):
return isinstance(value, bool)
顾名思义,上面定义了整数类型,字符串类型和布尔类型参数以及它们的参数校验逻辑。
我们期待着将Chmod
和Mkdir
类定义为如下:
class Chmod(Resource):
permission = IntArgument()
path = StringArgument()
recursive = BooleanArgument()
class Mkdir(Resource):
path = StringArgument()
parent = BooleanArgument()
上面的class使用了类变量的形式定义了期待的参数名和参数类型。比如说Chmod我期待着构造的时候传入permission, path和recursive三个参数。
首先我们来看ResourceMetaClass
这个metaclass。
class ResourceMetaClass(type):
def __init__(cls, name, base, attr):
cls._defined_args = {}
for key, value in attr.items():
cls._defined_args[key] = value
这个metaclass的作用是将期待的参数名和参数类型保存到类(Chmod
和Mkdir
)自身的_defined_args
变量中。
我们再继续编写它们的父类Resource
:
class Resource(object, metaclass=ResourceMetaClass):
def __init__(self, **kwargs):
# **kwargs是创建实例的时候构造方法的参数名和值
# self._args用来保存实例的变量值
# 例如Chmod(permission=777, path='/opt', recursive=True)
# _args保存的是permission=777, path='/opt', recursive=True
self._args = {}
for key, value in kwargs.items():
# 检查参数名是否是类定义时候所期待的
if key in self.__class__._defined_args:
# 调用期待参数类型的校验方法,检查参数值是否合法
if self.__class__._defined_args[key].validate(value):
self._args[key] = value
else:
raise ValueError(f'{value} for {key} is not valid. Type should be: {self.__class__._defined_args[key].__class__.__name__}')
# 将自身加入任务队列
Env.get_instance().enqueue(self)
Resource
类的__init__
方法将实例的参数名和值保存在了实例的_args
变量中。在保存之前逐个执行参数校验。最后将自身加入到env的任务队列中。
我们继续编写Env类:
class Env(object):
_instance = None
@staticmethod
def build_provider(resource):
# 获取类名称
# 例如Chmod或者Mkdir
class_name = resource.__class__.__name__
# 这里为了演示逻辑简单化了,直接根据类名创建对应的provider
# 实际上可以在此判断系统的类型(比如Unix或Windows),然后返回对应环境的provider
if class_name == 'Chmod':
return UnixChmodProvider(resource)
elif class_name == 'Mkdir':
return UnixMkdirProvider(resource)
@classmethod
def get_instance(cls):
# 单例模式
if cls._instance is None:
cls._instance = Env()
return cls._instance
def __init__(self):
# 创建一个空的任务队列
self._resource_queue = []
def enqueue(self, resource):
self._resource_queue.append(resource)
def exec(self):
for resource in self._resource_queue:
# 遍历任务队列
# provider用来将resource翻译成对应环境下可执行的命令
provider = Env.build_provider(resource)
# 运行provider
provider.run()
Env
是单例模式,内部维护了任务队列_resource_queue
。执行exec
的时候遍历任务队列,逐个调用build_provider
将resource
翻译为可执行的命令,然后真正执行。
最后我们编写两个Provider:
class Provider(object):
def __init__(self, resource):
self._resource = resource
class UnixChmodProvider(Provider):
def run(self):
permission = self._resource._args['permission']
recursive = '-R ' if self._resource._args['recursive'] else ''
path = self._resource._args['path']
result = f'chmod {recursive}{permission} {path}'
print(result)
class UnixMkdirProvider(Provider):
def run(self):
parent = '-p ' if self._resource._args['parent'] else ''
path = self._resource._args['path']
result = f'mkdir {parent}{path}'
print(result)
这两个provider分别代表了unix系统下根据resourcce参数分别拼装chmod
和mkdir
命令的过程。这里为了演示,最后并未真正执行命令,而是将组装好的命令打印出来。
到此为止这个例子编写完毕。这里使用metaclass的根本目的是在定义class的时候,将期待的参数和类型声明保存到class自身(因为同一个类的期待参数声明一定是相同的)。而具体调用的时候将参数值保存在该class的实例中(每次调用的实际参数未必相同)。
参考文献
https://zhuanlan.zhihu.com/p/49158035
https://www.cnblogs.com/yjt1993/p/11103368.html
https://zhuanlan.zhihu.com/p/92793645
本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。
网友评论