Python中的元编程

就像元数据是关于数据的数据同样,元编程是编写程序来操做程序(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)。全部用于读取、分析、转换或修改自身的程序都是元编程的例子。好比:html

  • Domain-specific languages (DSLs)
  • Parsers
  • Interpreters
  • Compilers
  • Theorem provers
  • Term rewriters

这篇教程介绍Python中的元编程,它经过对Python特性的回顾来更新您的Python知识,这样您就能够更好地理解本文中的概念。本文也解释了Python中的type函数除了返回一个对象(上层的)的类以外是如何拥有更重要的意义的。而后,讨论了在Python中元编程的方法以及元编程如何简化某些特定类型的任务。python

一点自我检讨

若是你已经由一些Python编程经历,你可能知道那句话:Python中一切皆对象,类建立对象。可是若是一切皆对象(则类也是对象),那么是谁建立了类呢?这正是我要回答的问题。编程

咱们来验证一下前面的说法是否正确api

>>> 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:app

>>> type(SomeClass)
<type 'classobj'>>>>
inspect.isclass(type(SomeClass))
True

classobj是一个特殊的类,在Python3中全部的类都默认继承自它。如今一切变得有道理了,可是 classobj 呢,对它调用type()又会如何呢?dom

>>> 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之外一切皆对象,他们要么是类的对象,要么是元类的对象。ide

来验证这个观点:函数

>>> some_obj = SomeClass()
>>> isinstance(some_obj,SomeClass)
True
>>> isinstance(SomeClass, type)
True

所以咱们能够知道实例是一个类的实例化,而类是一个元类的实例化。code

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)

编写一个元类包含两步:

  1. 建立一个子类继承自元类 type(Write a subclass of the metaclass type)
  2. 经过“元类钩子”将新的元类插入到类建立过程(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来建立自定义的元类。最后咱们展现了一些用到元类的场景。关因而否使用元类,在网上也有比较大的争议。可是经过本文咱们应该能分析什么类型的问题用元编程来解决可能会更好。

因为本人能力有限,如有有不精准或模糊的地方,请见原文连接

相关文章
相关标签/搜索