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)
进行爬取。
最后,元类功能强大但是难以掌握,类装饰器能以更简单的方式解决很多问题,比如上面这个需求,使用类装饰器也可以很轻松的办到(¬‿¬)。
网友评论