1. 程式人生 > >1Python進階強化訓練之裝飾器使用技巧進階

1Python進階強化訓練之裝飾器使用技巧進階

黃金分割 解決方案 return 技巧 原函數

Python進階強化訓練之裝飾器使用技巧進階


如何使用函數裝飾器?

實際案例

某些時候我們想為多個函數,統一添加某種功能,比如記時統計、記錄日誌、緩存運算結果等等。

我們不想在每個函數內一一添加完全相同的代碼,有什麽好的解決方案呢?


解決方案

定義裝飾奇函數,用它來生成一個在原函數基礎添加了新功能的函數,替代原函數

如有如下兩道題:

題目一

斐波那契數列又稱黃金分割數列,指的是這樣一個數列:1,1,2,3,5,8,13,21,….,這個數列從第三項開始,每一項都等於前兩項之和,求數列第n項。

題目二

一個共有10個臺階的樓梯,從下面走到上面,一次只能邁1-3個臺階,並且不能後退,走完整個樓梯共有多少種方法?

腳本如下:

# 函數裝飾器
def memp(func):
    cache = {}
    
    def wrap(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
        
    return wrap
    
    
# 第一題
@memp
def fibonacci(n):
    if n <= 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n - 2)
    
print(fibonacci(50))


# 第二題
@memp
def climb(n, steps):
    count = 0
    if n == 0:
        count = 1
    elif n > 0:
        for step in steps:
            count += climb(n - step, steps)
    return count
    
    
print(climb(10, (1, 2, 3)))

輸出結果:

C:\Python\Python35\python.exe E:/python-intensive-training/s11.py
20365011074
274

Process finished with exit code 0

如何為被裝飾的函數保存元數據?

實際案例

在函數對象張保存著一些函數的元數據,例如:

方法描述
f.__name__函數的名字
f.__doc__函數文檔字符串
f.__module__函數所屬模塊名
f.__dict__屬性字典
f.__defaults__默認參數元素

我們在使用裝飾器後,再使用上面的這些屬性訪問時,看到的是內部包裹函數的元數據,原來函數的元數據變丟失掉了,應該如何解決?

解決方案

使用標準庫functools中的裝飾器wraps裝飾內部包裹函數,可以指定將原來函數的某些屬性更新到包裹函數上面

from functools import wraps

def mydecoratot(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """wrapper function"""
        print("In wrapper")
        func(*args, **kwargs)
    return wrapper
    
@mydecoratot
def example():
    """example function"""
    print(‘In example‘)
    
print(example.__name__)
print(example.__doc__)

輸出結果:

C:\Python\Python35\python.exe E:/python-intensive-training/s12.py
example
example function

Process finished with exit code 0

如何定義帶參數的裝飾器?

實際案例

實現一個裝飾器,它用來檢查被裝飾函數的參數類型,裝飾器可以通過指定函數參數的類型,調用時如果檢測出類型不匹配則拋出異常,比如調用時可以寫成如下:

@typeassert(str, int, int)
def f(a, b, c):
   ......

或者

@typeassert(y=list)
def g(x, y):
   ......

解決方案

提取函數簽名:inspect.signature()

帶參數的裝飾器,也就是根據參數定制化一個裝飾器,可以看成生產裝飾器的工廠,美的調用typeassert,返回一個特定的裝飾器,然後用他去裝飾其他函數。

from inspect import signature

def typeassery(*ty_args, **ty_kwargs):
    def decorator(func):
        # 獲取到函數參數和類型之前的關系
        sig = signature(func)
        btypes = sig.bind_partial(*ty_args, **ty_kwargs).arguments
        
        def wrapper(*args, **kwargs):
            for name, obj in sig.bind(*args, **kwargs).arguments.items():
                if name in btypes:
                    if not isinstance(obj, btypes[name]):
                        raise TypeError(‘"%s" must be "%s" ‘ % (name, btypes[name]))
            return func(*args, **kwargs)
            
        return wrapper
        
    return decorator
    
@typeassery(int, str, list)
def f(a, b, c):
    print(a, b, c)
    
# 正確的
f(1, ‘abc‘, [1, 2, 3])
# 錯誤的
f(1, 2, [1, 2, 3])

執行結果

C:\Python\Python35\python.exe E:/python-intensive-training/s13.py
1 abc [1, 2, 3]
Traceback (most recent call last):
  File "E:/python-intensive-training/s13.py", line 28, in <module>
    f(1, 2, [1, 2, 3])
  File "E:/python-intensive-training/s13.py", line 14, in wrapper
    raise TypeError(‘"%s" must be "%s" ‘ % (name, btypes[name]))
TypeError: "b" must be "<class ‘str‘>" 

Process finished with exit code 1

如何實現屬性可修改的函數裝飾器?

實際案例

為分析程序內那些函數執行時間開銷較大,我們定義一個帶timeout參數的函數裝飾器,裝飾功能如下:

  1. 統計被裝飾函數單詞調用運行時間

  2. 時間大於參數timeout的,將此次函數調用記錄到log日誌中

  3. 運行時可修改timeout的值

解決方案

為包裹函數增加一個函數,用來修改閉包中使用的自由變量

在python3中使用nonlocal訪問嵌套作用於中的變量引用

代碼如下:

from functools import wraps
import time
import logging
from random import randint

def warn(timeout):
    # timeout = [timeout]  # py2
    
    def decorator(func):
        def wrapper(*args, **kwargs):
            start = time.time()
            res = func(*args, **kwargs)
            used = time.time() - start
            if used > timeout:
                # if used > timeout:  # py2
                msg = ‘"%s": "%s" > "%s"‘ % (func.__name__, used, timeout)
                # msg = ‘"%s": "%s" > "%s"‘ % (func.__name__, used, timeout[0])  # py2
                logging.warn(msg)
            return res
            
        def setTimeout(k):
            nonlocal timeout
            timeout = k
            # timeout[0] = k  # py2
            
        wrapper.setTimeout = setTimeout
        return wrapper
        
    return decorator
    
@warn(1.5)
def test():
    print(‘In Tst‘)
    while randint(0, 1):
        time.sleep(0.5)
        
for _ in range(10):
    test()
    
test.setTimeout(1)

for _ in range(10):
    test()

輸出結果:

C:\Python\Python35\python.exe E:/python-intensive-training/s14.py
In Tst
In Tst
WARNING:root:"test": "2.503000259399414" > "1.5"
In Tst
In Tst
In Tst
In Tst
In Tst
In Tst
In Tst
In Tst
In Tst
WARNING:root:"test": "1.0008063316345215" > "1"
In Tst
In Tst
In Tst
WARNING:root:"test": "1.0009682178497314" > "1"
In Tst
In Tst
WARNING:root:"test": "1.5025172233581543" > "1"
In Tst
In Tst
In Tst
In Tst
Process finished with exit code 0

如何在類中定義裝飾器?

實際案例

實現一個能將函數調用信息記錄到日誌的裝飾器:

  1. 把每次函數的調用時間,執行時間,調用次數寫入日誌

  2. 可以對被裝飾函數分組,調用信息記錄到不同日誌

  3. 動態修改參數,比如日誌格式

  4. 動態打開關閉日誌輸出功能

解決方案

為了讓裝飾器在使用上更加靈活,可以把類的實例方法作為裝飾器,此時包裹函數中就可以持有實例對象,便於修改屬性和擴展功能

代碼如下:

import logging
from time import localtime, time, strftime, sleep
from random import choice

class CallingInfo:
    def __init__(self, name):
        log = logging.getLogger(name)
        log.setLevel(logging.INFO)
        fh = logging.FileHandler(name + ‘.log‘)  # 日誌保存的文件
        log.addHandler(fh)
        log.info(‘Start‘.center(50, ‘-‘))
        self.log = log
        self.formattter = ‘%(func)s -> [%(time)s - %(used)s - %(ncalls)s]‘
        
    def info(self, func):
        def wrapper(*args, **kwargs):
            wrapper.ncalls += 1
            lt = localtime()
            start = time()
            res = func(*args, **kwargs)
            used = time() - start
            info = {}
            info[‘func‘] = func.__name__
            info[‘time‘] = strftime(‘%x %x‘, lt)
            info[‘used‘] = used
            info[‘ncalls‘] = wrapper.ncalls
            msg = self.formattter % info
            self.log.info(msg)
            return res
            
        wrapper.ncalls = 0
        return wrapper
        
    def SetFormatter(self, formatter):
        self.formattter = formatter
        
    def turnOm(self):
        self.log.setLevel(logging.INFO)
        
    def turnOff(self):
        self.log.setLevel(logging.WARN)
        
cinfo1 = CallingInfo(‘mylog1‘)
cinfo2 = CallingInfo(‘mylog2‘)

# 設置日誌指定格式
# cinfo1.SetFormatter(‘%(func)s -> [%(time)s - %(ncalls)s]‘)
# 關閉日誌
# cinfo2.turnOff()

@cinfo1.info
def f():
    print(‘in F‘)
    
@cinfo1.info
def g():
    print(‘in G‘)
    
@cinfo2.info
def h():
    print(‘in H‘)
    
for _ in range(50):
    choice([f, g, h])()
    sleep(choice([0.5, 1, 1.5]))

#裝飾器

本文出自 “Eden” 博客,請務必保留此出處http://edeny.blog.51cto.com/10733491/1924942

1Python進階強化訓練之裝飾器使用技巧進階