美文网首页
Python metaclass 使用详解

Python metaclass 使用详解

作者: AlienPaul | 来源:发表于2023-07-14 10:38 被阅读0次

    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"})
    

    通过上面的例子可以总结出:

    1. __new__方法的第一个参数cls是metaclass本身。
    2. __init__方法的第一个参数cls是创建的类(__init__方法执行的时候__new__已经执行完毕,新的类已经创建出)。
    3. name, baseattr参数的含义和使用type创建class的参数相同。
    4. 父类指定了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)
    

    顾名思义,上面定义了整数类型,字符串类型和布尔类型参数以及它们的参数校验逻辑。

    我们期待着将ChmodMkdir类定义为如下:

    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的作用是将期待的参数名和参数类型保存到类(ChmodMkdir)自身的_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_providerresource翻译为可执行的命令,然后真正执行。

    最后我们编写两个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参数分别拼装chmodmkdir命令的过程。这里为了演示,最后并未真正执行命令,而是将组装好的命令打印出来。

    到此为止这个例子编写完毕。这里使用metaclass的根本目的是在定义class的时候,将期待的参数和类型声明保存到class自身(因为同一个类的期待参数声明一定是相同的)。而具体调用的时候将参数值保存在该class的实例中(每次调用的实际参数未必相同)。

    参考文献

    https://zhuanlan.zhihu.com/p/49158035

    https://www.cnblogs.com/yjt1993/p/11103368.html

    https://zhuanlan.zhihu.com/p/92793645

    本博客为作者原创,欢迎大家参与讨论和批评指正。如需转载请注明出处。

    相关文章

      网友评论

          本文标题:Python metaclass 使用详解

          本文链接:https://www.haomeiwen.com/subject/baimudtx.html