說說Python的元編程
提到元這個字,你也許會想到元數(shù)據(jù),元數(shù)據(jù)就是描述數(shù)據(jù)本身的數(shù)據(jù),元類就是類的類,相應的元編程就是描述代碼本身的代碼,元編程就是關于創(chuàng)建操作源代碼(比如修改、生成或包裝原來的代碼)的函數(shù)和類。主要技術是使用裝飾器、元類、描述符類。本文的主要目的是向大家介紹這些元編程技術,并且給出實例來演示它們是怎樣定制化源代碼的行為。
裝飾器
裝飾器就是函數(shù)的函數(shù),它接受一個函數(shù)作為參數(shù)并返回一個新的函數(shù),在不改變原來函數(shù)代碼的情況下為其增加新的功能,比如最常用的計時裝飾器:
- from functools import wraps
- def timeit(logger=None):
- """
- 耗時統(tǒng)計裝飾器,單位是秒,保留 4 位小數(shù)
- """
- def decorator(func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- start = time.time()
- result = func(*args, **kwargs)
- end = time.time()
- if logger:
- logger.info(f"{func.__name__} cost {end - start :.4f} seconds")
- else:
- print(f"{func.__name__} cost {end - start :.4f} seconds")
- return result
- return wrapper
- return decorator
(注:比如上面使用 @wraps(func) 注解是很重要的, 它能保留原始函數(shù)的元數(shù)據(jù)) 只需要在原來的函數(shù)上面加上 @timeit() 即可為其增加新的功能:
- @timeit()
- def test_timeit():
- time.sleep(1)
- test_timeit()
- #test_timeit cost 1.0026 seconds
上面的代碼跟下面這樣寫的效果是一樣的:
- test_timeit = timeit(test_timeit)
- test_timeit()
裝飾器的執(zhí)行順序
當有多個裝飾器的時候,他們的調用順序是怎么樣的?
假如有這樣的代碼,請問是先打印 Decorator1 還是 Decorator2 ?
- from functools import wraps
- def decorator1(func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- print('Decorator 1')
- return func(*args, **kwargs)
- return wrapper
- def decorator2(func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- print('Decorator 2')
- return func(*args, **kwargs)
- return wrapper
- @decorator1
- @decorator2
- def add(x, y):
- return x + y
- add(1,2)
- # Decorator 1
- # Decorator 2
回答這個問題之前,我先給你打個形象的比喻,裝飾器就像函數(shù)在穿衣服,離它最近的最先穿,離得遠的最后穿,上例中 decorator1 是外套,decorator2 是內衣。
- add = decorator1(decorator2(add))
在調用函數(shù)的時候,就像脫衣服,先解除最外面的 decorator1,也就是先打印 Decorator1,執(zhí)行到 return func(*args, **kwargs) 的時候會去解除 decorator2,然后打印 Decorator2,再次執(zhí)行到 return func(*args, **kwargs) 時會真正執(zhí)行 add() 函數(shù)。
需要注意的是打印的位置,如果打印字符串的代碼位于調用函數(shù)之后,像下面這樣,那輸出的結果正好相反:
- def decorator1(func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- result = func(*args, **kwargs)
- print('Decorator 1')
- return result
- return wrapper
- def decorator2(func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- result = func(*args, **kwargs)
- print('Decorator 2')
- return result
- return wrapper
裝飾器不僅可以定義為函數(shù),也可以定義為類,只要你確保它實現(xiàn)了__call__() 和 __get__() 方法。
關于裝飾器的其他用法,可以參考前文:
- 我是裝飾器
- 再談裝飾器
元類
Python 中所有類(object)的元類,就是 type 類,也就是說 Python 類的創(chuàng)建行為由默認的 type 類控制,打個比喻,type 類是所有類的祖先。我們可以通過編程的方式來實現(xiàn)自定義的一些對象創(chuàng)建行為。
定一個類繼承 type 類 A,然后讓其他類的元類指向 A,就可以控制 A 的創(chuàng)建行為。典型的就是使用元類實現(xiàn)一個單例:
- class Singleton(type):
- def __init__(self, *args, **kwargs):
- self._instance = None
- super().__init__(*args, **kwargs)
- def __call__(self, *args, **kwargs):
- if self._instance is None:
- self._instance = super().__call__(*args, **kwargs)
- return self._instance
- else:
- return self._instance
- class Spam(metaclass=Singleton):
- def __init__(self):
- print("Spam!!!")
元類 Singleton 的__init__和__new__ 方法會在定義 Spam 的期間被執(zhí)行,而 __call__方法會在實例化 Spam 的時候執(zhí)行。
如果想更好的理解元類,可以閱讀Python黑魔法之metaclass
descriptor 類(描述符類)
descriptor 就是任何一個定義了 __get__(),__set__()或 __delete__()的對象,描述器讓對象能夠自定義屬性查找、存儲和刪除的操作。這里舉官方文檔[1]一個自定義驗證器的例子。
定義驗證器類,它是一個描述符類,同時還是一個抽象類:
- from abc import ABC, abstractmethod
- class Validator(ABC):
- def __set_name__(self, owner, name):
- self.private_name = '_' + name
- def __get__(self, obj, objtype=None):
- return getattr(obj, self.private_name)
- def __set__(self, obj, value):
- self.validate(value)
- setattr(obj, self.private_name, value)
- @abstractmethod
- def validate(self, value):
- pass
自定義驗證器需要從 Validator 繼承,并且必須提供 validate() 方法以根據(jù)需要測試各種約束。
這是三個實用的數(shù)據(jù)驗證工具:
OneOf 驗證值是一組受約束的選項之一。
- class OneOf(Validator):
- def __init__(self, *options):
- self.options = set(options)
- def validate(self, value):
- if value not in self.options:
- raise ValueError(f'Expected {value!r} to be one of {self.options!r}')
Number 驗證值是否為 int 或 float。根據(jù)可選參數(shù),它還可以驗證值在給定的最小值或最大值之間。
- class Number(Validator):
- def __init__(self, minvalue=None, maxvalue=None):
- self.minvalue = minvalue
- self.maxvalue = maxvalue
- def validate(self, value):
- if not isinstance(value, (int, float)):
- raise TypeError(f'Expected {value!r} to be an int or float')
- if self.minvalue is not None and value < self.minvalue:
- raise ValueError(
- f'Expected {value!r} to be at least {self.minvalue!r}'
- )
- if self.maxvalue is not None and value > self.maxvalue:
- raise ValueError(
- f'Expected {value!r} to be no more than {self.maxvalue!r}'
- )
String 驗證值是否為 str。根據(jù)可選參數(shù),它可以驗證給定的最小或最大長度。它還可以驗證用戶定義的 predicate。
- class String(Validator):
- def __init__(self, minsize=None, maxsize=None, predicate=None):
- self.minsize = minsize
- self.maxsize = maxsize
- self.predicate = predicate
- def validate(self, value):
- if not isinstance(value, str):
- raise TypeError(f'Expected {value!r} to be an str')
- if self.minsize is not None and len(value) < self.minsize:
- raise ValueError(
- f'Expected {value!r} to be no smaller than {self.minsize!r}'
- )
- if self.maxsize is not None and len(value) > self.maxsize:
- raise ValueError(
- f'Expected {value!r} to be no bigger than {self.maxsize!r}'
- )
- if self.predicate is not None and not self.predicate(value):
- raise ValueError(
- f'Expected {self.predicate} to be true for {value!r}'
- )
實際應用時這樣寫:
- class Component:
- name = String(minsize=3, maxsize=10, predicate=str.isupper)
- kind = OneOf('wood', 'metal', 'plastic')
- quantity = Number(minvalue=0)
- def __init__(self, name, kind, quantity):
- self.name = name
- self.kind = kind
- self.quantity = quantity
描述器阻止無效實例的創(chuàng)建:
- >>> Component('Widget', 'metal', 5) # Blocked: 'Widget' is not all uppercase
- Traceback (most recent call last):
- ...
- ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'
- >>> Component('WIDGET', 'metle', 5) # Blocked: 'metle' is misspelled
- Traceback (most recent call last):
- ...
- ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}
- >>> Component('WIDGET', 'metal', -5) # Blocked: -5 is negative
- Traceback (most recent call last):
- ...
- ValueError: Expected -5 to be at least 0
- >>> Component('WIDGET', 'metal', 'V') # Blocked: 'V' isn't a number
- Traceback (most recent call last):
- ...
- TypeError: Expected 'V' to be an int or float
- >>> c = Component('WIDGET', 'metal', 5) # Allowed: The inputs are valid
最后的話
關于 Python 的元編程,總結如下:
如果希望某些函數(shù)擁有相同的功能,希望不改變原有的調用方式、不寫重復代碼、易維護,可以使用裝飾器來實現(xiàn)。
如果希望某一些類擁有某些相同的特性,或者在類定義實現(xiàn)對其的控制,我們可以自定義一個元類,然后讓它類的元類指向該類。
如果希望實例的屬性擁有某些共同的特點,就可以自定義一個描述符類。