7.12 訪問閉包中定義的變量

2018-02-24 15:26 更新

問題

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

解決方案

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

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
>>>

討論

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

還可以進一步的擴展,讓閉包模擬類的實例。你要做的僅僅是復制上面的內(nèi)部函數(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()

下面是一個交互式會話來演示它是如何工作的:

>>> 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
>>>

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

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)

如果這樣做,你會得到類似如下的結果:

>>> 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
>>>

結果顯示,閉包的方案運行起來要快大概8%,大部分原因是因為對實例變量的簡化訪問,閉包更快是因為不會涉及到額外的self變量。

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

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號