Clean Code in Python-符合Python風格的程式

簡潔的Python重構你的舊程式,Mariano Anaya 著,賴屹民譯。沒整理的筆記。

在程式語言中,典型寫法(idiom)是為了執行特定工作而採取的特殊寫法,是與語言有關的程式碼,當程式碼遵循這些表達風格時,就是符合語言習慣寫法的(idiomatic),而在Python中通常稱為Pythonic。

索引與切片

num_tuple = (1, 1, 3, 5, 6 ,7 ,11 ,12, 21 ,28)#從後面開始算index
num_tuple[-1]
>28
num_tuple[-2]
>21
#index=3~index=6的元素(6不算)
num_tuple[3:6]
>(5, 6, 7)
#index=3之前所有元素(index=3不算)
num_tuple[:3]
>(1, 1, 3)
#index=3之後所有元素
num_tuple[3:]
>(5, 6 ,7 ,11 ,12, 21 ,28)
#index位置start=1, End=7, 間隔=2
#index = 1 3 5
interval= slice(1, 7, 2)
num_tuple[interval]
>(1, 5, 7)
  • 優先使用內建slice語法, 不要試著使用for迴圈來迭代tuple、list或string等親手排除元素。
  • 用一個範圍取值,結果應該是與類別同一個型態的實例。
  • 用slice提供範圍,應遵守Python語義,不納入結束的元素。

環境管理器(context manager)

是Python獨有的實用功能,能針對模式做出正確的回應。通常能在資源管理程式附近看到環境管理器。
ex1: 打開檔案、關閉檔案,釋出配置的資源等。若要加上例外處理,則可把清理程式放在finally區塊。
ex2 優化: with進入環境管理器,區塊結束時,檔案會自動關閉。

#ex1
filedata = open(filename)
try:
process_file(data)
finally:
filedata.close()
#ex2 用with 優化
with open(filename) as filedata:
process_file(filedata)

with 陳述句

包含兩種魔術方法
1. __enter__ 此方法回傳的東西指派給as後面的變數,這是選擇性的,它並不一定要回傳特定內容或給他指派變數。
2. __exit__ 此方法當區塊內程式結束時便會離開這個環境,即使遇到例外或錯誤也會被呼叫,安全地清理環境並可用自訂的方式處理。

環境處理器執行DB任務

def stop_db():
run("systemctl stop postgresql.service")
def start_db():
run("systemctl start postgresql.service")
class DBHandler:
def __enter__(self):
stop_db()
return self

def __exit__(self, exc_type, ex_value, ex_traceback):
start_db()
def backup_db():
run("pg_dump database")
def main():
with DBHandler():
backup_db()

contextlib模組實現環境管理器與改善現有環境管理器

#優化
import contextlib
@contextlib.contextmanager
def db_handler():
stop_db()
yield
start_db()
with db_handler():
db_backup()
這種方式的好處,容易重構既有函式、重用程式碼,當我們需要不屬於任何特定物件的環境管理器時,非常適合。

特性、屬性、物件方法的各型態

Python底線的意義

python中並沒有嚴格的限制public、private、protected,但是有一些規範可以使用。在預設情況下,物件所有屬性都是Public的。

#單底線
class Connector:
def __init__(self, source)
self.source = source
self._timeout = 60
  • _timeout 只能在Connector內被存取,不能從外部呼叫。可以在必要的時候安全的重構timeout並保留於之前一樣的介面。跟著這些規則讓程式更容易重構。
  • 不屬於物件介面的東西前面都需要加 _ 並將其當成private。
