python版代碼整潔之道
- 2020 年 2 月 19 日
- 筆記
總第 113 篇文章,本文大約 8000 字,閱讀大約需要 20 分鐘
原文:https://github.com/zedr/clean-code-python
python 版的代碼整潔之道。目錄如下所示:
- 介紹
- 變量
- 函數
1. 介紹
軟件工程的原則,來自 Robert C. Martin's 的書–《Clean Code》,而本文則是適用於 Python 版本的 clean code。這並不是一個風格指導,而是指導如何寫出可讀、可用以及可重構的 pyhton 代碼。
並不是這裡介紹的每個原則都必須嚴格遵守,甚至只有很少部分會得到普遍的贊同。下面介紹的都只是指導而已,但這都是來自有多年編程經驗的 《Clean Code》的作者。
這裡的 python 版本是 3.7+
2. 變量
2.1 採用有意義和可解釋的變量名
糟糕的寫法
ymdstr = datetime.date.today().strftime("%y-%m-%d")
好的寫法
current_date: str = datetime.date.today().strftime("%y-%m-%d")
2.2 對相同類型的變量使用相同的詞彙
糟糕的寫法:這裡對有相同下劃線的實體採用三個不同的名字
get_user_info() get_client_data() get_customer_record()
好的寫法:如果實體是相同的,對於使用的函數應該保持一致
get_user_info() get_user_data() get_user_record()
更好的寫法:python 是一個面向對象的編程語言,所以可以將相同實體的函數都放在類中,作為實例屬性或者是方法
class User: info : str @property def data(self) -> dict: # ... def get_record(self) -> Union[Record, None]: # ...
2.3 採用可以搜索的名字
我們通常都是看的代碼多於寫過的代碼,所以讓我們寫的代碼是可讀而且可以搜索的是非常重要的,如果不聲明一些有意義的變量,會讓我們的程序變得難以理解,例子如下所示。
糟糕的寫法
# 86400 表示什麼呢? time.sleep(86400)
好的寫法
# 聲明了一個全局變量 SECONDS_IN_A_DAY = 60 * 60 * 24 time.sleep(SECONDS_IN_A_DAY)
2.4 採用帶解釋的變量
糟糕的寫法
address = 'One Infinite Loop, Cupertino 95014' city_zip_code_regex = r'^[^,\]+[,\s]+(.+?)s*(d{5})?$' matches = re.match(city_zip_code_regex, address) save_city_zip_code(matches[1], matches[2])
還行的寫法
這個更好一點,但還是很依賴於正則表達式
address = 'One Infinite Loop, Cupertino 95014' city_zip_code_regex = r'^[^,\]+[,\s]+(.+?)s*(d{5})?$' matches = re.match(city_zip_code_regex, address) city, zip_code = matches.groups() save_city_zip_code(city, zip_code)
好的寫法
通過子模式命名來減少對正則表達式的依賴
address = 'One Infinite Loop, Cupertino 95014' city_zip_code_regex = r'^[^,\]+[,\s]+(?P<city>.+?)s*(?P<zip_code>d{5})?$' matches = re.match(city_zip_code_regex, address) save_city_zip_code(matches['city'], matches['zip_code'])
2.5 避免讓讀者進行猜測
不要讓讀者需要聯想才可以知道變量名的意思,顯式比隱式更好。
糟糕的寫法
seq = ('Austin', 'New York', 'San Francisco') for item in seq: do_stuff() do_some_other_stuff() # ... # item 是表示什麼? dispatch(item)
好的寫法
locations = ('Austin', 'New York', 'San Francisco') for location in locations: do_stuff() do_some_other_stuff() # ... dispatch(location)
2.6 不需要添加額外的上下文
如果類或者對象名稱已經提供一些信息來,不需要在變量中重複。
糟糕的寫法
class Car: car_make: str car_model: str car_color: str
好的寫法
class Car: make: str model: str color: str
2.7 採用默認參數而不是條件語句
糟糕的寫法
def create_micro_brewery(name): name = "Hipster Brew Co." if name is None else name slug = hashlib.sha1(name.encode()).hexdigest() # etc.
這個寫法是可以直接給 name
參數設置一個默認數值,而不需要採用一個條件語句來進行判斷的。
好的寫法
def create_micro_brewery(name: str = "Hipster Brew Co."): slug = hashlib.sha1(name.encode()).hexdigest() # etc.
3. 函數
3.1 函數參數(2個或者更少)
限制函數的參數個數是很重要的,這有利於測試你編寫的函數代碼。超過3個以上的函數參數會導致測試組合爆炸的情況,也就是需要考慮很多種不同的測試例子。
沒有參數是最理想的情況。一到兩個參數也是很好的,三個參數應該盡量避免。如果多於 3 個那麼應該需要好好整理函數。通常,如果函數多於2個參數,那代表你的函數可能要實現的東西非常多。此外,很多時候,一個高級對象也是可以用作一個參數使用。
糟糕的寫法
def create_menu(title, body, button_text, cancellable): # ...
很好的寫法
class Menu: def __init__(self, config: dict): title = config["title"] body = config["body"] # ... menu = Menu( { "title": "My Menu", "body": "Something about my menu", "button_text": "OK", "cancellable": False } )
另一種很好的寫法
class MenuConfig: """A configuration for the Menu. Attributes: title: The title of the Menu. body: The body of the Menu. button_text: The text for the button label. cancellable: Can it be cancelled? """ title: str body: str button_text: str cancellable: bool = False def create_menu(config: MenuConfig): title = config.title body = config.body # ... config = MenuConfig config.title = "My delicious menu" config.body = "A description of the various items on the menu" config.button_text = "Order now!" # The instance attribute overrides the default class attribute. config.cancellable = True create_menu(config)
優秀的寫法
from typing import NamedTuple class MenuConfig(NamedTuple): """A configuration for the Menu. Attributes: title: The title of the Menu. body: The body of the Menu. button_text: The text for the button label. cancellable: Can it be cancelled? """ title: str body: str button_text: str cancellable: bool = False def create_menu(config: MenuConfig): title, body, button_text, cancellable = config # ... create_menu( MenuConfig( title="My delicious menu", body="A description of the various items on the menu", button_text="Order now!" ) )
更優秀的寫法
rom dataclasses import astuple, dataclass @dataclass class MenuConfig: """A configuration for the Menu. Attributes: title: The title of the Menu. body: The body of the Menu. button_text: The text for the button label. cancellable: Can it be cancelled? """ title: str body: str button_text: str cancellable: bool = False def create_menu(config: MenuConfig): title, body, button_text, cancellable = astuple(config) # ... create_menu( MenuConfig( title="My delicious menu", body="A description of the various items on the menu", button_text="Order now!" ) )
3.2 函數應該只完成一個功能
這是目前為止軟件工程里最重要的一個規則。函數如果完成多個功能,就很難對這個函數解耦、測試。如果可以對一個函數分離為僅僅一個動作,那麼該函數可以很容易進行重構,並且代碼也方便閱讀。即便你僅僅遵守這一點建議,你也會比很多開發者更加優秀。
糟糕的寫法
def email_clients(clients: List[Client]): """Filter active clients and send them an email. 篩選活躍的客戶並發郵件給他們 """ for client in clients: if client.active: email(client)
好的寫法
def get_active_clients(clients: List[Client]) -> List[Client]: """Filter active clients. """ return [client for client in clients if client.active] def email_clients(clients: List[Client, ...]) -> None: """Send an email to a given list of clients. """ for client in clients: email(client)
這裡其實是可以使用生成器來改進函數的寫法。
更好的寫法
def active_clients(clients: List[Client]) -> Generator[Client]: """Only active clients. """ return (client for client in clients if client.active) def email_client(clients: Iterator[Client]) -> None: """Send an email to a given list of clients. """ for client in clients: email(client)
3.3 函數的命名應該表明函數的功能
糟糕的寫法
class Email: def handle(self) -> None: # Do something... message = Email() # What is this supposed to do again? # 這個函數是需要做什麼呢? message.handle()
好的寫法
class Email: def send(self) -> None: """Send this message. """ message = Email() message.send()
3.4 函數應該只有一層抽象
如果函數包含多於一層的抽象,那通常就是函數實現的功能太多了,應該把函數分解成多個函數來保證可重複使用以及更容易進行測試。
糟糕的寫法
def parse_better_js_alternative(code: str) -> None: regexes = [ # ... ] statements = regexes.split() tokens = [] for regex in regexes: for statement in statements: # ... ast = [] for token in tokens: # Lex. for node in ast: # Parse.
好的寫法
REGEXES = ( # ... ) def parse_better_js_alternative(code: str) -> None: tokens = tokenize(code) syntax_tree = parse(tokens) for node in syntax_tree: # Parse. def tokenize(code: str) -> list: statements = code.split() tokens = [] for regex in REGEXES: for statement in statements: # Append the statement to tokens. return tokens def parse(tokens: list) -> list: syntax_tree = [] for token in tokens: # Append the parsed token to the syntax tree. return syntax_tree
3.5 不要將標誌作為函數參數
標誌表示函數實現的功能不只是一個,但函數應該僅做一件事情,所以如果需要標誌,就將多寫一個函數吧。
糟糕的寫法
from pathlib import Path def create_file(name: str, temp: bool) -> None: if temp: Path('./temp/' + name).touch() else: Path(name).touch()
好的寫法
from pathlib import Path def create_file(name: str) -> None: Path(name).touch() def create_temp_file(name: str) -> None: Path('./temp/' + name).touch()
3.6 避免函數的副作用
函數產生副作用的情況是在它做的事情不只是輸入一個數值,返回其他數值這樣一件事情。比如說,副作用可能是將數據寫入文件,修改全局變量,或者意外的將你所有的錢都寫給一個陌生人。
不過,有時候必須在程序中產生副作用–比如,剛剛提到的例子,必須寫入數據到文件中。這種情況下,你應該盡量集中和指示產生這些副作用的函數,比如說,保證只有一個函數會產生將數據寫到某個特定文件中,而不是多個函數或者類都可以做到。
這條建議的主要意思是避免常見的陷阱,比如分析對象之間的狀態的時候沒有任何結構,使用可以被任何數據修改的可修改數據類型,或者使用類的實例對象,不集中副作用影響等等。如果你可以做到這條建議,你會比很多開發者都開心。
糟糕的寫法
# This is a module-level name. # It's good practice to define these as immutable values, such as a string. # However... name = 'Ryan McDermott' def split_into_first_and_last_name() -> None: # The use of the global keyword here is changing the meaning of the # the following line. This function is now mutating the module-level # state and introducing a side-effect! # 這裡採用了全局變量,並且函數的作用就是修改全局變量,其副作用就是修改了全局變量, # 第二次調用函數的結果就會和第一次調用不一樣了。 global name name = name.split() split_into_first_and_last_name() print(name) # ['Ryan', 'McDermott'] # OK. It worked the first time, but what will happen if we call the # function again?
好的寫法
def split_into_first_and_last_name(name: str) -> list: return name.split() name = 'Ryan McDermott' new_name = split_into_first_and_last_name(name) print(name) # 'Ryan McDermott' print(new_name) # ['Ryan', 'McDermott']
另一個好的寫法
from dataclasses import dataclass @dataclass class Person: name: str @property def name_as_first_and_last(self) -> list: return self.name.split() # The reason why we create instances of classes is to manage state! person = Person('Ryan McDermott') print(person.name) # 'Ryan McDermott' print(person.name_as_first_and_last) # ['Ryan', 'McDermott']
總結
原文的目錄實際還有三個部分:
- 對象和數據結構
- 類
- 單一職責原則(Single Responsibility Principle, SRP)
- 開放封閉原則(Open/Closed principle,OCP)
- 里氏替換原則(Liskov Substitution Principle ,LSP)
- 接口隔離原則(Interface Segregation Principle ,ISP)
- 依賴倒置原則(Dependency Inversion Principle ,DIP)
- 不要重複
不過作者目前都還沒有更新,所以想了解這部分內容的,建議可以直接閱讀《代碼整潔之道》對應的這部分內容了。