程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Python >> python的閉包與裝飾器

python的閉包與裝飾器

編輯:Python

原文發表在我的博客主頁,轉載請注明出處

前言

如果把python當作腳本語言,每次就是寫個幾十行上百行來處理數據的話,裝飾器也許不是很必要,但是如果要開發一個大型系統,裝飾器是躲不開的,最開始體會ryu的裝飾器之美是在閱讀ryu源碼的時候,用python官網的一句話來說,learning about descriptors creates a deeper understanding of how python works and an appreciation for the elegance of its design。本篇文章從閉包講起,也從閉包結束,因為如果理解了閉包,也就理解了裝飾器。

python函數作用域LEGB

不論在什麼語言中,變量都是必不可少的,在python中,一般存在以下幾個變量類型,local:函數內部作用域;enclosing:函數內部與內嵌函數之間;global:全局作用域;build-in:內置作用域。解釋器在遇到變量的時候,會按照如下的順序進行查找:L > E > G > B,簡稱LEGB。下面以一個簡單的函數來說明上述過程:

#coding:utf-8
#global var
passline = 60
def func(val):
    # for func, it's local var
    # for in_func, it's enclosing var
    passline = 90
    if val >= passline:
        print "pass"
    else:
        print "fail"
    def in_func():
        print val
    in_func()

def Max(val1, val2):
    # max is a built-in fun
    return max(val1, val2)

f = func(89)
f()
print Max(90, 100)

local,global,build-in這三個類型比較容易理解,enclosing變量呢?下一節詳細講解。

閉包理解與使用

首先理解下python中的函數,在python中,函數是一個對象(可以通過type函數查看),在內存中占用空間;函數執行完成之後內部的變量會被解釋器回收,但是如果某變量被返回,則不會回收,因為引用計數器的值不為0;既然函數也是一個對象,他也擁有自己的屬性;對於python函數來說,返回的不一定是變量,也可以是函數。
上一節提到了enclosing變量,在func函數中又定義了一個函數in_func,它輸出了變量val,在輸出過程中查找變量的時候發現本地沒有,所以他就會去func函數裡面找並且找到了,這個變量就是enclosing變量。在上面一段代碼中,敏感的人可能已經發現了問題,分析代碼執行的過程,func函數返回了in_func函數給了f,但是沒有返回變量,所以在調用func完成之後val變量應該已經被解釋器回收,但是在執行了f函數之後卻仍然輸出了val的值89,為什麼呢?其原因就是:如果引用了enclosing作用域變量的話,會將變量添加到函數屬性中,當再次查找變量時,不是去代碼中查找,而是去函數屬性中查找。可以通過如下代碼進行驗證:

#coding:utf-8
passline = 60
def func(val):
    print "%x" %id(val)
    if val >= passline:
        print "pass"
    else:
        print "fail"
    def in_func():
        print val
    in_func()
    return in_func


f = func(89)
f() #in_func
print f.__closure__

執行上面的代碼,可以發現f函數的__closure__屬性擁有一個變量,這個變量的ID和func函數中val變量的ID一樣。
上面的代碼是用來判斷學生的成績是否及格,即在百分制中60分及格,如果現在需要添加新的功能,即150分制中90分作為及格線,如何完成代碼呢?最簡單的就是我們創建兩個函數,分別為func_100和func_150來完成判斷,判斷邏輯完全一樣,代碼如下:

#coding:utf-8

def func_150(val):
    passline = 90
    if val >= passline:
        print "pass"
    else:
        print "fail"

def func_100(val):
    passline = 60
    if val >= passline:
        print "pass"
    else:
        print "fail"

func_100(89)
func_150(89)

如果再增加應用場景呢?這樣重復而沒有任何技術含量的代碼的增添十分繁瑣,我們可以使用新的方法——閉包來解決這個問題。什麼是閉包呢?閉包就是內部函數中對enclosing作用域的變量進行引用。代碼如下:

#coding:utf-8
def set_passline(passline):
    def cmp(val):
        if val >= passline:
            print "pass"
        else:
            print "fail"
    return cmp

f_100 = set_passline(60)
f_150 = set_passline(90)
print type(f_100)
print f_100.__closure__
f_100(89)
f_150(89)

在上述代碼中,我們定義了一個set_passline函數,這個函數返回cmp函數,在這個函數中定義了cmp函數,在cmp函數中是我們之前的邏輯。在之後不論要進行多少分制的及格判斷,只需要調用set_passline函數設置及格線就好,我們以百分制60分及格為例,分析上述代碼的執行過程,基本分為兩步,一是set_passline函數把返回值cmp函數給f_100,同時將60作為屬性給cmp函數,二是f_100函數的執行其實相當於執行存儲了passline的cmp函數。
接下來考慮一個問題,上面的passline是整數,能否換成函數?既然變量和函數都是對象,而且以python的靈活性,答案是肯定的。下面的代碼同時描述了閉包的另一個應用場景,同時展示了如何使用。

#coding:utf-8
def my_sum(*arg):
    if len(arg) == 0:
        return 0
    for val in arg:
        if not isinstance(val, int):
            return 0
    return sum(arg)

def my_average(*arg):
    if len(arg) == 0:
        return 0
    for val in arg:
        if not isinstance(val, int):
            return 0
    return sum(arg)/len(arg)

def dec(func):
    def in_dec(*arg):
        if len(arg) == 0:
            return 0
        for val in arg:
            if not isinstance(val, int):
                return 0
        return func(*arg)
    return in_dec

# 1.dec return in_dec -> my_sum
# 2.my_sum = in_dec(*arg)
my_sum = dec(my_sum)

在上面的代碼中,首先定義了兩個函數,這兩個函數的功能大同小異,分別為求和函數和求平均值函數,由於求平均值元祖的長度不能為空,同時元祖中的數據都應該為整數,所以在每個函數中都先需要參數檢查,本著以人為本的方針,這部門代碼應該復用。所以在下面定義了另外一個函數dec,這個函數的參數為一個函數,返回值為一個內嵌函數,這個內嵌函數的邏輯和上面求和求平均值的邏輯一樣,先判斷,再返回結果。這個函數如何使用呢?比如現在我們要求和,代碼如下:

def my_sum(*arg):
    return sum(arg)

def dec(func):
    def in_dec(*arg):
        if len(arg) == 0:
            return 0
        for val in arg:
            if not isinstance(val, int):
                return 0
        return func(*arg)
    return in_dec

# 1.dec return in_dec -> my_sum
# 2.my_sum = in_dec(*arg)
my_sum = dec(my_sum)

my_sum = dec(my_sum)函數分兩步,首先my_sum得到dec返回的in_dec函數,其次調用in_dec函數。
所以,遵上來看,閉包可以在很大程度上實現封裝和代碼復用。

裝飾器

最開始就說,這篇博客始於閉包,終於閉包,所以裝飾器不多說,只說四句話:
1.裝飾器就是對閉包的使用;
2.裝飾器用來裝飾函數;
3.返回一個函數對象,被裝飾的函數接收;
4.被裝飾函數標識符指向返回的函數對象。

總結

接觸python裝飾器很久了,殊不知從閉包更容易理解裝飾器。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved