1. 程式人生 > 實用技巧 >Python學習筆記:裝飾器(Decorator)

Python學習筆記:裝飾器(Decorator)

最近看到兩篇寫的非常好的知識文章:如何理解Python裝飾器理解Python裝飾器(Decorator),對我理解python中的裝飾器有非常大的作用。現將其記錄下來,方便以後溫故知新。

裝飾器概念

和裝飾器離不開的是一種叫閉包的概念。

閉包

首先看如下程式碼段:

# print_msg是外圍函式
def print_msg():
    msg = "I'm closure"

    # printer是巢狀函式
    def printer():
        print(msg)

    return printer


# 這裡獲得的就是一個閉包
closure = print_msg()
# 輸出 I'm closure
closure()

msg是一個區域性變數,在print_msg函式執行之後應該就不會存在了。但是巢狀函式引用了這個變數,將這個區域性變數封閉在了巢狀函式中,這樣就形成了一個閉包。維基百科中對閉包的定義如下:

在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。

結合這個例子再看維基百科的解釋,就清晰明瞭多了。閉包就是引用了自有變數的函式,就如例子中的外函式和內函式組合成一個閉包。

一般情況下,在我們認知當中,如果一個函式結束,函式的內部所有東西都會釋放掉,還給記憶體,區域性變數都會消失。但是閉包是一種特殊情況,如果外函式在結束的時候發現有自己的臨時變數將來會在內部函式中用到,就把這個臨時變數繫結給了內部函式,然後自己再結束。

裝飾器

裝飾器一般有如下形式:

def decorator(func):
    # 傳入的引數自定義
    def wrapper(*args, **kargs):
        do something……
        # 傳入了什麼方法,就返回那個方法
        return func(*args, **kargs)
    # 返回內層函式
    return wrapper

裝飾器的使用方法可分兩種:使用語法糖@或者不用。

# 使用語法糖
@decorator
def f(*args, **kargs)
    pass
f(*args, **kargs)

# 不使用語法糖
def f(*args, **kargs)
    pass
x = decorator(f)
x(*args, **kargs)

從上面可以看出,@語法只是把我們定義好的函式f傳到裝飾器函式decorator裡,這樣執行函式f的時候就會變成執行decorator了。

裝飾器舉例

下面是一個顯示函式名字的裝飾器和呼叫情況:

import functools
# 裝飾器本體
def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('call %s():' % func.__name__)
        print('args = {}'.format(*args))
        return func(*args, **kwargs)

    return wrapper

# 裝飾器包裝函式
@log
def test(p):
    print(test.__name__ + " param: " + p)
    
# 呼叫包裝過的函式
test("I'm a param")

# 結果如下
call test():
args = I'm a param
test param: I'm a param

其中有一個 @functools.wraps(func) 字樣的玩意是python自帶的裝飾器。它能把原函式的元資訊拷貝到裝飾器括號裡面的函式中,包括docstring、name、引數列表等等。因為我們的test函式是輸出自己的函式名,所以functools.wraps傳入的函式是func自己。可以嘗試去除 @functools.wraps(func),會發現test.__name__的輸出變成了wrapper。

另外,原例子中如果不用@語法使用自定義裝飾器的話,不用上面那個python自帶裝飾器也能讓test.__name__的輸出為test自己。

進階裝飾器:帶引數傳入

在上面的例子中,裝飾器只是包裝了函式,在包裝時沒有傳入引數。那麼傳入引數的裝飾器是什麼樣的呢?只需要在原來的基礎上再加一層包裝即可。一個例子如下:

def outside(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print('call %s():' % func.__name__)
            print('args = {}'.format(*args))
            print('log_param = {}'.format(text))
            return func(*args, **kwargs)

        return wrapper

    return decorator

@ outside("這是外面的引數")
def test1(p):
    print(test1.__name__ + " param: " + p)
test1("這是引數")

# 不用@的話這樣使用裝飾器:
x = (outside("這是外面的引數"))(test)
x("這是引數")

# 不用@也可以這麼寫
(outside("外面的引數"))(test1)
(outside("外面的引數"))(test1)("裡面的引數")

理解裝飾器

從上面的用法可以看出,裝飾器的用法是在保證不修改原函式的基礎上增加他的功能,讓我們的程式設計更加高效。

番外:內建裝飾器

常見的內建裝飾器有三種,@property、@staticmethod、@classmethod

property裝飾器

裝飾器不僅可以裝飾函式,還能裝飾類。該裝飾器把類內方法當成屬性來使用,必須要有返回值,相當於getter;假如沒有定義 @方法名.setter 修飾方法的話,就是隻讀屬性。

class Car:
    def __init__(self, name, price):
        self._name = name
        self._price = price
    @property
    def car_name(self):
        return self._name
    # 讓car_name可以讀寫
    @car_name.setter
    def car_name(self, value):
        self._name = value
    # car_price是隻讀屬性
    @property
    def car_price(self):
        return str(self._price) + '萬'

benz = Car('benz', 30)
print(benz.car_name) # benz
benz.car_name = "baojun"
print(benz.car_name) # baojun
print(benz.car_price) # 30萬

在上面的例子中,汽車有兩個屬性,名字和價錢,還有返回相對應屬性的方法。使用了@property和@car_name.setter的car_name方法可以像屬性一樣被呼叫而且支援讀寫屬性;只使用@property的方法car_price則只能去讀取屬性。