使用 Python 函數進行模組化

2019-09-01 23:43:00

使用 Python 函數來最大程度地減少重複任務編碼工作量。

你是否對函數、類、方法、庫和模組等花哨的程式設計術語感到困惑?你是否在與變數作用域鬥爭?無論你是自學成才的還是經過正式培訓的程式設計師,程式碼的模組化都會令人困惑。但是類和庫鼓勵模組化程式碼,因為模組化程式碼意味著只需構建一個多用途程式碼塊集合,就可以在許多專案中使用它們來減少編碼工作量。換句話說,如果你按照本文對 Python 函數的研究,你將找到更聰明的工作方法,這意味著更少的工作。

本文假定你對 Python 很熟(LCTT 譯註:稍微熟悉就可以),並且可以編寫和執行一個簡單的指令碼。如果你還沒有使用過 Python,請首先閱讀我的文章:Python 簡介

函數

函數是邁向模組化過程中重要的一步,因為它們是形式化的重複方法。如果在你的程式中,有一個任務需要反復執行,那麼你可以將程式碼放入一個函數中,根據需要隨時呼叫該函數。這樣,你只需編寫一次程式碼,就可以隨意使用它。

以下一個簡單函數的範例:

#!/usr/bin/env python3import timedef Timer():    print("Time is " + str(time.time() ))

建立一個名為 mymodularity 的目錄,並將以上函數程式碼儲存為該目錄下的 timestamp.py

除了這個函數,在 mymodularity 目錄中建立一個名為 __init__.py 的檔案,你可以在檔案管理器或 bash shell 中執行此操作:

$ touch mymodularity/__init__.py

現在,你已經建立了屬於你自己的 Python 庫(Python 中稱為“模組”),名為 mymodularity。它不是一個特別有用的模組,因為它所做的只是匯入 time 模組並列印一個時間戳,但這只是一個開始。

要使用你的函數,像對待任何其他 Python 模組一樣對待它。以下是一個小應用,它使用你的 mymodularity 軟體包來測試 Python sleep() 函數的準確性。將此檔案儲存為 sleeptest.py,注意要在 mymodularity 資料夾 之外,因為如果你將它儲存在 mymodularity 裡面,那麼它將成為你的包中的一個模組,你肯定不希望這樣。

#!/usr/bin/env python3import timefrom mymodularity import timestampprint("Testing Python sleep()...")# modularitytimestamp.Timer()time.sleep(3)timestamp.Timer()

在這個簡單的指令碼中,你從 mymodularity 包中呼叫 timestamp 模組兩次。從包中匯入模組時,通常的語法是從包中匯入你所需的模組,然後使用 模組名稱 + 一個點 + 要呼叫的函數名(例如 timestamp.Timer())。

你呼叫了兩次 Timer() 函數,所以如果你的 timestamp 模組比這個簡單的例子複雜些,那麼你將節省大量重複程式碼。

儲存檔案並執行:

$ python3 ./sleeptest.pyTesting Python sleep()...Time is 1560711266.1526039Time is 1560711269.1557732

根據測試,Python 中的 sleep 函數非常準確:在三秒鐘等待之後,時間戳成功且正確地增加了 3,在微秒單位上差距很小。

Python 庫的結構看起來可能令人困惑,但其實它並不是什麼魔法。Python 被程式設計 為一個包含 Python 程式碼的目錄,並附帶一個 __init__.py 檔案,那麼這個目錄就會被當作一個包,並且 Python 會首先在當前目錄中查詢可用模組。這就是為什麼語句 from mymodularity import timestamp 有效的原因:Python 在當前目錄查詢名為 mymodularity 的目錄,然後查詢 timestamp.py 檔案。

你在這個例子中所做的功能和以下這個非模組化的版本是一樣的:

#!/usr/bin/env python3import timefrom mymodularity import timestampprint("Testing Python sleep()...")# no modularityprint("Time is " + str(time.time() ) )time.sleep(3)print("Time is " + str(time.time() ) )

對於這樣一個簡單的例子,其實沒有必要以這種方式編寫測試,但是對於編寫自己的模組來說,最佳實踐是你的程式碼是通用的,可以將它重用於其他專案。

通過在呼叫函數時傳遞資訊,可以使程式碼更通用。例如,假設你想要使用模組來測試的不是 系統sleep 函數,而是 使用者自己實現sleep 函數,更改 timestamp 程式碼,使它接受一個名為 msg 的傳入變數,它將是一個字串,控制每次呼叫 timestamp 時如何顯示:

#!/usr/bin/env python3import time# 更新程式碼def Timer(msg):    print(str(msg) + str(time.time() ) )

現在函數比以前更抽象了。它仍會列印時間戳,但是它為使用者列印的內容 msg 還是未定義的。這意味著你需要在呼叫函數時定義它。

Timer 函數接受的 msg 引數是隨便命名的,你可以使用引數 mmessagetext,或是任何對你來說有意義的名稱。重要的是,當呼叫 timestamp.Timer 函數時,它接收一個文字作為其輸入,將接收到的任何內容放入 msg 變數中,並使用該變數完成任務。

以下是一個測試測試使用者正確感知時間流逝能力的新程式:

#!/usr/bin/env python3from mymodularity import timestampprint("Press the RETURN key. Count to 3, and press RETURN again.")input()timestamp.Timer("Started timer at ")print("Count to 3...")input()timestamp.Timer("You slept until ")

將你的新程式儲存為 response.py,執行它:

