譯者 | 布加迪
審校 | 重樓
是否曾經將您的Python代碼與經驗豐富的開發人員的代碼進行比較,感到明顯的差異?盡管可以從在線資源學習Python,但初學者代碼和專家代碼之間通常存在差距。這是由于經驗豐富的開發人員堅持奉行社區確保的最佳實踐。這些實踐在在線教程中經常被忽視,但對于大規模應用程序而言至關重要。我在本文中將介紹生產級代碼中使用的7個技巧,確保代碼更清晰、更有條理。
Python是一種動態類型的編程語言,在運行時推斷變量類型。雖然它提高了靈活性,但在協作環境中大大降低了代碼的可讀性和可理解性。
Python支持函數聲明中的類型提示,充當函數參數類型和返回類型的注釋。盡管Python在運行時不強制執行這些類型,但它仍然很有幫助,因為它使您的代碼更容易被其他人和您自己理解。
從一個基本的例子開始,這是一個帶類型提示的簡單函數聲明:
Def sum(a: int, b: int) -> int:Return a + b
在這里,盡管函數不言自明,但我們看到函數參數和返回值都表示為int類型。函數體可以是一行,也可以是幾百行。然而,我們只需查看函數聲明就能理解前置條件和返回類型。
其中,這些注釋只是為了清晰和指引,它們并不在執行期間強制執行類型。所以,即使您傳入不同類型的值,比如字符串而不是整數,函數仍然會運行。但是要小心:如果您不提供預期的類型,它可能會在運行時導致意外的行為或錯誤。比如在提供的示例中,函數sum()需要兩個整數作為參數。但是如果您嘗試添加一個字符串和一個整數,Python會拋出運行時錯誤。為什么?因為它不知道如何將字符串和整數相加!這就好比試圖把蘋果和橘子加在一起,那毫無意義。然而,如果兩個參數都是字符串,它將毫無問題地將它們連接起來。
下面是帶有測試用例的澄清版本:
print(sum(2,5)) # 7# print(sum('hello', 2)) # TypeError: can only concatenate str (not "int") to str# print(sum(3,'world')) # TypeError: unsupported operand type(s) for +: 'int' and 'str'print(sum('hello', 'world')) # helloworld
針對高級注釋,Python包含typing標準庫。不妨以一種更有趣的方式來看其用法。
from typing import Union, Tuple, Listimport numpy as npdef sum(variable: Union[np.ndarray, List]) -> float: total = 0 # function body to calculate the sum of values in iterable return total
這里,我們修改了同一個求和函數,它現在接受numpy數組或列表iterable。它計算并以浮點值的形式返回它們的和。我們利用typing庫中的Union注釋來指定變量參數可以接受的可能類型。
不妨進一步更改函數聲明,以顯示列表成員還應該是類型float。
def sum(variable: Union[np.ndarray, List[float]]) -> float: total = 0 # function body to calculate the sum of values in iterable return total
這些只是幫助理解Python中的類型提示的一些入門示例。隨著項目規模擴大,代碼庫變得更模塊化,類型注釋顯著地增強了可讀性和可維護性。typing庫提供了一組豐富的特性,包括可選的各種iterable、泛型以有力支持自定義類型的功能,使開發人員能夠精確而清晰地表達復雜的數據結構和關系。
盡管類型提示似乎很有幫助,但它仍然容易出錯,因為未強制執行注釋。這些只是開發人員的額外文檔,但如果使用不同的參數類型,函數仍然會執行。因此,需要以一種防御性的方式為函數和代碼強制執行前置條件。因此,我們手動檢查這些類型,違反條件時拋出適當的錯誤。
下面的函數顯示了如何使用輸入參數計算利息。
def calculate_interest(principal, rate, years): return principal * rate * years
這是簡單的操作,但這個函數是否適用于所有可能的解決方案?不,不適用于無效值作為輸入傳遞的個別情況。我們需要確保輸入值在一個有效的范圍內,那樣函數才能正確執行。實際上,必須滿足一些預設值條件,函數實現才能正確。
我們做這一步,如下所示:
from typing import Uniondef calculate_interest( principal: Union[int, float], rate: float, years: int) -> Union[int, float]: if not isinstance(principal, (int, float)): raise TypeError("Principal must be an integer or float") if not isinstance(rate, float): raise TypeError("Rate must be a float") if not isinstance(years, int): raise TypeError("Years must be an integer") if principal <= 0: raise ValueError("Principal must be positive") if rate <= 0: raise ValueError("Rate must be positive") if years <= 0: raise ValueError("Years must be positive") interest = principal * rate * years return interest
注意,我們使用條件語句進行輸入驗證。Python有時也用于此目的的斷言語句。然而,用于輸入驗證的斷言并不是最佳實踐,因為它們很容易被禁用,會導致生產環境中的意外行為。對于輸入驗證和強制執行前置條件、后置條件以及代碼不變量,最好使用顯式Python條件表達式。
考慮這樣一個場景:您擁有一個大型文檔數據集。您需要處理文檔,并對每個文檔執行某些操作。然而由于文件太大,您無法同時將所有文檔加載到內存中并對它們進行預處理。
一種可行的解決方案是只在需要時將文檔加載到內存中,并且一次只處理一個文檔,這也稱為延遲加載。即使我們知道需要什么文檔,也不會加載資源,除非有需要。當我們的代碼中沒有使用大量文檔時,不需要在內存中保留它們。這正是生成器和yield語句解決這個問題的方法。
生成器允許延遲加載,從而提高Python代碼執行的內存效率。值可以根據需要動態生成,減少了內存占用資源,并提高了執行速度。
import osdef load_documents(directory): for document_path in os.listdir(directory): with open(document_path) as _file: yield _filedef preprocess_document(document): filtered_document = None # preprocessing code for the document stored in filtered_document return filtered_documentdirectory = "docs/"for doc in load_documents(directory): preprocess_document(doc)
在上面的函數中,load_documents函數使用yield關鍵字。該方法返回類型<class generator>的對象。當我們迭代處理這個對象時,它從最后一個yield語句所在的位置繼續執行。因此,加載和處理單個文檔,提高了Python代碼的效率。
對任何語言來說,有效地利用資源最重要。我們只在需要時才通過使用生成器在內存中加載一些內容,如上所述。然而,當我們的程序不再需要某個資源時,關閉該資源同樣重要。我們需要防止內存泄漏,并執行適當的資源拆卸以節省內存。
上下文管理器簡化了資源設置和拆卸的常見用例。資源不再需要時,釋放它們很重要,即使在出現異常和失敗的情況下也是如此。上下文管理器使用自動清理,同時保持代碼簡潔易讀,降低內存泄漏的風險。
資源可以有多種變體,比如數據庫連接、鎖、線程、網絡連接、內存訪問和文件句柄。不妨關注最簡單的情況:文件句柄。這里的挑戰是確保每個打開的文件只關閉一次。關閉文件失敗可能導致內存泄漏,而試圖關閉文件句柄兩次會導致運行時錯誤。為了解決這個問題,應該將文件句柄封裝在try-except-finally塊中。這確保了文件被正確關閉,不管執行過程中是否發生了錯誤。下面是具體實現的樣子:
file_path = "example.txt"file = Nonetry: file = open(file_path, 'r') contents = file.read() print("File contents:", contents)finally: if file is not None: file.close()
然而,Python提供了一個使用上下文管理器的更優雅的解決方案,它自動處理資源管理。下面介紹我們如何使用文件上下文管理器簡化上述代碼:
file_path = "example.txt"with open(file_path, 'r') as file: contents = file.read() print("File contents:", contents)
在這個版本中,我們不需要顯式關閉文件。上下文管理器負責處理它,防止潛在的內存泄漏。
雖然Python為文件處理提供了內置的上下文管理器,但我們也可以為自定義類和函數創建自己的上下文管理器。針對基于類的實現,我們定義了__enter__和__exit__dunder方法。這里有一個基本的例子:
class CustomContextManger: def __enter__(self): # Code to create instance of resource return self def __exit__(self, exc_type, exc_value, traceback): # Teardown code to close resource return None
現在,我們可以在“with ”塊中使用這個自定義的上下文管理器:
with CustomContextManger() as _cm: print("Custom Context Manager Resource can be accessed here")
這種方法保持了上下文管理器簡潔明了的語法,同時允許我們根據需要處理資源。
我們經??吹斤@式地實現具有相同邏輯的多個函數。這是普遍存在的代碼風格,過多的代碼重復會使代碼難以維護和不可擴展。裝飾器用于將類似的功能封裝在一個地方。當一個相似的功能被多個其他函數使用時,我們可以通過在裝飾器中實現通用功能來減少代碼重復。它遵循面向方面的編程(AOP)和單一職責原則。
裝飾器在Django、Flask和FastAPI等Python Web框架中被大量使用。不妨通過在Python中將解釋器用作日志記錄的中間件來解釋裝飾器的效果。在生產環境中,我們需要知道服務一個請求需要多長時間。這是一個常見的用例,將在所有端點之間共享。因此,不妨實現一個簡單的基于裝飾器的中間件,它將記錄服務請求所花費的時間。
下面的虛擬函數用于服務用戶請求。
def service_request(): # Function body representing complex computation return True
現在,我們需要記錄這個函數執行所花費的時間。一種方法是在這個函數中添加日志記錄,如下所示:
import timedef service_request(): start_time = time.time() # Function body representing complex computation print(f"Time Taken: {time.time() - start_time}s") return True
雖然這種方法有效,但它會導致代碼重復。如果我們添加更多的路由,將不得不在每個函數中重復日志代碼。這增加了代碼重復,因為這種共享日志功能需要添加到每個實現中。我們使用裝飾器進行移除。
日志中間件將按以下方式來實現:
def request_logger(func): def wrapper(*args, **kwargs): start_time = time.time() res = func() print(f"Time Taken: {time.time() - start_time}s") return res return wrapper
在這個實現中,外部函數是裝飾器,它接受函數作為輸入。內部函數實現日志功能,而輸入函數在包裝器中被調用。
現在,我們只需用request_logger裝飾器裝飾原來的service_request函數:
@request_loggerdef service_request(): # Function body representing complex computation return True
使用@符號將service_request函數傳遞給request_logger裝飾器。它記錄所花費的時間,并在不修改代碼的情況下調用原始函數。這種關注點分離讓我們得以以類似的方式輕松地將日志記錄添加到其他服務方法中,如下所示:
@request_loggerdef service_request(): # Function body representing complex computation return True@request_loggerdef service_another_request(): # Function body return True
匹配語句是在Python3.10中引入的,所以它是Python語法的一個相當新的添加。它允許更簡單、更易讀的模式匹配,避免了典型if- if-else語句中過多的樣板文件和分支。
針對模式匹配,匹配case語句是更自然的編寫方式,因為它們不一定需要像條件語句中那樣返回布爾值。來自Python文檔中的以下示例展示了匹配case語句聲明如何比條件語句更具靈活性。
def make_point_3d(pt): match pt: case (x, y): return Point3d(x, y, 0) case (x, y, z): return Point3d(x, y, z) case Point2d(x, y): return Point3d(x, y, 0) case Point3d(_, _, _): return pt case _: raise TypeError("not a point we support")
根據文檔,如果沒有模式匹配,這個函數的實現將需要幾次isinstance()檢查、一兩個len()調用以及一個更復雜的控制流。揭開底層,匹配示例和傳統Python版本轉換成相似的代碼。然而,熟悉模式匹配后,可能會首選匹配case方法,因為它提供了更清晰、更自然的語法。
總的來說,匹配case語句為模式匹配提供了一種經過改進的替代方案,這可能會在較新的代碼庫中變得更加普遍。
在生產環境中,我們的大部分代碼依賴外部配置參數,比如API密鑰、密碼和各種設置。出于可擴展性和安全性的考慮,將這些值直接硬編碼到代碼中被認為是糟糕的做法。相反,將配置與代碼本身分開來至關重要。我們通常使用JSON或YAML等配置文件來存儲這些參數,確保它們易于訪問代碼,無需直接嵌入到其中。
一種日常的用例是實現多個連接參數的數據庫連接。
我們可以將這些參數保留在一個單獨的YAML文件中。
# config.yamldatabase: host: localhost port: 5432 username: myuser password: mypassword dbname: mydatabase
為了處理這個配置,我們定義了一個名為DatabaseConfig的類:
class DatabaseConfig: def __init__(self, host, port, username, password, dbname): self.host = host self.port = port self.username = username self.password = password self.dbname = dbname @classmethod def from_dict(cls, config_dict): return cls(**config_dict)
在這里,from_dict類方法充當DatabaseConfig類的構建器方法,允許我們從字典創建數據庫配置實例。
在我們的主代碼中,我們可以使用參數hydration和構建器方法來創建數據庫配置。通過讀取外部YAML文件,我們提取數據庫字典,并使用它為配置類創建實例:
import yamldef load_config(filename): with open(filename, "r") as file: return yaml.safe_load(file)config = load_config("config.yaml")db_config = DatabaseConfig.from_dict(config["database"])
這種方法不需要將數據庫配置參數直接硬編碼到代碼中。它還比使用參數解析器有所改進,因為我們不再需要在每次運行代碼時傳遞多個參數。此外,通過參數解析器訪問配置文件路徑,我們可以確保代碼保持靈活性,而不依賴硬編碼路徑。這種方法便于更容易管理配置參數,可以隨時修改配置參數,不需要更改代碼庫。
我們在本文中討論了業界用于生產就緒代碼的一些最佳實踐。這些都是常見的行業實踐,可以緩解人們在實際情形中可能面臨的多個問題。
值得一提的是,盡管有所有這些最佳實踐,文檔、文檔字符串和測試驅動開發是迄今為止最重要的實踐。重要的是要考慮一個函數應該做什么,然后將所有的設計決策和實現記入文檔,因為隨著時間的推移,人們不斷更改代碼庫。
原文標題: Mastering Python: 7 Strategies for Writing Clear, Organized, and Efficient Code,作者:Kanwal Mehreen
鏈接:https://www.kdnuggets.com/mastering-python-7-strategies-for-writing-clear-organized-and-efficient-code
本文鏈接:http://www.tebozhan.com/showinfo-26-96053-0.html編寫干凈高效Python代碼的七個策略
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com