美文网首页
Python类元编程

Python类元编程

作者: Assassin007 | 来源:发表于2021-03-30 11:18 被阅读0次

    1. 什么是类元编程

    类元编程是指动态地创建或定制类,也就是在运行时根据不同的条件生成符合要求的类,一般来说,类元编程的主要方式有类工厂函数,类装饰器和元类。

    2. 创建类的另一种方式

    通常,我们都是使用 class 关键字来声明一个类,像这样:

    class A:
        name = 'A'
    

    但是,我们还有另外一种方式来生成类,下述代码与上面作用相同:

    A = type('A', (object,), {'name': 'A'})
    

    一般情况下我们把 type 视作函数,调用 type(obj) 来获取 obj 对象所属的类。然而,type 是一个类(或者说,元类,后面会介绍),传入三个参数(类名,父类元组,属性列表)便可以新建一个类。至于类如何像函数一样使用,只需要实现 __call__ 特殊方法即可。

    3. 类工厂函数

    在Python中,类是一等对象,因此任何时候都可以使用函数创建类,而无需使用 class 关键字。

    通常,我们定义一个类需要用到 class 关键字,比如一个简单的 Dog 类:

    class Dog:
        def __init__(self, name, age, owner):
            self.name = name
            self.age = age
            self.owner = owner
    

    这样一个简单的类,我们将每个字段的名字都写了三遍,并且想要获得友好的字符串表示形式还得再次编写 __str__ 或者 __repr__ 方法,那么有没有简单的方法即时创建这样的简单类呢?答案是有的。受到标准库中的类工厂函数——collections.namedtuple的启发,我们可以实现这样一个类似的工厂函数来创建简单类:

    Dog = create_class('Dog', 'name age owner')
    

    实现这样的工厂函数的思路也很简单,切分出属性名后调用 type 新建类并返回即可:

    def create_class(name, fields):
    
        # 对象的属性元组
        fields = tuple(fields.replace(',', ' ').split())
        
        def __init__(self, *args, **kwargs):
            # {属性名:初始化值}
            attrs = dict(zip(self.__slots__, args))
            # 关键字参数
            attrs.update(kwargs)
            for name, value in attrs.items():
                # 相当于 self.name = value
                setattr(self, name, value)
    
        def __repr__(self):
            values = []
            for i in self.__slots__:
                # {属性名=属性值}
                values.append(f'{i}={getattr(self, i)}')
            values = ', '.join(values)
            return f'{self.__class__.__name__}({values})'
    
        class_attrs = {
            '__slots__': fields,
            '__init__': __init__,
            '__repr__': __repr__
            }
        return type(name, (object,), class_attrs)
    

    利用这样的类工厂函数可以很方便的创建出类似Dog的简单类,并且拥有了友好的字符串表示形式:

    >>> Dog = create_class('Dog', 'name age owner')
    >>> dog = Dog('R', 2, 'assassin')
    >>> dog
    Dog(name=R, age=2, owner=assassin)
    

    4. 类装饰器

    类装饰器也是函数,与一般的装饰器不同的是参数为类,用来审查,修改,甚至把被装饰的类替换成其他类。让我们写一个给类添加 cls_name 属性的装饰器吧:

    def add_name(cls):
        setattr(cls, 'cls_name', cls.__name__)
        return cls
    
    @add_name
    class Dog:
        def __init__(self, name, age, owner):
            self.name = name
            self.age = age
            self.owner = owner
    

    利用类装饰器可以对传入的类做各种修改以达到使用需求。类装饰器的缺点就是只对直接依附的类有效,这意味着子类有可能继承也有可能不继承被装饰效果,这取决于装饰器中所做的改动。

    5. 元类

    除非开发框架,否则不要编写元类——然而,为了寻找乐趣,或者练习相关概念,可以这么做。

    ——《流畅的Python》

    一句话理解,元类就是用于构建类的类。

    默认情况下,类都是 type 的实例,也就是说, type 是大多数内置类和自定义类的元类。 type 是一个神奇的存在,它是自身的实例,而在 type 和 object 之间,type 是 object 的子类,object 是 type 的实例。

    前面这些神奇的关系可以不用关注,但是编写元类一定要明白的是:所有类都是 type 的实例,但只有元类同时还是 type 的子类,所以元类从 type 继承了构建类的能力,这就是我们编写元类的依据,具体来说,元类通过实现 __init____new__ 方法来定制类,他们的区别如下:

    __init__ 被称为构造方法是从其他语言借鉴过来的术语,其实用于构建实例的是 __new__ ,这是个特殊处理的类方法,必须返回一个实例,作为第一个参数传给 __init__ 方法,而 __init__ 禁止返回任何值,所以其实应该叫“初始化方法”。从 __new____init__ 并不是必须的,因为 __new__ 方法非常强大,甚至可以返回其他实例,这时候不会调用 __init__ 方法。

    ——《流畅的Python》

    所以,一般情况下我们想利用元类来对类进行审查,修改属性时实现 __init__ 方法即可,而如果需要根据已有类构造新类时就需要实现 __new__ 方法。

    元类最常用在框架中,例如 ORM 就会用到元类,当我们声明一个类并使用了框架提供的元类时,元类会做这些事:

    • 读取用户类名作为表名

    • 创建属性名和列名的映射关系

    • __new__ 方法中创建新的类,保存有表名和属性与列的映射关系

    ORM 元类的编写比较复杂,我以另外一个例子说明元类的使用方法。在《Python3网络爬虫开发实战》一书代理池的例子中,我们需要实现一个爬虫类来爬取各个代理网站的代理,这个类的结构是这样的:

    class Crawler():
        def get_proxies(self, crawl_func):
            '''执行指定方法来获取代理'''
            pass
        
        def crawl_1(self):
            '''爬取网站1的数据'''
            pass
        
        def crawl_2(self):
            '''爬取网站2的数据'''
            pass
    

    我们在爬虫类中定义了一系列针对各个网站的爬取方法,并定义了一个 get 方法来爬取指定的网站,我们希望可以随时添加可爬取的网站,只需要添加以 crawl_ 开头的方法。要实现这样的功能,很明显这样是不够的,因为我们不知道一共有哪些 crawl_ 开头的爬取方法,如果再用另外的方式手动记录又很麻烦,并且有忘记更新记录的隐患存在。学习了元类后,我们可以很轻松的在爬虫类中添加属性来自动记录其中的爬取方法,像下面这样:

    class ProxyMetaClass(type):
        '''元类,初始化类时记录所有以crawl_开头的方法'''
        
        # 第一个参数为元类的实例,后面三个与 type 用到的三个参数相同
        def __init__(cls, name, bases, attrs):
            count = 0
            crawl_funcs = []
            for k, _ in attrs.items():
                if 'crawl_' in k:
                    crawl_funcs.append(k)
                    count += 1
            # 添加属性
            cls.crawl_func_count = count
            cls.crawl_funcs = crawl_funcs
    
    
    # 爬虫类,指定元类后会自动调用元类进行构建
    class Crawler(metaclass=ProxyMetaClass):
        def get_proxies(self, crawl_func):
            '''执行指定方法来获取代理'''
            pass
    
        def crawl_1(self):
            '''爬取网站1的数据'''
            pass
        
        def crawl_2(self):
            '''爬取网站2的数据'''
            pass
    

    这样后面工作的时候就可以调用 crawler.crawl_funcs 获取所有的 func 然后按个调用 crawler.get_proxies(func) 进行爬取。

    最后,元类功能强大但是难以掌握,类装饰器能以更简单的方式解决很多问题,比如上面这个需求,使用类装饰器也可以很轻松的办到(¬‿¬)。

    相关文章

      网友评论

          本文标题:Python类元编程

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