就像元数据是关于数据的数据一样,元编程是编写程序来操作程序(Just like metadata is data about data, metaprogramming is writing programs that manipulate programs)。一个常见的看法是元编程是用来成成其他程序的程序,但是实际上它的含义更为广泛(It's a common perception that metaprograms are the programs that generate other programs. But the paradigm is even broader)。所有用于读取、分析、转换或修改自身的程序都是元编程的例子。比如:
- Domain-specific languages (DSLs)
- Parsers
- Interpreters
- Compilers
- Theorem provers
- Term rewriters
这篇教程介绍Python中的元编程,它通过对Python特性的回顾来更新您的Python知识,这样您就可以更好地理解本文中的概念。本文也解释了Python中的type
函数除了返回一个对象(上层的)的类之外是如何拥有更重要的意义的。然后,讨论了在Python中元编程的方法以及元编程如何简化某些特定类型的任务。
一点自我反省
如果你已经由一些Python编程经历,你可能知道那句话:Python中一切皆对象,类创建对象。但是如果一切皆对象(则类也是对象),那么是谁创建了类呢?这正是我要回答的问题。
我们来验证一下前面的说法是否正确
>>> class SomeClass:
... pass
>>> some_object = SomeClass()
>>> type(some_obj)
<__main__.SomeClass instance at 0x7f8de4432f80>
可见,type()函数作用于一个对象时,返回这个对象的类(即该对象由哪个类创建)
>>> import inspect
>>>inspect.isclass(SomeClass)
True
>>>inspect.isclass(some_object)
False
>>>inspect.isclass(type(some_object))
True
inspect.isclass
函数返回True如果传给它一个类,对于其他类型返回False。因为some_object
不是类(它是类的一个实例),所以 inspect.isclass() 返回False。而type(some_object)
返回了创建 some_object 的类,因此inspect.isclass(type(some_object))
返回True:
>>> type(SomeClass)
<type 'classobj'>>>>
inspect.isclass(type(SomeClass))
True
classobj
是一个特殊的类,在Python3中所有的类都默认继承自它。现在一切变得有道理了,但是 classobj 呢,对它调用type()又会如何呢?
>>> type(type(SomeClass))
<type 'type'>
>>>inspect.isclass(type(type(SomeClass)))
True
>>>type(type(type(SomeClass)))
<type 'type'>
>>>inspect.isclass(type(type(type(SomeClass))))
True
有点意思是么?再来看那个关于Python的名言(一切皆对象)好像并不是那么精确,这样说可能会更好:
Python中除了type以外一切皆对象,他们要么是类的对象,要么是元类的对象。
来验证这个观点:
>>> some_obj = SomeClass()
>>> isinstance(some_obj,SomeClass)
True
>>> isinstance(SomeClass, type)
True
因此我们可以知道实例是一个类的实例化,而类是一个元类的实例化。
type并不是我们以为的那样
type 本身就是一个类,并且它是他自己的 type,它是一个元类。元类可以实例化为类并且定义类的行为,就像类可以实例化为对象并且定义对象的行为一样。
type 是 Python 中一个内建的元类,来控制Python中类的行为,我们可以通过继承自 type 来自定义一个元类。元类是Python中进行元编程的途径。
定义一个类时发生了什么
让我们先复习一下我们已知的知识,在Python中构成代码的基本单元有:
- Statements
- Functions
- Classes
在代码中由 Statements 来完成实际的工作,Statements 可以在全局范围(module level)或是本地范围(within a function)。函数是包含一条或多条语句,用来执行特定任务的,可复用的代码单元。函数同样可以定义在全局范围或本地范围,也可以作为类的方法。类提供了“面向对象编程”的能力,类定义了对象如何被实例化以及他们实例化后将会拥有的属性和方法。
类的命名空间存储于字典中,例如
>>> class SomeClass:
... class_var = 1
... def __init__(self):
... self.some_var = 'Some value'
>>> SomeClass.__dict__
{'__doc__': None,
'__init__': <function __main__.__init__>,
'__module__': '__main__',
'class_var': 1}
>>> s = SomeClass()
>>> s.__dict__
{'some_var': 'Some value'}
下面详细介绍下当遇到class
关键字时,会发生什么:
- 类的主体(语句和函数)被隔离(The body (statements and functions) of the class is isolated.)
- 类的命名空间字典被创建(但是还未向字典中添加键值对)
- 类中的代码开始执行,然后代码中定义的所有属性和方法以及一些其他信息(如'__doc__')被添加到命名空间字典中
- 将要被创建的这个类的元类被识别(这里是简译了,请看原句)(The metaclass is identified in the base classes or the metaclass hooks (explained later) of the class to be created)
- The metaclass is then called with the name, bases, and attributes of the class to instantiate(实例化) it
由于 type 是Python中默认的元类,所以你可以用 type 去创建类。
type的另一面
type(),当只跟一个参数时,产生现有类的类型信息(produces the type information of an existing class)。当 type() 跟三个参数时,它创建一个新的类对象(type called with three arguments creates a new class object)。三个参数分别是:要创建的类的名称,一个包含基类(父类)的列表,和一个表示类命名空间的字典。
因此
class SomeClass: pass
等价于
SomeClass = type('SomeClass', (), {})
并且
class ParentClass:
pass
class SomeClass(ParentClass):
some_var = 5
def some_function(self):
print("Hello!")
等价于
def some_function(self):
print("Hello")
ParentClass = type('ParentClass', (), {})
SomeClass = type('SomeClass',
[ParentClass],
{'some_function': some_function,
'some_var':5})
因此,通过我们自定义的元类而不是 type,我们可以给类注入一些行为(we can inject some behavior to the classes that wouldn't have been possible)。但是,在我们实现通过元类注入行为之前,让我们来看看Python中更常见的实现元编程的方法。
装饰器(Decorators):Python中元编程的一个常见示例
装饰器是一种修改函数行为或者类行为的方法。装饰器的使用看起来大概是这个样子:
@some_decorator
def some_func(*args, **kwargs):
pass
@some_decorator
只是一种语法糖,表示函数some_func
被另一个函数some_decorator
封装起来。我们知道函数和类(除了 type 这个元类)在Python中都是对象,这意味着它们可以:
- 分配给一个变量(Assigned to a variable)
- 复制(copied)
- 作为参数传递给另一个函数(Passed as parameters to other functions)
上面的写法等同于
some_func = some_decorator(some_func)
你可能会想知道 some_decorator 是如何定义的
def some_decorator(f):
"""
The decorator receives function as a parameter.
"""
def wrapper(*args, **kwargs):
# doing something before calling the function
f(*args, **kwargs)
# doing something after the function is called
return wrapper
现在假设我们有一个从URL抓取数据的函数。被抓取服务器上有限流机制当它检测到同一个IP地址发来过多的请求并且请求间隔都一样时,会限制当前IP的请求。为了让我们的抓取程序表现的更随机一些,我们会让程序在每次请求之后暂定一小段随机时间来“欺骗”被抓取服务器。这个需求我们能通过装饰器来实现么?看代码
from functools import wraps
import random
import time
def wait_random(min_wait=1, max_wait=5):
def inner_function(func):
@wraps(func)
def wrapper(*args, **kwargs):
time.sleep(random.randint(min_wait, max_wait))
return func(*args, **kwargs)
return wrapper
return inner_function
@wait_random(10, 15)
def function_to_scrape():
# some scraping stuff
其中 inner_function 和 @wraps 装饰器可能对你来说还比较新。如果你仔细看,inner_function 和我们上文中定义的 some_decorator 类似。之所以用了三层def
关键字,是因为装饰器wait_random要接受参数(min_wait和max_wait)。@wraps
是个很好用的装饰器,他保存原函数(这里是func)的元数据(例如name, doc string, and function attributes)。如果我们没有用 @wraps,当我们对装饰之后的函数调用 help() 时 将不能得到有用的(期望的)结果,它将返回 wrapper 函数的 docstring,而不是 func 函数的(正常我们期望是func的)。
但是如果你有一个爬虫类包含多个类似的函数呢:
class Scraper:
def func_to_scrape_1(self):
# some scraping stuff
pass
def func_to_scrape_2(self):
# some scraping stuff
pass
def func_to_scrape_3(self):
# some scraping stuff
pass
一种方案是对每个方法前都用 @wait_random 进行装饰。但是我们可以做的更优雅:我们可以创建一个“类装饰器”。思路是遍历类的名称空间,识别出函数,然后用我们的装饰器进行封装
def classwrapper(cls):
for name, val in vars(cls).items():
# `callable` return `True` if the argument is callable
# i.e. implements the `__call`
if callable(val):
# instead of val, wrap it with our decorator.
setattr(cls, name, wait_random()(val))
return cls
现在我们可以用 @classwrapper 来封装整个Scraper类。但是再进一步,如果我们有很多和Scraper相似的类呢?当然你可以分别对每个类用 @classwrapper 进行装饰,但是也可以更优雅:创建一个元类。
元类(Metaclasses)
编写一个元类包含两步:
- 创建一个子类继承自元类 type(Write a subclass of the metaclass type)
- 通过“元类钩子”将新的元类插入到类创建过程(Insert the new metaclass into the class creation process using the metaclass hook)
我们创建 type 元类的子类,修改一些魔术方法,像__init__
,__new__
,__prepare__
以及__call__
以实现在创建类的过程中修改类的行为。这些方法包含了像父类,类名,属性等信息。Python2中,元类钩子(metaclass hook)是类中一个名为__metaclass__
的静态属性(the metaclass hook is a static field in the class called metaclass)。Python3中, 你可以在类的基类列表中指定元类作为元类参数(you can specify the metaclass as a metaclass argument in the base-class list of a class)。
>>> class CustomMetaClass(type):
... def __init__(cls, name, bases, attrs):
... for name, value in attrs.items():
# do some stuff
... print('{} :{}'.format(name, value))
>>> class SomeClass(metaclass=CustomMetaClass):
... class_attribute = "Some string"
__module__ :__main__
__metaclass__ :<class '__main__.CustomMetaClass'>
class_attribute :Some string
属性被自动打印出来由于 CustomMetaClass 中的 __init__方法。我们来假设一下在你的Python项目中有一位“烦人”的伙伴习惯用 camelCase(驼峰法)方式来命名类中的属性和方法。你知道这不是一条好的实践,应该用 snake_case(即下划线方式)方式。那么我们可以编写一个元类来讲所有驼峰法的属性名称和方法名称修改为下划线方式吗?
def camel_to_snake(name):
"""
A function that converts camelCase to snake_case.
Referred from: https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case
"""
import re
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
class SnakeCaseMetaclass(type):
def __new__(snakecase_metaclass, future_class_name,
future_class_parents, future_class_attr):
snakecase_attrs = {}
for name, val in future_class_attr.items():
snakecase_attrs[camel_to_snake(name)] = val
return type(future_class_name, future_class_parents,
snakecase_attrs)
你可能已经注意到这里用了__new__
方法而不是__init__
。实际上 __new是创建一个实例过程的第一步,它负责返回由类实例化而来的实例。另一方面, \init并不返回任何东西,它仅仅负责在实例创建之后对实例进行各种初始化。记住一个简单的法则:**当你需要控制一个实例的创建过程时用`new;当你需要对一个新创建的实例进行初始化时用
init__`**。
一般在实现元类的时候不用 __init,因为他“不够强大”:在实际调用 \init之前类的创建过程已经完成。你可以理解`init`就像一个类装饰器,但不同的是 \init__在创建子类的时候会被调用,而装饰器则不会。
由于我们的任务包含创建一个新的实例(防止这些驼峰法的属性名称潜入到类中),重写我自定义元类 SnakeCaseMetaClass 中的 __new__方法。让我们来检查一下这是否按预期工作了:
>>> class SomeClass(metaclass=SnakeCaseMetaclass):
... camelCaseVar = 5
>>> SomeClass.camelCaseVar
AttributeError: type object 'SomeClass' has no attribute 'camelCaseVar'
>>> SomeClass.camel_case_var
5
结果是预期的。现在你知道了Python中如何编写元类。
总结
在这篇文章中,介绍了Python中实例
,类
和元类
的关系。也展示了元编程的知识,这是一种操作代码的方法。我们还讨论了装饰器
和类装饰器
用来对类和方法(函数)注入一些额外的行为。然后我们展示了如何通过继承默认的元类type
来创建自定义的元类。最后我们展示了一些用到元类的场景。关于是否使用元类,在网上也有比较大的争议。但是通过本文我们应该能分析什么类型的问题用元编程来解决可能会更好。
由于本人能力有限,若有有不精准或模糊的地方,请见原文链接。