從with關鍵字到編寫自己簡單的ContextManager(二)

接[url=http://luozhaoyu.iteye.com/blog/1512902]上文[/url]
contextlib.contextmanager的用法是怎樣的?我摘抄一下模塊源代碼
[quote] Typical usage:

@contextmanager
def some_generator(<arguments>):
<setup>
try:
yield <value>
finally:
<cleanup>

This makes this:

with some_generator(<arguments>) as <variable>:
<body>

equivalent to this:

<setup>
try:
<variable> = <value>
<body>
finally:
<cleanup>[/quote]
大致就是可以把程序中的主體,<body>從中抽取出來,放到with塊之中。而之後處理的<finally>都可以放到some_generator函數裏面。
some_generator函數的編寫很好辦。只是contextmanager這個裝飾器應該作爲一個函數好呢還是一個類好呢?這是個很有意思的問題,我也想了好一會,等下一起討論下。先給出我的解法:
class MyGeneratorContextManager(object):
def __init__(self, gen):
print("__init__ called")
self.gen = gen


def __enter__(self):
print("__enter__ called")
return self.gen.next()


def __exit__(self, exc_type, exc_val, exc_tb):
print("__exit__called exc_type = %s, exc_val = %s, exc_tb = %s"\
% (exc_type, exc_val, exc_tb))
# 這裏沒有做異常處理,需要處理StopIteration異常
# 不是用return也可以
# 下面這句話將輸出yield [1, 2, 3]後面的的打印語句end foo
return self.gen.next()


def MyContextManager(func):
def tmpf(*args):
print("func info:", func)
return MyGeneratorContextManager(func(*args))
return tmpf


@MyContextManager
def foo(val):
# 嘗試用老方法捕捉錯誤
try:
print("start foo", val)
yield [1, 2, 3]
# 下面一行需要調用self.gen.next()才能輸出
print("end foo")
except (Exception, AssertionError):
# 但是實際上並沒有捕捉到yield中的錯誤
# except的功能完全被__exit__取代
print("EXCEPTION ENCOUNTERED!")
finally:
print("FINALLY")


print("foo is ", foo)
print("foo() is ", foo("bbbb"))
print("\nWITH INFO BELOW:")
with foo("aaaa") as tmp:
print("START WITH")
#: tmp實際上就是yield穿過來的值
print(tmp)
for i in tmp:
print(i)
assert 1>2
# 出錯之後直接從with中跳出去,下面不可能被執行
print("END WITH")

輸出結果是:

# 首先可以看到foo的值是閉包中的tmpf函數
('foo is ', <function tmpf at 0x7fb78b15f140>)
('func info:', <function foo at 0x7fb78b15f0c8>)
__init__ called
('foo() is ', <__main__.MyGeneratorContextManager object at 0x7fb78b1591d0>)

WITH INFO BELOW:
# 請看,兩次調用foo(),發現他們最終都是同一個foo函數
('func info:', <function foo at 0x7fb78b15f0c8>)
# 但是奇怪的是,函數被初始化了兩次?這是因爲這是個工廠模式,每次調用的函數雖然一樣,但是會生成不同的類
__init__ called
__enter__ called
('start foo', 'aaaa')
START WITH
[1, 2, 3]
1
2
3
# assert觸發的錯誤沒有被except捕捉到!被__exit__函數捕捉到了
__exit__called exc_type = <type 'exceptions.AssertionError'>, exc_val = , exc_tb = <traceback object at 0x7fb78b15c2d8>
end foo
FINALLY
# 爲什麼跳出StopIteration異常?這是因爲gen.next()已經走到頭了,我們沒有處理這異常
Traceback (most recent call last):
File "/home/leonardo/Aptana Studio 3 Workspace/PythonStudy/src/thinking/mycontext_manager.py", line 68, in <module>
print("END WITH")
File "/home/leonardo/Aptana Studio 3 Workspace/PythonStudy/src/thinking/mycontext_manager.py", line 31, in __exit__
return self.gen.next()
StopIteration

首先創建了一個工廠模式的MyContextManager,它實際上又是個閉包函數,也可以作爲裝飾器方便以後使用。

其次我定義了一個類MyGeneratorContextManager,這個函數在初始化的時候,就接收一個generator。請注意,接收的[b]是generator而不是function[/b]。
請看函數
def tmpf(*args):
print("func info:", func)
return MyGeneratorContextManager(func(*args))

func在這裏雖然是一個函數,但是func(*args)是一個generator,忘記傳參數就糟了

在__enter__中最緊要的是要
return self.gen.next()

因爲我們在with中面對的是一個generator,如果不對其進行next(),這個函數是不會動的。

進入到with塊之後,foo函數裏yield的值會直接傳給tmp,這個值無關緊要。with塊中所有語句就好像全部被填到yield那個地方去了一樣。

程序於是執行with塊中的語句,當其中出現異常的時候,我們外圍的except語句塊應該迅速捕捉到這一點,並輸出纔對。實際上不是,實際上當foo函數出現異常的時候,__exit__函數是第一時間捕捉到這個異常的。通過它的打印信息我們可以看出。

當處理完這個異常之後,我們調用了一下self.gen.next(),這個語句保證field語句後面的語句會被執行。最後執行了finally中的內容。你看except語句塊被完全架空了。

好,我們回過頭來再看,我們到底實現了什麼東西?我們想執行的是
[quote]<setup>
try:
<variable> = <value>
<body>
finally:
<cleanup>[/quote]
現在我們把body單獨抽出來了,放到with當中,把variable value替換成了yield value。這樣做到掐頭去尾,把前後不變的東西拿了出來,把內部的東西抽了出來。
咦?這不就是個裝飾器麼?錯。裝飾器是保持真身不懂,把前後改了,這是兩個不同的方面。


最後我們回到我一開始提出的問題,爲什麼要設計一個mycontextmanager函數?可不可以不用工廠模式,直接用MyGeneratorContextManager函數一步到位?
這有兩個前提條件要注意。第一,如果用class作爲裝飾器,這個類必須是可調用的(callable),如果將來我們要使用它,只可能它的調用__call__函數,因爲它又要實現能在call之後調用__exit__,所以它只能返回self, type(self)之類的東西。第二,因爲foo()是用在with語句中的,所以它必須是一個generator,也就是說foo.__call__()的返回結果是一個generator。那麼可以找到有這麼一個返回值,它既是self, type(self)這樣類相關的類型,而且還是一個包含yield的generator麼?:-)


要是對yield還不熟悉的朋友可能現在還不是很清楚,尤其是generator函數和with塊中代碼的關係,很可能還有一些把這代碼再折騰折騰的想法。嗯,這裏我就不寫了,有問題給我留言吧!
發佈了20 篇原創文章 · 獲贊 3 · 訪問量 5698
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章