with 在 Python 是個很神奇的功能。他看似只在幫你開檔案的時候幫你,也不知道真正做了什麼,但是所有人都叫你開檔案的時候包一下。其實它的用處還有很多有趣的作法呢 :)

其實 with 他是個在 python 的 code block 前後幫你偷偷做事情的東西,在經典的開檔案例子中,他就會幫你偷偷在最後把檔案關掉,以免你的 file descripter 用太多個。

1
2
3
with open("file.txt") as f:
    print(f.read())
    # don't need to close f 

但是這個應用實在太無聊哈哈,他既然可以幫你關檔案,當然可以幫你把更多東西藏起來。下面我們用個 API Server 當做例子。我們通常可能會需要在處理完邏輯之後做一個回應的動作(你語言癌嗎)。

1
2
3
4
5
6
7
8
9
10
11
12
13
try:
    # do something
    if success:
        status_code = 200
        rtMsg = "We are good!"
    else:
        status_code = 400
        rtMsg = "Are you ok??"
except:
    status_code = 500
    rtMsg = "Oooops, internal error"

send(status_code, rtMsg)

這樣的寫法會讓回應的程式和主要的邏輯混在一起。有些 status code回應的訊息其實非常罐頭,並不需要穿插在主要邏輯裡面使得程式可讀性變得很差,也讓人很煩躁。

好在,在 Python 中有個內建的函式庫叫做 contextlib 可以幫你做出自己的 with block ,把這些東西都打包起來。

首先,讓我們先定義一個處理各種回應的髒東西的 helper class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class resultHandler(object):
    def __init__(self):
        # might have some other params
        self.msg = ""
        self.status_code = 200
        self.rtMsg = ""

    def success(self, msg):
        self.msg = "Success"
        self.rtMsg = msg

    def fail(self, type, msg):
        self.msg = "Failed"
        self.status_code = type
        self.rtMsg = msg

    def teapot(self):
        self.msg = "Dunno"
        self.status_code = 418
        self.rtMsg = "What do you want from a teapot?!"

    def intError(self, e):
        self.msg = "Internal Error"
        self.status_code = 500
        self.rtMsg = str(e)

    def _send(self):
        # do actual returning
        print(self.status_code, self.msg, self.rtMsg)

這樣我們可以把很多髒東西都打包起來,如果有需要有更複雜或是有些常用的狀況類似 400 找不到東西或是 418 我是一個茶壺(誰會常用這東西拉),都可以把它變成一個 method。

再來,我們就要來做自己的 with block 了。

1
2
3
4
5
6
7
8
9
10
11
12
from contextlib import contextmanager
@contextmanager
def apiWrapper(**some_param):
    # pre-checkings, like authentication
    do_checking(**some_param)
    control = resultHandler()
    try:
        yield control
    except Exception as e:
        control.intError(e)
    finally:
        control._send()

透過 @contextmanger 這個函式裝飾,我們可以讓他變成一個可以是包含外部定義內容的函式。在 contextlib 中還有可以嚴格定義成物件的做法,有興趣可以去官方文件看看,他們有附上精美的例子 :) 。在這裡,使用 yield 就是執行外部定義的程式,後面接的變數就是可以送到外部去的變數。在這裡我們把控制回應狀態的 control 變數送過去,讓主要邏輯可以控制回應的內容。

而在前後我們可以做些常規的動作,類似在前面做權限的確認,或是取得公用的變數等等。我們也可以把 try except 打包在這裡面,500 Internal Error 就可以自動被處理,也可以依照是否是 debug mode 來決定要輸出多細部的 Python Exception ,甚至也可以偷偷做 logging 。

最後讓我們看看外面怎麼使用:

1
2
3
4
5
6
7
8
with apiWrapper() as ctl:
    print("do things")
    ctl.success("good")
    
# Output:
# >> I'm checking things :))
# >> do things
# >> 200 Success good

像是這樣,就可以只處理主要邏輯,而其他髒髒的錯誤處理或是繁複的 http status return 等等都可以交給 resultHandler 來處理。這樣可以留給主要邏輯足夠的彈性,也可以把髒的東西打包起來。

讓我們看看如果有錯的話會發生什麼事情:

1
2
3
4
5
6
7
with apiWrapper() as ctl:
    print(aaaaa)
    ctl.success("good")

# Output:
# >> I'm checking things :))
# >> 500 Internal Error name 'aaaaa' is not defined

如果們所願的,他會自動幫我們 try excpet 起來,並提供相對應的錯誤資訊。

當然,這不只是可以這麼使用。常用的用法也可以是 stdout/stderr 的重導向。只要是有很多區塊是需要有前置作業、後期處理,都可以用 with 打包起來讓程式看起來更乾淨嚕 :)

剛好好朋友問我,之前在做 cython 的 stdout/stderr 重導向時看到這東西,發現其實非常好用。順手寫了個範例,想說就這樣只給他有點太浪費了,就拿來寫篇部落格啦XD 但是之前打了兩千字的東西並不是這篇,希望那篇可以早日完成不要難產了哈哈。如果正巧問我程式,我也正巧有空回答,我又很巧地寫了範例,那記得提醒我拿來發部落格哈哈哈~

  • Posted at June 20, 2018