python版代碼整潔之道

  • 2020 年 2 月 19 日
  • 筆記

總第 113 篇文章,本文大約 8000 字,閱讀大約需要 20 分鐘

原文:https://github.com/zedr/clean-code-python

python 版的代碼整潔之道。目錄如下所示:

  1. 介紹
  2. 變量
  3. 函數

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)
  • 不要重複

不過作者目前都還沒有更新,所以想了解這部分內容的,建議可以直接閱讀《代碼整潔之道》對應的這部分內容了。