Python类元编程

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) 进行爬取。

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

posted @ 2020-08-14 15:24  Assassin007  阅读(218)  评论(0编辑  收藏  举报