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來建立自定義的元類。最後咱們展現了一些用到元類的場景。關因而否使用元類,在網上也有比較大的爭議。可是經過本文咱們應該能分析什麼類型的問題用元編程來解決可能會更好。

因爲本人能力有限,如有有不精準或模糊的地方,請見原文連接

相關文章
相關標籤/搜索