一文讀懂 with as 語句的來龍去脈

2021-09-20 06:06:04 字數 4151 閱讀 7683

說到 with 大家通常看到的應該是這樣的:

示例 1

with open('courses.txt') as f:

for i in f:

print(i.strip())

開啟乙個檔案,然後迴圈做一些事情。但是你知道為什麼會有 with 嗎?我們自己是不是能夠寫出可以作用在 with 關鍵字上的物件呢?

with 語句的目的是簡化 try/finally 模式。這種模式用於保證一段**執行完畢後執行某項操作,即便那段**由於異常、return 語句或sys.exit() 呼叫而中止,也會執行指定的操作。finally 子句中的**通常用於釋放重要的資源,或者還原臨時變更的狀態。

示例1的功能我們可以使用 try/finally 模式實現:

示例2

try:

f = open('courses.txt')

for i in f:

print(i.strip())

finally:

f.close()

try中的 except 和 else 不是必須的,這裡為了簡單明瞭,我們只用了 finally。

對比兩個示例,我們可以看到示例1相對簡潔,這就是 with 的由來。

其實,語言中的一些特性或者說一些亮眼的特性,必然是有乙個演化的過程的,我們作為後來者和使用者應該多花一些心思去思索其背後的實現過程,相信你會收穫更多。

那麼 上下文管理器 又是什麼呢?

上下文管理器協議包含enterexit兩個方法。with 語句開始執行時,會在上下文管理器物件上呼叫enter方法。with 語句執行結束後,會在上下文管理器物件上呼叫exit方法,以此扮演 finally 子句的角色。最常見的例子是確保關閉檔案物件(示例1)。

下面我們實現乙個簡單的例子來直觀的感受一下:

class t:

def __enter__(self):

print('t.__enter__')

return '我是__enter__的返回值'

def __exit__(self, exc_type, exc_val, exc_tb):

print('t.__exit__')

with t() as t:

print(t)

輸出:

t.__enter__

我是__enter__的返回值

t.__exit__

示例3中實現了乙個類t,它的物件包含了__enter__和__exit__方法,有了這兩個方法就可以使用 with 處理該物件。執行 with 後面的表示式t()得到的是上下文管理器物件,通過as字句把物件繫結到了變數t上。

觀察輸出結果,可以看到with塊先呼叫了__enter__方法,在處理完內部邏輯(print(t))之後呼叫了exit方法,而t其實就是__enter__方法的返回值。

當然,這個例子只是為了方便我們理解上下文管理器,下面我們看乙個更有意思的例子:

示例4

obj1 = haha('你手機拿反了')

with obj1 as content:

print('哈哈鏡花緣')

print(content)

print('#### with 執行完畢後,再輸出content: ####')

print(content)

輸出:

緣花鏡哈哈

了反拿機手你

#### with 執行完畢後,在輸出content: ####

你手機拿反了

示例4中,上下文管理器是 haha 類的例項,python 呼叫此例項的enter方法,把返回結果繫結到 變數content 上。

列印乙個字串,然後列印 content 變數的值。可以看到列印出的內容都是是反向的。

最後,當 with 塊已經執行完畢。可以看出,__enter__ 方法返回的值——即儲存在 content 變數中的值——是字串 『你手機拿反了』。

輸出不再是反向的了。

haha類的實現:

import sys

class haha:

def __init__(self, word):

self.word = word

def reverse_write(self, text):

self.original_write(text[::-1])

def __enter__(self):

self.original_write = sys.stdout.write

sys.stdout.write = self.reverse_write

return self.word

def __exit__(self, exc_type, exc_value, traceback):

sys.stdout.write = self.original_write

return true

在__enter__方法中,我們接管了標準輸出,將其替換成我們自己編寫的方法reverse_write,reverse_write方法將引數內容反轉。而在__exit__方法中,我們將標準輸出還原。__exit__方法需要返回true。

總之,with之於上下文管理器,就像for之於迭代器一樣。with就是為了方便上下文管理器的使用。

上下文管理器特性在標準庫中有一些應用:

在 sqlite3 模組中用於管理事務;

在 threading 模組中用於維護鎖、條件和訊號;

另外,說到上下文管理器就不得不提一下@contextmanager 裝飾器,它能減少建立上下文管理器的樣板**量,因為不用編寫乙個完整的類,定義enter__和 __exit方法,而只需實現有乙個 yield 語句的生成器,生成想讓enter方法返回的值。

在使用 @contextmanager 裝飾的生成器中,yield 語句的作用是把函式的定義體分成兩部分:

yield 語句前面的所有**在 with 塊開始時(即直譯器呼叫enter方法時)執行

yield 語句後面的**在with 塊結束時(即呼叫exit方法時)執行。

下面我們用 @contextmanager 裝飾器來實現一下示例4的功能:

示例5

import sys

import contextlib

@contextlib.contextmanager

def woha(n):

original_write = sys.stdout.write

def reverse_write(text):

original_write(text[::-1])

sys.stdout.write = reverse_write

yield n

sys.stdout.write = original_write

return true

obj1 = woha('你手機拿反了')

with obj1 as content:

print('哈哈鏡花緣')

print(content)

print('#### with 執行完畢後,在輸出content: ####')

print(content)

輸出:

緣花鏡哈哈

了反拿機手你

#### with 執行完畢後,在輸出content: ####

你手機拿反了

這裡我們需要注意的是:**執行到yield時,會產出乙個值,這個值會繫結到 with 語句中 as 子句的變數上。執行 with 塊中的**時,這個函式會在yield這裡暫停。此時,相當於示例4中執行完__enter__方法。而控制權一旦跳出 with 塊(塊內**執行完畢)則繼續執行 yield 語句之後的**。

@contextmanager 裝飾器優雅且實用,把三個不同的 python 特性結合到了一起:函式裝飾器、生成器和 with 語句。

現在,我想你應該能夠解答開篇提到的兩個問題了吧!

一文讀懂Nginx

問 nginx的負載均衡演算法有什麼?預設是什麼演算法?答 1 輪詢 按請求的時間輪詢查空閒的後端伺服器 2 指定輪詢機率 機率的原因是後端伺服器的效能不均勻,好的多分點,差的少分點 3 固定ip繫結固定伺服器 預設是加權輪詢,就是優先訪問權重高的伺服器 問 nginx是單執行緒的嗎?答 是單執行緒...

一文讀懂SpringMVC

主要講的是dispatcherservlet這個類 ioc其實是乙個map,工程啟動後掃瞄路徑,根據類的全限定名建立bean 問 怎麼根據路徑找到方法?map還存key為 aaa value為該controller例項 問 autowired原理?自定義註解,在載入的時候,掃瞄controller層...

堆疊 一文讀懂

堆疊 stack 是一種先進後出的 操作受限的線性表,也可以直接稱為棧。可以把棧想象成乙個桶一樣,往這個桶裡面一層一層的放東西,先放進去的在裡面,後放進去的東西依次在外面。但取東西的時候就是先取靠近外面的,再依次一層層取裡面的。這就是 後進先出 last in first out 的原則。因此 棧 ...