以一張圖開始今天的死磕,這時早上組長剛說我的。有感,想跟深入的再熟悉一下元編程。
軟件開發領域中最經典的口頭禪就是“don’t repeat yourself”。 也就是說,任何時候當你的程序中存在高度重復(或者是通過剪切復制)的代碼時,都應該想想是否有更好的解決方案。 在Python當中,通常都可以通過元編程來解決這類問題。 簡而言之,元編程就是關於創建操作源代碼(比如修改、生成或包裝原來的代碼)的函數和類。 主要技術是使用裝飾器、類裝飾器和元類。
一、你想在函數上添加一個包裝器,增加額外的操作處理(比如日志、計時等)。
之前思路:利用裝飾器。
裝飾器最基本的原理如下:
@timethis def countdown(n): pass
效果等同如下:
def countdown(n): pass countdown = timethis(countdown)
所以我們在inner函數中實現我們想要的業務邏輯即可。
def wraper(func): def inner(*args,**kwargs): # 你想實現的額外功能 res = func() return res return inner
但是如果我們打印 func.__name__,就會出現inner,這個函數的重要的元信息比如名字、文檔字符串、注解和參數簽名都丟失了。
二、如何解決上述問題呢
注意:任何時候你定義裝飾器的時候,都應該使用 functools
庫中的 @wraps
裝飾器來注解底層包裝函數
def wraper(func): @wraps def inner(*args,**kwargs): # 你想實現的額外功能 res = func() return res return inner
這樣就能解決元信息丟失的情況了。__wrapped__
屬性還能讓被裝飾函數正確暴露底層的參數簽名信息。例如:
>>> from inspect import signature >>> print(signature(countdown)) (n:int) >>>
特別的,內置的裝飾器 @staticmethod
和 @classmethod
就沒有遵循這個約定 (它們把原始函數存儲在屬性 __func__
中)。
三、如何解除裝飾器
遺漏點:要使用__wrapped__,原函數必須被@wraps包裹
>>> @somedecorator >>> def add(x, y): ... return x + y ... >>> orig_add = add.__wrapped__ >>> orig_add(3, 4) 7
四、什么時候會用到三層包裹的裝飾器。
遺漏點:最外層處理裝飾器的參數,返回次外層函數。相當於可以傳遞除被裝飾函數名外的其他參數。
假設你想寫一個裝飾器,給函數添加日志功能,同時允許用戶指定日志的級別和其他的選項。
from functools import wraps import logging def logged(level, name=None, message=None): """ Add logging to a function. level is the logging level, name is the logger name, and message is the log message. If name and message aren't specified, they default to the function's module and name. """ def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) return wrapper return decorate # Example use @logged(logging.DEBUG) def add(x, y): return x + y @logged(logging.CRITICAL, 'example') def spam(): print('Spam!')
五、給靜態方法和類方法提供裝飾器
import time from functools import wraps # A simple decorator def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() r = func(*args, **kwargs) end = time.time() print(end-start) return r return wrapper # Class illustrating application of the decorator to different kinds of methods class Spam: @timethis def instance_method(self, n): print(self, n) while n > 0: n -= 1 @classmethod @timethis def class_method(cls, n): print(cls, n) while n > 0: n -= 1 @staticmethod @timethis def static_method(n): print(n) while n > 0: n -= 1
注意:類方法和靜態方法應該在裝飾器函數之后,@classmethod
和 @staticmethod
實際上並不會創建可直接調用的對象, 而是創建特殊的描述器對象。因此當你試着在其他裝飾器中將它們當做函數來使用時就會出錯。
六、你想通過反省或者重寫類定義的某部分來修改它的行為,但是你又不希望使用繼承或元類的方式。
這種情況可能是類裝飾器最好的使用場景了。例如,下面是一個重寫了特殊方法 __getattribute__
的類裝飾器, 可以打印日志:
def log_getattribute(cls): # Get the original implementation orig_getattribute = cls.__getattribute__ # Make a new definition def new_getattribute(self, name): print('getting:', name) return orig_getattribute(self, name) # Attach to the class and return cls.__getattribute__ = new_getattribute return cls # Example use @log_getattribute class A: def __init__(self,x): self.x = x def spam(self): pass
>>> a = A(42)
# a = A(42) = log_getattribute(A)(42) = new_A(42) 這個new_A新增了一個方法,當取屬性時,會執行新方法
>>> a.x # a就執行了new_getattribute()
getting: x
42
>>> a.spam() getting: spam >>>
七、你想通過改變實例創建方式來實現單例、緩存或其他類似的特性。
假設你不想任何人創建這個類的實例:
class NoInstances(type): def __call__(self, *args, **kwargs): raise TypeError("Can't instantiate directly") # Example class Spam(metaclass=NoInstances): @staticmethod def grok(x): print('Spam.grok')
>>> Spam.grok(42) Spam.grok >>> s = Spam() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "example1.py", line 7, in __call__ raise TypeError("Can't instantiate directly") TypeError: Can't instantiate directly >>>
還可以根據元類建立單例模式;
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 # Example class Spam(metaclass=Singleton): def __init__(self): print('Creating Spam')
>>> a = Spam() Creating Spam >>> b = Spam() >>> a is b True >>> c = Spam() >>> a is c True >>>
八、元類的構成
如果我們要修改__new__,我們會經常看到下面這段代碼:
class Meta(type): def __new__(cls, name, bases, dct): return super().__new__(cls, name, bases, dct)
當你定義一個類的時候:
class Foo(p1, p2): v = 'var1' def func(self): return 'func1'
python大致會把他解析成這樣:
name = 'Foo' base = (p1, p2) def func(self): return 'func' dct = {'v': 'var1', 'func': func} Foo = type( name, base, dct )
name就是類名,這里是Foo, base是要繼承的父類,(Base1,Base2),dict包含了里面所有的方法和變量。
作為一個具體的應用例子,下面定義了一個元類,它會拒絕任何有混合大小寫名字作為方法的類定義:class NoMixedCaseMeta(type): def __new__(cls, clsname, bases, clsdict): for name in clsdict: if name.lower() != name: raise TypeError('Bad attribute name: ' + name) return super().__new__(cls, clsname, bases, clsdict) class Root(metaclass=NoMixedCaseMeta): pass class A(Root): def foo_bar(self): # Ok pass class B(Root): def fooBar(self): # TypeError pass
九、用type去定義一個元類
使用函數 types.new_class()
來初始化新的類對象。 你需要做的只是提供類的名字、父類元組、關鍵字參數,以及一個用成員變量填充類字典的回調函數。
def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price def cost(self): return self.shares * self.price cls_dict = { '__init__' : __init__, 'cost' : cost, } # Make a class import types Stock = types.new_class('Stock', (), {}, lambda ns: ns.update(cls_dict)) Stock.__module__ = __name__
這種方式會構建一個普通的類對象,並且按照你的期望工作
>>> s = Stock('ACME', 50, 91.1) >>> s <stock.Stock object at 0x1006a9b10> >>> s.cost() 4555.0 >>>
下面一個例子:
class Spam(Base, debug=True, typecheck=False): pass
那么可以將其翻譯成如下的 new_class()
調用形式:
Spam = types.new_class('Spam', (Base,), {'debug': True, 'typecheck': False}, lambda ns: ns.update(cls_dict))
ew_class()
第四個參數最神秘,它是一個用來接受類命名空間的映射對象的函數。 通常這是一個普通的字典,但是它實際上是 __prepare__()
方法返回的任意對象,這個函數需要使用上面演示的 update()
方法給命名空間增加內容。
十、你想自己去實現一個新的上下文管理器,以便使用with語句。
contexlib
模塊中的
@contextmanager
裝飾器。 下面是一個實現了代碼塊計時功能的上下文管理器例子:
import time from contextlib import contextmanager @contextmanager def timethis(label): start = time.time() try: yield finally: end = time.time() print('{}: {}'.format(label, end - start)) # Example use with timethis('counting'): n = 10000000 while n > 0: n -= 1
在函數 timethis()
中,yield
之前的代碼會在上下文管理器中作為 __enter__()
方法執行, 所有在 yield
之后的代碼會作為 __exit__()
方法執行。 如果出現了異常,異常會在yield語句那里拋出。
通常情況下,如果要寫一個上下文管理器,你需要定義一個類,里面包含一個 __enter__()
和一個__exit__()
方法,如下所示:
import time class timethis: def __init__(self, label): self.label = label def __enter__(self): self.start = time.time() def __exit__(self, exc_ty, exc_val, exc_tb): end = time.time() print('{}: {}'.format(self.label, end - self.start))
@contextmanager
應該僅僅用來寫自包含的上下文管理函數。 如果你有一些對象(比如一個文件、網絡連接或鎖),需要支持 with
語句,那么你就需要單獨實現 __enter__()
方法和 __exit__()
方法。