閉包:用函式代替類
有時我們會定義只有一個方法(除了__init__()之外)的類,而這種類可以通過使用閉包(closure)來替代,閉包是被外層函式包圍的內層函式,它能夠獲取外層函式范圍中的變數(即使外層函式已執行完畢),因此閉包可以保存額外的變數環境,用于在函式呼叫時使用,考慮下面這個例子,這個類允許用戶通過某種模板方案來獲取URL,
from urllib.request import urlopen
class UrlTemplate:
def __init__(self, template) -> None:
self.template = template
def open(self, **kwargs):
return urlopen(self.template.format_map(kwargs))
yahoo = UrlTemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}')
for line in yahoo.open(names='IBM,AAPL,FB', fields = 'sllclv'):
print(line.decode('utf-8'))
這個類可以用一個簡單的函式來替代:
def urltempalte(template):
# 后面用物件再次呼叫函式的時候傳入kwargs
def opener(**kwargs):
return urlopen(template.format_map(kwargs))
return opener
yahoo = urltempalte('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}')
for line in yahoo(names='IBM,AAPL,FB', fields = 'sllclv'):
print(line.decode('utf-8'))
在許多情況下,我們會使用只有單個方法的類的原因是需要保存額外的狀態給類方法使用,我們上面提到的UrlTemplate類的唯一目的就是將template的值保存在某處,然后在open()方法中使用它,而使用閉包來解決該問題會更加簡短和優雅,我們使用opener()函式記住引數template的值,然后再隨后的呼叫中使用該值,
所以大家在撰寫代碼中需要附加額外的狀態給函式時,一定要考慮使用閉包,
訪問定義在閉包的內的變數
我們知道閉包內層可以用來存盤函式需要用到的變數環境,接下來,我們可以通過函式來擴展閉包,使得在閉包內層定義的變數可以被訪問和修改,
一般來說,閉包內層定義的變數對外界來說是完全隔離的,如果想要訪問和修改它們,需要撰寫存取函式(accessor function, 即getter/setter方法),并將它們做為函式屬性附加到閉包上來提供對內層變數的訪問支持:
def sample():
n = 0
# 閉包函式
def func():
print("n =", n)
# 存取函式(accessor function),即getter/setter方法
def get_n():
return n
def set_n(value):
# 必須要加nolocal才能修改內層變數
nonlocal n
n = value
# 做為函式屬性附加
func.get_n = get_n
func.set_n = set_n
return func
該演算法測驗運行結果如下:
f = sample()
f() # n = 0
f.set_n(10)
f() # n = 10
print(f.get_n()) # 10
可以看到,get_n()和set_n()作業起來很像實體的方法,注意一定要將get_n()和set_n()做為函式屬性附加上去,否則在呼叫set_n()和get_n()就會報錯:'function' object has no attribute 'set_n',
如果我們希望讓閉包完全模擬成類實體,我們需要架構內層函式拷貝到一個實體的字典中然后將它回傳,示例如下:
import sys
class ClosureInstance:
def __init__(self, locals=None) -> 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()
print(s) # <__main__.ClosureInstance object at 0x101efc280>
s.push(10)
s.push(20)
s.push('Hello')
print(len(s)) # 3
print(s.pop()) # Hello
print(s.pop()) # 20
print(s.pop()) # 10
用閉包模型類的功能比傳統的類實作方法要快一些,比如我們用下面這個類做為測驗對比,
class Stack2:
def __init__(self) -> None:
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
s = Stack()
print(timeit('s.push(1);s.pop()', 'from __main__ import s'))
# 0.98746542
s = Stack2()
print(timeit('s.push(1);s.pop()', 'from __main__ import s'))
# 1.07070521
可以看到采用閉包的版本要快大約8%,因為對于實體而言,測驗話費的大部分時間都在對實體變數的訪問上,而閉包要更快一些,因為不用涉及額外的self變數,
不過這種奇技淫巧在代碼中還是要謹慎使用,因為相比一個真正的類,這種方法其實是相當怪異的,像繼承、屬性、描述符或者類方法這樣的特性在這種方法都是無法使用的,而且我們還需要一些花招才能讓特殊方法正常作業(比如我們上面ClosureInstance中對__len__()的實作),不過,這仍然是一個非常有學術價值的例子,它告訴我們對閉包內部提供訪問機制能夠實作怎樣的功能,
參考文獻
- [1] https://www.python.org/
- [2] Martelli A, Ravenscroft A, Ascher D. Python cookbook[M]. " O'Reilly Media, Inc.", 2005.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/347011.html
標籤:其他
下一篇:花生殼內網穿透遠程訪問工業路由器