$ python3 ./response.pyPress the RETURN key. Count to 3, and press RETURN again.Started timer at 1560714482.3772075Count to 3...You slept until 1560714484.1628013

函數和所需引數

新版本的 timestamp 模組現在 需要 一個 msg 引數。這很重要,因為你的第一個應用程式將無法執行,因為它沒有將字串傳遞給 timestamp.Timer 函數:

$ python3 ./sleeptest.pyTesting Python sleep()...Traceback (most recent call last):  File "./sleeptest.py", line 8, in <module>    timestamp.Timer()TypeError: Timer() missing 1 required positional argument: 'msg'

你能修復你的 sleeptest.py 應用程式,以便它能夠與更新後的模組一起正確執行嗎?

變數和函數

通過設計,函數限制了變數的範圍。換句話說,如果在函數內建立一個變數,那麼這個變數 在這個函數內起作用。如果你嘗試在函數外部使用函數內部出現的變數,就會發生錯誤。

下面是對 response.py 應用程式的修改,嘗試從 timestamp.Timer() 函數外部列印 msg 變數:

#!/usr/bin/env python3from mymodularity import timestampprint("Press the RETURN key. Count to 3, and press RETURN again.")input()timestamp.Timer("Started timer at ")print("Count to 3...")input()timestamp.Timer("You slept for ")print(msg)

試著執行它,檢視錯誤:

$ python3 ./response.pyPress the RETURN key. Count to 3, and press RETURN again.Started timer at 1560719527.7862902Count to 3...You slept for 1560719528.135406Traceback (most recent call last):  File "./response.py", line 15, in <module>    print(msg)NameError: name 'msg' is not defined

應用程式返回一個 NameError 訊息,因為沒有定義 msg。這看起來令人困惑,因為你編寫的程式碼定義了 msg,但你對程式碼的了解比 Python 更深入。呼叫函數的程式碼,不管函數是出現在同一個檔案中,還是打包為模組,都不知道函數內部發生了什麼。一個函數獨立地執行它的計算,並返回你想要它返回的內容。這其中所涉及的任何變數都只是 原生的:它們只存在於函數中,並且只存在於函數完成其目的所需時間內。

Return 語句

如果你的應用程式需要函數中特定包含的資訊,那麼使用 return 語句讓函數在執行後返回有意義的資料。

時間就是金錢,所以修改 timestamp 函數,以使其用於一個虛構的收費系統:

#!/usr/bin/env python3import timedef Timer(msg):    print(str(msg) + str(time.time() ) )    charge = .02    return charge

現在,timestamp 模組每次呼叫都收費 2 美分,但最重要的是,它返回每次呼叫時所收取的金額。

以下一個如何使用 return 語句的演示:

#!/usr/bin/env python3from mymodularity import timestampprint("Press RETURN for the time (costs 2 cents).")print("Press Q RETURN to quit.")total = 0while True:    kbd = input()    if kbd.lower() == "q":        print("You owe $" + str(total) )        exit()    else:        charge = timestamp.Timer("Time is ")        total = total+charge

在這個範例程式碼中,變數 chargetimestamp.Timer() 函數的返回,它接收函數返回的任何內容。在本例中,函數返回一個數位,因此使用一個名為 total 的新變數來跟蹤已經進行了多少更改。當應用程式收到要退出的信號時,它會列印總花費:

$ python3 ./charge.pyPress RETURN for the time (costs 2 cents).Press Q RETURN to quit.Time is 1560722430.345412Time is 1560722430.933996Time is 1560722434.6027434Time is 1560722438.612629Time is 1560722439.3649364qYou owe $0.1

行內函式

函數不必在單獨的檔案中建立。如果你只是針對一個任務編寫一個簡短的指令碼,那麼在同一個檔案中編寫函數可能更有意義。唯一的區別是你不必匯入自己的模組,但函數的工作方式是一樣的。以下是時間測試應用程式的最新疊代:

#!/usr/bin/env python3import timetotal = 0def Timer(msg):    print(str(msg) + str(time.time() ) )    charge = .02    return chargeprint("Press RETURN for the time (costs 2 cents).")print("Press Q RETURN to quit.")while True:    kbd = input()    if kbd.lower() == "q":        print("You owe $" + str(total) )        exit()    else:        charge = Timer("Time is ")        total = total+charge

它沒有外部依賴(Python 發行版中包含 time 模組),產生與模組化版本相同的結果。它的優點是一切都位於一個檔案中,缺點是你不能在其他指令碼中使用 Timer() 函數,除非你手動複製和貼上它。

全域性變數

在函數外部建立的變數沒有限制作用域,因此它被視為 全域性 變數。

全域性變數的一個例子是在 charge.py 中用於跟蹤當前花費的 total 變數。total 是在函數之外建立的,因此它系結到應用程式而不是特定函數。

應用程式中的函數可以存取全域性變數,但要將變數傳入匯入的模組,你必須像傳送 msg 變數一樣將變數傳入模組。

全域性變數很方便,因為它們似乎隨時隨地都可用,但也很難跟蹤它們,很難知道哪些變數不再需要了但是仍然在系統記憶體中停留(儘管 Python 有非常好的垃圾收集機制)。

但是,全域性變數很重要,因為不是所有的變數都可以是函數或類的本地變數。現在你知道了如何向函數傳入變數並獲得返回,事情就變得容易了。

總結

你已經學到了很多關於函數的知識,所以開始將它們放入你的指令碼中 —— 如果它不是作為單獨的模組,那麼作為程式碼塊,你不必在一個指令碼中編寫多次。在本系列的下一篇文章中,我將介紹 Python 類。