1. 程式人生 > >《Python高階程式設計》(一)裝飾器

《Python高階程式設計》(一)裝飾器

裝飾器

定義

裝飾器是一個用於封裝函式或類的程式碼的工具。裝飾器就是接受另一個函式作為引數,並用其完成一些操作的函式。

裝飾器的本質

  • 本質:裝飾器就是一個可以接受呼叫也可以返回呼叫的呼叫
>>> def decorated_by(func):
...     func.__doc__ += '\nDecorated by decorated_by'
...     return func
... 
>>> def add(x, y):
...     """Return the sum of x and y"""
...     return x+y
... 
>>> add = decorated_by(add)
>>>
help(add)
  • 通過在裝飾器名稱前放置@字元實現,即:
    @decorated_by 等同於 add = decorated_by(add)
>>> @decorated_by
... def add1(x,y):
...     """Return the sum of x and y"""
...     return x+y
  • 通過@使用多個裝飾器時,需要按照自底向上的順序應用

裝飾器應用示例

  • 類中的@classmethod、@staticmethod (使一個類上的方法不需要這個類的例項)
  • Django中的@login_required、@permission_required
  • Flask中的@app.route

編寫裝飾器的用例

  • 附加功能:檢查身份、記錄函式結果等
  • 資料的清理或新增:
  • 函式註冊:在任務執行器中註冊一個任務或帶有訊號處理器的函式

案例

1 函式登錄檔

  • 可以擁有完全分離的登錄檔
  • 也可以在多個登錄檔中註冊同一個函式
class Registry:
        def __init__(self):
            self.functions = []
        """
        函式登錄檔:register是一個裝飾器,位置引數被裝飾到登錄檔變數,
        並返回未改變的裝飾方法。
        任何接收register裝飾器的方法都將把自身附加到functions
        """
        def register(self, decorated):
            self.functions.append(decorated)
            return decorated
        def run_all(self, *args, **kwargs):
            return_values = []
            #訪問登錄檔,遍歷登錄檔執行函式並將結果組成列表返回
            for func in self.functions:
                return_values.append(func(*args, **kwargs))
            return return_values
# 可以擁有完全分離的登錄檔
# 也可以在多個登錄檔中註冊同一個函式
a = Registry()
b = Registry()
#註冊函式
@a.register
def foo(x=3):
    return x

@b.register
def bar(x=5):
    return x
    
@a.register
@b.register
def baz(x=7):
    return x
    
print a.run_all()  #[3, 7]
print b.run_all()  #[5, 7]
print a.run_all(x=4) #[4, 4]    

2 使用者驗證案例

  • 使用場景:一個期望將使用者作為其第一個引數的方法,可以使用如下裝飾器做檢查
#-*- coding:utf8 -*-

class User(object):
    """A representation of a user in our application"""
    def __init__(self, username, email):
        self.username = username
        self.email = email

class AnonymousUser(User):
    """An Anonymous User; a stand-in for an actual user"""
    def __init__(self):
        User.__init__(self, None, None)

    def __nonzero__(self):
        return False

import functools
# 前面的方法會使得呼叫時丟失一些重要的功能資訊,比如__name__
# functools.wraps python實現的一個裝飾器,可以將原函式物件的指定屬性複製給包裝函式物件, 預設有 __module__、__name__、__doc__,或者通過引數選擇。
def requires_user(func):
    @functools.wraps(func)
    def inner(user, *args, **kwargs):
        print "%s was used." % user.__name__
        if user and isinstance(user, User):
            return func(user, *args, **kwargs)
        else:
            raise ValueError('A valid user is required to run this.')
    return inner

a = User('xiaoming', '[email protected]')

@requires_user
def test(user):
    print user

test(a)

3 輸出格式化

在所有相關函式的結尾手動將結果轉換為JSON格式非常繁瑣,可使用裝飾器實現格式化

import functools
import json

class JSONOutputError(Exception):
    def __init__(self, message):
        self._message = message
    def __str__(self):
        return self._message

def json_output(decorated):
    @functools.wraps(decorated)
    def inner(*args, **kwargs):
        try:
            result = decorated(*args, **kwargs)
        except JSONOutputError as ex:
            result = {
                'status':'error',
                'message':str(ex),
            }
        return json.dumps(result)
    return inner

@json_output
def error():
    raise JSONOutputError('This function is erratic')

print error() # {"status": "error", "message": "This function is erratic"}

@json_output
def do_nothing():
    return {'status':'done'}

print do_nothing()   #{"status": "done"}

4 日誌管理

  • 通用的日誌管理函式。logged裝飾器的功能:呼叫函式時對函式執行時間進行計時,並將結果記錄到日誌內
import functools
import logging
import time

def logged(method):
    """Cause the decorated method to be run and its results logged, along with some other diagnostic information"""
    @functools.wraps(method)
    def inner(*args, **kwargs):
        #record start time
        start = time.time()
        #Run the decorated method
        return_value = method(*args, **kwargs)
        #record end time
        end = time.time()
        delta = end - start
        #Log the method
        #logger = logging.getLogger('decorator.logged')
        logging.warning('called method %s at %.02f; execution time %.02f seconds; result %r.' % (method.__name__, start, delta, return_value))
        return return_value
    return inner

@logged
def sleep_and_return(return_value):
    time.sleep(2)
    return return_value

print sleep_and_return(42) 

