7.12 訪問閉包中定義的變量

2018-02-24 15:26 更新

問題

你想要擴(kuò)展函數(shù)中的某個(gè)閉包,允許它能訪問和修改函數(shù)的內(nèi)部變量。

解決方案

通常來講,閉包的內(nèi)部變量對(duì)于外界來講是完全隱藏的。但是,你可以通過編寫訪問函數(shù)并將其作為函數(shù)屬性綁定到閉包上來實(shí)現(xiàn)這個(gè)目的。例如:

def sample():
    n = 0
    # Closure function
    def func():
        print('n=', n)

    # Accessor methods for n
    def get_n():
        return n

    def set_n(value):
        nonlocal n
        n = value

    # Attach as function attributes
    func.get_n = get_n
    func.set_n = set_n
    return func

下面是使用的例子:

>>> f = sample()
>>> f()
n= 0
>>> f.set_n(10)
>>> f()
n= 10
>>> f.get_n()
10
>>>

討論

為了說明清楚它如何工作的,有兩點(diǎn)需要解釋一下。首先,nonlocal 聲明可以讓我們編寫函數(shù)來修改內(nèi)部變量的值。其次,函數(shù)屬性允許我們用一種很簡單的方式將訪問方法綁定到閉包函數(shù)上,這個(gè)跟實(shí)例方法很像(盡管并沒有定義任何類)。

還可以進(jìn)一步的擴(kuò)展,讓閉包模擬類的實(shí)例。你要做的僅僅是復(fù)制上面的內(nèi)部函數(shù)到一個(gè)字典實(shí)例中并返回它即可。例如:

import sys
class ClosureInstance:
    def __init__(self, locals=None):
        if locals is None:
            locals = sys._getframe(1).f_locals

        # Update instance dictionary with callables
        self.__dict__.update((key,value) for key, value in locals.items()
                            if callable(value) )
    # Redirect special methods
    def __len__(self):
        return self.__dict__['__len__']()

# Example use
def Stack():
    items = []
    def push(item):
        items.append(item)

    def pop():
        return items.pop()

    def __len__():
        return len(items)

    return ClosureInstance()

下面是一個(gè)交互式會(huì)話來演示它是如何工作的:

>>> s = Stack()
>>> s
<__main__.ClosureInstance object at 0x10069ed10>
>>> s.push(10)
>>> s.push(20)
>>> s.push('Hello')
>>> len(s)
3
>>> s.pop()
'Hello'
>>> s.pop()
20
>>> s.pop()
10
>>>

有趣的是,這個(gè)代碼運(yùn)行起來會(huì)比一個(gè)普通的類定義要快很多。你可能會(huì)像下面這樣測試它跟一個(gè)類的性能對(duì)比:

class Stack2:
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def __len__(self):
        return len(self.items)

如果這樣做,你會(huì)得到類似如下的結(jié)果:

>>> from timeit import timeit
>>> # Test involving closures
>>> s = Stack()
>>> timeit('s.push(1);s.pop()', 'from __main__ import s')
0.9874754269840196
>>> # Test involving a class
>>> s = Stack2()
>>> timeit('s.push(1);s.pop()', 'from __main__ import s')
1.0707052160287276
>>>

結(jié)果顯示,閉包的方案運(yùn)行起來要快大概8%,大部分原因是因?yàn)閷?duì)實(shí)例變量的簡化訪問,閉包更快是因?yàn)椴粫?huì)涉及到額外的self變量。

Raymond Hettinger對(duì)于這個(gè)問題設(shè)計(jì)出了更加難以理解的改進(jìn)方案。不過,你得考慮下是否真的需要在你代碼中這樣做,而且它只是真實(shí)類的一個(gè)奇怪的替換而已,例如,類的主要特性如繼承、屬性、描述器或類方法都是不能用的。并且你要做一些其他的工作才能讓一些特殊方法生效(比如上面 ClosureInstance 中重寫過的 __len__() 實(shí)現(xiàn)。)

最后,你可能還會(huì)讓其他閱讀你代碼的人感到疑惑,為什么它看起來不像一個(gè)普通的類定義呢?(當(dāng)然,他們也想知道為什么它運(yùn)行起來會(huì)更快)。盡管如此,這對(duì)于怎樣訪問閉包的內(nèi)部變量也不失為一個(gè)有趣的例子。

總體上講,在配置的時(shí)候給閉包添加方法會(huì)有更多的實(shí)用功能,比如你需要重置內(nèi)部狀態(tài)、刷新緩沖區(qū)、清除緩存或其他的反饋機(jī)制的時(shí)候。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)