#雙底線ex1
class Connector:
def __init__(self, source)
self.source = source
self.__timeout = 60
def connect(self):
print(f"connecting with {self.__timeout}s")
conn = Connector("postgresql://localhost")
conn.connect()
>connecting with 60s
conn.__timeout
>Traceback (most recent call last):
> File "<stdin>", line 1, in <module>
>AttributeError: 'Connector' object has no attribute '__timeout'
#雙底線ex2
vars(conn)
>{'source': 'postgresql://localhost', '_Connector__timeout': 60}
conn._Connector__timeout
>60
conn._Connector__timeout=30
conn.connect()
>connecting with 30s
  • AttributeError 表示這不存在,當使用雙底線時,Python會做名稱重整(name mangling),建立”_<class-name>__<attribute-name>” 屬性。
  • __雙底線的目的在於為了覆寫將會將會被繼承多次類別裡面的方法,避免產生重複的方法名稱。
  • _將單底線視為private的做法,__雙底線是不符合Python風格的做法。

特性(property)

當需要定義對物件的某些屬性的存取控制時。

#ex 使用者註冊之應用程式,避免錯誤的使用者資訊。
import re
EMAIL_FORMAT = re.compile(r"[^@]+@[^@]+\.[^@]+")def is_valid_email(potentially_valid_email: str):
return re.match(EMAIL_FORMAT, potentially_valid_email) is not None
class User:
def __init__(self, username):
self.username = username
self._email = None
@property
def email(self):
return self._email
@email.setter
def email(self, new_email):
if not is_valid_email(new_email):
raise ValueError(f"Can't set {new_email} as it's not a valid email")
self._email = new_email
  • @property方法會回傳private的email值。
  • @email.setter, 呼叫方執行<user>.email = <new_email>時的方法。
  • 比起自訂方法名稱get_*或set_*,property的做法可以清楚地看到想處理的東西。
  • property很適合能在做”命令/查詢職責分離(command and query separation)”,方法只做一件事。

可迭代物件

Python的迭代是用它自己的協定來運作的。當你試著用 for i in myobj: … 這種格式迭代時,Py會在極高層面依序檢查下兩件事:

  1. 物件有沒有兩種迭代器方法之一:__next__ 或 __iter__
  2. 物件是不是序列:__len__ 與__getitem__
  3. 作為後備機制,序列是可迭代的,可以用兩種方法處理for迴圈可處理的物件。

迭代協定的工作方式-迭代器是可迭代物建構的,且可迭代物是被迭代的對象。下列為一個可建立可迭代日期範圍的物件。
容器(container)可迭代物。

from datetime import timedelta
class DataRangeContainerIterable:
def __init__(self, start_date, end_date):
self.start_date = start_date
self.end_date = end_date
def __iter__(self):
current_day = self.start_date
while current_day < self.end_date:
yield current_day
current_day += timedelta(days=1)

容器物件

容器是實作了__contains__的物件。當py發件in關鍵字時就會呼叫。
code:
element in container
py:
container.__contains__(element)

物件動態屬性

可改變之預設引數

不要將可改變物件做函式的預設引數使用,可以像以下這樣寫。

def user_display(user_metadata: dict = None):
user_metadata = user_meatadata or {"name":"Duncan", "age":42}

name = user_metadata.pop("name")
age = user_metadata.pop("age")
return f"{name} ({age})"

擴展內建型態

擴展list、string、dict等正確的作法是使用collection模組,這個模組實作了一些特別的容器資料型態,用來替代 Python 一般內建的容器。參閱 容器資料型態

注意事項

要寫出符合風格的程式碼,需了解此程式的主要功能外也要注意典型寫法的潛在問題,此篇討論的內容多數都可避免,而在任何情況下反 anti-pattern都是不正確的。若在審視程式碼時發現這些情況,請好好重構它。

本章小節

本章作者探討了一些Python獨特的功能、各種方法、協定與內部機制等,藉此了解Python典型的最佳寫法,目前尚未完全內化在我腦中,在實務或是自己的開源專案上,每天花時間去思考與重構,環境管理器、魔術方法等使用時機,融入其中。

而這些規則是簡潔程式碼的必要條件,但還不夠,因此需要結合後續章節軟體開發的一些規則及設計模式等。

--

--