1. 程式人生 > >python死磕五之元編程

python死磕五之元編程

decorator cme icm 信息丟失 sta __call__ creat inf som

技術分享圖片

  以一張圖開始今天的死磕,這時早上組長剛說我的。有感,想跟深入的再熟悉一下元編程。

  軟件開發領域中最經典的口頭禪就是“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: Cant 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__() 方法。

python死磕五之元編程