[Python]閉包的理解和使用

閉包廣泛使用在函數式編程語言中,雖然不是很容易理解,但是又不得不理解。

閉包是什麼?

在一些語言中,在函數中可以(嵌套)定義另一個函數時,如果內部的函數引用了外部的函數的變量,則可能產生閉包。閉包可以用來在一個函數與一組“私有”變量之間創建關聯關係。在給定函數被多次調用的過程中,這些私有變量能夠保持其持久性。
—— 維基百科)

舉個例子

def sum(a,b):
   return a+b

def sum1(a):
    def add(b):
         return a+b  #a爲外部變量
    return add  #返回函數

type(sum(1,2))  #<class 'int'>
type(sum1(1))   #<class 'function'>

一般支持將函數當做對象使用的編程語言,如Python,JavaScript都支持閉包

如何理解閉包

閉包存在的意義是夾帶了外部變量,如果沒有的話,其實和普通函數沒有區別。同一個函數夾帶了不同的私貨就是不同的閉包,可以理解爲對函數的輕量級的封裝。

下面這個例子是計算數字的平方和立方,如果是用普通函數,要麼是需要寫兩個函數,要麼需要傳兩個參數

def rlt(v):
   def product(num):
        return num ** v
   return product

square = rlt(2)
cube = rlt(3)

print(square(2), cube(2))  # 4, 8

閉包相當於固定了某些變量,對使用者來說就便捷了很多,下面會講到閉包的原理

總結下:
閉包其實和普通函數的區別:
1、普通函數傳遞變量,閉包傳遞函數
2、閉包的封裝性更好,調用的參數更少

什麼時候用閉包?

1. 裝飾器

閉包在python中非常常見,但是可能很難意識到閉包的存在。這裏不得不提到Python中的裝飾器Decorator。
裝飾器顧名思義是裝飾作用。在代碼運行期間動態增加功能的方式,稱之爲“裝飾器”(Decorator)。

舉個例子,
計算函數運行時間,正常寫法

import  time

def do_sth():
    time.sleep(3)


startTime = time.time()
do_sth()
endTime = time.time()
print("do_sth run {} ".format(endTime-startTime))   #do_sth run 3.0005998611450195

如果我們要計算別的函數運行時間,就要重複多次代碼,我們把這些重複代碼放到裝飾器裏去,如下面代碼

import  time

def timer(fun):
    def wrapper():
        startTime = time.time()
        fun()
        endTime = time.time()
        print("{} run {}".format(func.__name__, endTime - startTime))
    return wrapper

@timer
def do_sth():
    time.sleep(3)

timer(do_sth)()  # 一:不加@timer語法糖的調用方式
do_sth()  #二:加@timer語法糖的調用方式, 和方式一等價

@timer放到do_sth的函數定義前,相當於執行了
do_sth = timer(do_sth)

裝飾器Pythonic的調用方式完全和普通函數調用方式一樣,是不是很方便?

如果裝飾器需要帶參數呢?那就需要再加一層,用於接收這些函數。又是一層的閉包。

import  time

def timer(text):
    def decorator(fun):
        def wrapper():
            startTime = time.time()
            fun()
            endTime = time.time()
            print("{} {} total run time:{}".format(text, fun.__name__, endTime - startTime))
        return wrapper
    return decorator

@timer('excute')
def do_sth():
    time.sleep(3)

三層嵌套的效果是
do_sth = timer('excute')(do_sth)
想一想下面的代碼打印結果是什麼?

print(do_sth.__name__)  
print(timer('excute').__name__)
print(timer('excute')(do_sth).__name__)

do_sth.__name__的結果不再是do_sth。這裏需要把原始函數的__name__等屬性賦值到wrapper()函數中,否則,有些依賴函數簽名的代碼執行就會出錯。

wrapper.__name__ = func.__name__ ? 不用,Python內置的functools.wraps就是做這個的
另外,之前寫的wrapper是不帶參數的,只適配不帶參數的函數調用,如果是doActive(active, f)則無法使用。所以更新定義:def wrapper(*args, **kw):

於是,一個完整的不帶參數的decorator的寫法如下:

import time
import functools

def timer(fun):
    @functools.wraps(fun)
    def wrapper(*args, **kw):
        startTime = time.time()
        fun(*args, **kw)
        endTime = time.time()
        print("{} {} total run time:{}".format(fun.__name__, endTime - startTime))
    return wrapper

@timer
def do_sth():
    time.sleep(3)

print(do_sth.__name__)   #do_sth

試試改寫上面的帶參數的decorator

import  time
import functools

def timer(text):
    @functools.wraps(timer)
    def decorator(fun):
        @functools.wraps(fun)
        def wrapper():
            startTime = time.time()
            fun()
            endTime = time.time()
            print("{} {} total run time:{}".format(text, fun.__name__, endTime - startTime))
        return wrapper
    return decorator

@timer('excute')
def do_sth():
    time.sleep(3)

這次如下代碼的運行結果是?

print(do_sth.__name__)
print(timer('excute').__name__)
print(timer('excute')(do_sth).__name__)

2. 惰性求值

常用於數據庫訪問的時候

# 僞代碼示意

class QuerySet(object):
    def __init__(self, sql):
        self.sql = sql
        self.db = Mysql.connect().corsor()  # 僞代碼

    def __call__(self):
        return db.execute(self.sql)

def query(sql):
    return QuerySet(sql)

result = query("select name from user_app")
if time > now:
    print result  # 這時才執行數據庫訪問

上面這個不太恰當的例子展示了通過閉包完成惰性求值的功能,但是上面query返回的結果並不是函數,而是具有函數功能的類。有興趣的可以去看看Django的queryset的實現,原理類似。

3.需要對某個函數的參數提前賦值

Python中已經有了很好的解決訪問 functools.parial,但是用閉包也能實現。

def partial(**outer_kwargs):
    def wrapper(func):
        def inner(*args, **kwargs):
            for k, v in outer_kwargs.items():
                kwargs[k] = v
            return func(*args, **kwargs)
        return inner
    return wrapper

@partial(age=15)
def say(name=None, age=None):
    print name, age

say(name="the5fire")
# 當然用functools比這個簡單多了
# 只需要: functools.partial(say, age=15)(name='the5fire')

python偏函數int2 = functools.partial(int, base=2),可以類比C++的bind1st, bind2nd

閉包的原理?

閉包其實也是一種函數,普通函數的__closure__None,閉包裏是是一個元組,存放着所有的cell對象,每個cell`對象保存着這個閉包裏所有的外部變量。

def sum(a, b):
    return  a+b
print(sum.__closure__)  #None

def rlt(v):
   def product(num):
        return num ** v
   return product

square = rlt(2)
cube = rlt(3)

print(square.__closure__)    #(<cell at 0x0000000001E2F768: int object at 0x000007FEF25E62B0>,)
for x in square.__closure__:
    print(x.cell_contents)   #2

print(cube.__closure__)
for x in cube.__closure__: #(<cell at 0x0000000001E2F798: int object at 0x000007FEF25E62D0>,)
    print(x.cell_contents) #3

參考資料:
廖雪峯 Python裝飾器
Python中的閉包

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章