利用世界盃,讀懂 Python 裝飾器

痴海發表於2018-07-17

Python 裝飾器是在面試過程高頻被問到的問題,裝飾器也是一個非常好用的特性,
熟練掌握裝飾器會讓你的程式設計思路更加寬廣,程式也更加 pythonic。

今天就結合最近的世界盃帶大家理解下裝飾器。

德國戰車

6 月 17 日德國戰墨西哥,小痴雖然是一個偽球迷,但每年的世界盃還是會了解下。而德國是上屆的冠軍,又是這屆奪冠熱門。德意志戰車在 32 年間小組賽就沒有輸過!臥槽!雖然小痴很少賭球,但這次德國如此強大,肯定會贏吧。搏一搏單車變摩托!隨後小痴買了德國隊贏。心裡想著這次肯定穩了!贏了會所嫩模!小痴連比賽都不看,美滋滋的敲著程式碼。

然後比賽結果卻是德國爆冷 0:1 輸給墨西哥隊,德國隊輸了比賽,小痴也下海乾活。只是此時的天台有點擠,風還有大。

小痴含淚的寫下了下面的程式碼:

def guess_win(func):
    def rooftop_status():
        result = func()
        print('天台已滿,請排隊!')
        return result
    return rooftop_status
@guess_win
def german_team():
    print('德國必勝!') 
複製程式碼

輸出結果:

德國必勝!
天台已滿,請排隊! 
複製程式碼

裝飾器是什麼

首先我們先來了解下什麼是裝飾器,嚴格來說,裝飾器只是語法糖,裝飾器是可呼叫的物件,可以像常規的可呼叫物件那樣呼叫,特殊的地方是裝飾器的引數是一個函式。

裝飾器的存在是為了適用兩個場景,一個是增強被裝飾函式的行為,另一個是程式碼重用。

比如在上面的例子中我們在壓德國隊贏的時候,原本的 german_team() 函式只是輸出德國必勝,但在使用裝飾器(guess_win)後,它的功能多了一項:輸出「天台已滿,請排隊!」。這就是一個簡單的裝飾器,實現了「增強被裝飾函式的行為」。

一個良好的裝飾器必須要遵守兩個原則:

  • 1 不能修改被裝飾函式的程式碼

  • 2 不能修改被裝飾函式的呼叫方式

這裡並不難以理解,在現在的生產環境中,很多程式碼是不能輕易的改寫,因為這樣有可能傳送意想不到的影響。還有一點就是我們在看大神的程式碼,我們根本不懂如何改寫。同時你也不能修改呼叫方式,因為你並不知道有在一個專案中,有多少處應用了此函式。

裝飾器理解基礎

如果你想要很好的理解裝飾器,那下面的兩個內容需要你先有所認知。

  • 1 函式名可以賦值給變數

  • 2 高階函式

1 函式名可以賦值給變數

我們來看下這個例子:

def func(name):
    print('我是{}!慌的一逼!'.format(name))
func('梅西')
y = func
y('勒夫') 
複製程式碼

輸出結果:

我是梅西!慌的一逼!
我是勒夫!慌的一逼!
複製程式碼

在程式碼中我們首先定義了函式 func,並呼叫了 func 函式,並且把 func 賦值給 y。y = func 表明了:函式名可以賦值給變數,並且不影響呼叫。

這樣講,可能還有些人不太明白。我們在來對比下我們常用的操作。這其實和整數、數字是一樣的,下面的程式碼你肯定熟悉:

a = 1
b = a
print(a, b) 
複製程式碼

2 高階函式

高階函式滿足如下的兩個條件中的任意一個:a.可以接收函式名作為實參;b.返回值中可以包含函式名。

在 Python 標準庫中的 map 和 filter 等函式就是高階函式。

l = [1, 2, 4]
r = map(lambda x: x*3, l)
for i in r:
    print('當前天台人數:', i) 
複製程式碼

輸出結果:

當前天台人數: 3
當前天台人數: 6
當前天台人數: 12 
複製程式碼

自定義一個能返回函式的函式,也是高階函式:

def f(l):
    return map(lambda x: x *5, l)
a = f(l)
for i in a:
    print('當前天台人數:', i) 
複製程式碼

輸出結果:

當前天台人數: 5
當前天台人數: 10
當前天台人數: 20 
複製程式碼

實現一個類似的裝飾器

現在你已經知道了「函式名賦值」和「高階函式」,有了這兩個基礎,我們就可以嘗試實現一個類似的裝飾器。

def status(func):
    print('慌的一逼!')
    return func
def name():
    print('我是梅西!')
temp = status(name)
temp() 
複製程式碼

輸出結果:

慌的一逼!
我是梅西! 
複製程式碼

在這個例子中我們定義了一個 status 函式,status 接收一個函式名然後直接返回該函式名。這樣我們實現了不修改原函式 name,並且新增了一個新功能的需求。但是這裡有個缺陷就是函式的呼叫方式改變了。即不是原本的 name,而是 temp。

要解決這個問題很簡單,相信 a = a*3 這樣的表示式大家都見過,那麼上述程式碼中的 temp = status(name) 同樣可以修改為 name = status(name),這樣我們就完美的解決了問題:既新增新功能又沒有修改原函式和其呼叫方式。修改後的程式碼如下:

def status(func):
    print('慌的一逼!')
    return func
def name():
    print('我是梅西!')
name = status(name)
name() 
複製程式碼

但這樣的程式碼卻有個不便之處,即每次使用這樣的裝飾器,我們都要寫類似 name = status(name) 的程式碼。程式設計師都是懶的,所以才有那麼多高階的語法。在 python 中為了簡化這種情況,提供了一個語法糖 @,在每個被裝飾的函式上方使用這個語法糖就可以省掉這一句程式碼 name = status(name),最後的程式碼如下:

def status(func):
    print('慌的一逼!')
    return func
@status
def name():
    print('我是梅西!')
name() 
複製程式碼

這樣我們就弄清楚了裝飾器的工作原理:

  • 1 寫一個高階函式,即引數是函式,返回的也是函式。

  • 2 在利用語法糖@,簡化賦值操作。

但是對比開頭的例子,還是有些不一樣。在開始的例子中,我們還實現了一個 rooftop_status 函式,來判斷下當前的天台狀是否人滿。但是我們現在是直接返回了函式名,這樣函式呼叫後我們就沒辦法做任何事情。梅西和德國慌了,我們也慌了,各個都要天台見,但在這之前我們也要考慮下天台的情況。

為了能判斷天台的情況,所以此時我們需要在巢狀一層函式,將實現額外功能的部分寫在內層函式中,然後將這個內層函式返回即可。這也是為什麼裝飾器都是巢狀函式的原因。

另外,開篇的例子並沒有返回值,也沒有引數,要對既有引數又有返回值的函式進行裝飾的話,還需要進一步完善。 能夠處理返回值的裝飾器:

def guess_win(func):
    def rooftop_status():
        result = func()
        print('天台已滿,請排隊!')
        return result
    return rooftop_status
@guess_win
def german_team():
    print('德國必勝!')
    return '贏了會所嫩模!輸了下海乾活!'
x = german_team()
print(x) 
複製程式碼

輸出結果:

德國必勝!
天台已滿,請排隊!
贏了會所嫩模!輸了下海乾活! 
複製程式碼

能夠處理引數的裝飾器:

def guess_win(func):
    def rooftop_status(*args, **kwargs):
        result = func(*args, **kwargs)
        print('天台已滿,請排隊!')
        return result
    return rooftop_status
@guess_win
def german_team(arg):
    print('{}必勝!'.format(arg))
    return '贏了會所嫩模!輸了下海乾活!'
x = german_team('德國')
y = german_team('西班牙')
print(x) 
複製程式碼

輸出結果:

德國必勝!
天台已滿,請排隊!
西班牙必勝!
天台已滿,請排隊!
贏了會所嫩模!輸了下海乾活! 
複製程式碼

總結

裝飾器的本質是函式,其引數是另一個函式(被裝飾的函式)。裝飾器通常會額外處理被裝飾的函式,然後把它返回,或者將其替換成另一個函式或可呼叫物件。行為良好的裝飾器可以重用,以減少程式碼量。

對於這屆的世界盃,我總結了下。

本文首發與公眾號「痴海」,後臺回覆「1024」,領取 2018 最新 python 教程。


相關文章