執行結果:
42
WARNING:root:called method sleep_and_return at 1546072641.69; execution time 2.00 seconds; result 42. 42

5 帶引數的裝飾器

在案例3的基礎上改進,裝飾器得到一個帶有縮排和換行的JSON塊。

import functools
import json

class JSONOutputError(Exception):
    def __init__(self, message):
        self._message = message
    def __str__(self):
        return self._message

def json_output_args(indent=None, sort_keys=False):
    def actual_decorator(decorated):
        @functools.wraps(decorated)
        def inner(*args, **kwargs):
            try:
                result = decorated(*args, **kwargs)
            except JSONOutputError as ex:
                result = {
                    'status': 'error',
                    'message': str(ex),
                }
            return json.dumps(result, indent=indent, sort_keys=sort_keys)

        return inner

    return actual_decorator


@json_output_args(indent=4)
def do_nothing():
    return {'status': 'done'}


print do_nothing()

執行結果:
{
“status”: “done”
}

裝飾類

  • 用途
    • 類裝飾器可以與被裝飾類的屬性互動
    • 類裝飾器可以新增屬性或將屬性引數化,或是修改一個類的API,從而使類被宣告的方式與例項被使用的方式不同
  • 案例:
    • 該裝飾器首先儲存了類的原始方法__init__的副本,再建立一個_init_新方法,完成__init__賦值時間戳的屬性
    • 裝飾器還加入了__lt__和__gt__魔術方法
>>> import time
>>> import functools
>>> def sortable_by_creation_time(cls):
    original_init = cls.__init__

    @functools.wraps(original_init)
    def new_init(self, *args, **kwargs):
        original_init(self, *args, **kwargs)
        self._created = time.time()

    cls.__init__ = new_init
    cls.__lt__ = lambda self, other: self._created < other._created
    cls.__gt__ = lambda self, other: self._created > other._created
    return cls
>>> @sortable_by_creation_time
class Sortable(object):
    def __init__(self, identifier):
        self.identifier = identifier
    def __repr__(self):
        return self.identifier
>>> first = Sortable('first')
>>> second = Sortable('second')
>>> third = Sortable('third')
>>> sortable = [second, first, third]
>>> sortable
[second, first, third]
>>> sorted(sortable)
[first, second, third]

pycharm執行sorted沒有排序,之間IDE測試的(下次試)

型別轉換

  • 裝飾器的唯一需求是一個可呼叫函式接受一個可呼叫函式並返回一個可呼叫函式,但並沒有要求必須返回同種型別的可呼叫函式
    • 更高階的裝飾器是:裝飾器裝飾一個函式,但返回一個類
    • 案例:裝飾器建立了Task的一個子類並返回該類,該類是一個可呼叫函式並呼叫一個類建立該類的例項,返回該類的__init__方法
class Task(object):
    """A trivial task class. Task classes have a run method, with run the task"""

    def run(self, *args, **kwargs):
        raise NotImplementedError('Subclasses must implement run')

    def identify(self):
        return 'I am a task'

def task(decorated):
    """Return a class that runs the given function if its run method is called"""
    class TaskSubclass(Task):
        def run(self, *args, **kwargs):
            return decorated(*args, **kwargs)

    return TaskSubclass

@task
def foo():
    return 2 + 2

f = foo()
print f.run()
print f.identify()

執行結果:
4
I am a task

  • 使用一個裝飾器可以將一個函式替換為一個類,可以使開發人員只需要考慮所編寫任務的實際內容

問題

  • 任務被@task_class裝飾器裝飾時,它會變為一個類,執行foo()時會出錯,即:
>>> print foo()
<__main__.TaskSubclass object at 0x0171BE10>

與實際需求不服(應該返回4)

改進方法

(1)呼叫基類Task時新增__call__方法。
(2)@task_class裝飾器返回TaskSubclass類的例項而不是類本身

- 能夠接受該方案是由於對於裝飾器來說唯一的要求是返回一個可呼叫函式,而Task新增的__call__方法意味著它的例項現在可以被呼叫
- 該模式的價值:Task類雖然簡單,但可以看到如何將更多的功能新增進來,這對於管理和執行任務非常有用。

class Task(object):
	"""A trivial task class. Task classes have a run method, with run the task"""
	def __call__(self, *args, **kwargs):
	    return self.run(*args, **kwargs)
	def run(self, *args, **kwargs):
	    raise NotImplementedError('Subclasses must implement run')
	def identify(self):
	    return 'I am a task'

def task(decorated):
    """Return a class that runs the given function if its run method is called"""
    class TaskSubclass(Task):
        def run(self, *args, **kwargs):
            return decorated(*args, **kwargs)
    return TaskSubclass()

@task
def foo():
    return 2+2

print  foo()

結果為4

但是執行f=foo(); f.run()是報錯:(待查詢問題)

print f.run()
AttributeError: 'int' object has no attribute 'run'

總結

  • 裝飾器只是一個函式,具有其他函式的所有靈活性,它可以為了響應輸入而完成所需要完成的工作。
  • 本質上,裝飾器是一個接受可呼叫函式的可呼叫函式,並返回一個可呼叫函式。即裝飾器可以被用於裝飾類和函式(類本身也是可呼叫函式)
  • 考慮使用裝飾器作為一種封裝不相關函式開頭和結尾功能的方法。裝飾器是用於函式註冊、發訊號、某種情況下的類增強以及其他功能的強大工具