Clean Code

Variables

相同型態的變數名稱命名要一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
## Bad: user, clilent, customer但都是user
def get_user_info(): pass
def get_client_data(): pass
def get_customer_record(): pass
## Good: 一致的使用user
def get_user_info(): pass
def get_user_data(): pass
def get_user_record(): pass

## Even Better: 使用class
# - 添加property定義的方法可以像訪問屬性一樣訪問 User().data
# - 使用Union[Record, None]表示回傳值可能是Record or None
# - 使用Dict[str, str]表示回傳值是key, value都是str
from typing import Union, Dict

class Record:
pass

class User:
info: str

# 添加property定義的方法可以像訪問屬性一樣訪問 User().data
@property
def data(self) -> Dict[str, str]: # -> 用來表示回傳屬性; Dict表示key, value都是str
return {}

def get_record(self) -> Union[Record, None]: # Union表示皆有可能是Record or None
return Record()
使用可以搜尋的名稱來定義值
1
2
3
4
5
6
7
8
import time

# Bad 沒人知道86400是什麼意思
time.sleep(86400)

# Good 使用全大寫的變數名稱來定義
SECONDS_IN_A_DAY = 60 * 60 * 24
time.sleep(SECONDS_IN_A_DAY)
不添加沒意義的context
1
2
3
4
5
6
7
8
9
10
# Bad 
class Car:
car_make: str
car_model: str
car_color: str
# Good
class Car:
make: str
model: str
color: str
多使用default arguments
1
2
3
4
5
6
7
8
9
10
11
12
import hashlib

# Bad
def create_micro_brewery(name):
name = "Hipster Brew Co." if name is None else name
slug = hashlib.sha1(name.encode()).hexdigest()
# etc.

# Good
def create_micro_brewery(name: str = "Hipster Brew Co."):
slug = hashlib.sha1(name.encode()).hexdigest()
# etc.

Funcitons

一個function只做一件事
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from typing import Generator, Iterator


class Client:
active: bool

def email(client: Client):
pass

def active_clients(clients: Iterator[Client]) -> Generator[Client, None, None]:
"""Only active clients"""
return (client for client in clients if client.active) # 使用 generator expression

# 一個function只做一件事,這樣可以讓程式碼更容易閱讀
def email_client(clients: Iterator[Client]) -> None:
"""Send an email to a given list of clients.
"""
for client in active_clients(clients):
email(client)
不使用超過兩個變數,超過則使用class包裝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# Bad
def create_menu(title, body, button_text, cancellable):
pass

# Good
# - 使用dataclass可以讓class更簡潔,不需要定義__init__和__repr__還有__eq__
# - 使用astuple將class的屬性轉換成tuple並且unpack將tuple的值取出
from dataclasses import astuple, dataclass


# 使用@dataclass可以讓class更簡潔,不需要定義__init__和__repr__還有__eq__
@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):
# 使用astuple將class的屬性轉換成tuple並且unpack將tuple的值取出
title, body, button_text, cancellable = astuple(config)
# ...
# 也可以寫成以下,但只有Python3.8+才支援
title = config["title"]


create_menu(
MenuConfig(
title="My delicious menu",
body="A description of the various items on the menu",
button_text="Order now!"
)
)
# ...


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)


function名稱應該要明確具體做的事情
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Bad: 使用不具體的名稱handle
class Email:
def handle(self) -> None:
pass

message = Email()
message.handle()

# Good: 使用更具體的名稱Send
class Email:
def send(self) -> None:
"""Send this message"""

message = Email()
message.send()
不建立產生 side effects 的function

Bad: 這個function會修改外部變數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# type: ignore

# This is a module-level name.
# It's good practice to define these as immutable values, such as a string.
# However...
fullname = "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 fullname
fullname = fullname.split()


split_into_first_and_last_name()

# MyPy will spot the problem, complaining about 'Incompatible types in
# assignment: (expression has type "List[str]", variable has type "str")'
print(fullname) # ["Ryan", "McDermott"]

# OK. It worked the first time, but what will happen if we call the
# function again?

Good:

1
2
3
4
5
6
7
8
9
10
11
from typing import List, AnyStr


def split_into_first_and_last_name(name: AnyStr) -> List[AnyStr]:
return name.split()


fullname = "Ryan McDermott"
name, surname = split_into_first_and_last_name(fullname)

print(name, surname) # => Ryan McDermott

Also Good

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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"]

Classes

Open/Closed Principle (OCP)

“Incoporate new features by extending the system, not by making modifications (to it)”, Uncle Bob
意思是當你要新增新的功能時,應該要透過繼承來擴展系統,而不是修改它

這裡建議使用Mixin Pattern,這樣可以讓子類繼承多個父類,並且可以避免重複代碼

Bad

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from dataclasses import dataclass


@dataclass
class Response:
"""An HTTP response"""

status: int
content_type: str
body: str


class View:
"""A simple view that returns plain text responses"""

def get(self, request) -> Response:
"""Handle a GET request and return a message in the response"""
return Response(
status=200,
content_type='text/plain',
body="Welcome to my web site"
)


class TemplateView(View):
"""A view that returns HTML responses based on a template file."""

# 他重寫了整個get()方法,這樣就違反了OCP原則
# 原本的意圖指示更改行為,而不是替換它
def get(self, request) -> Response:
"""Handle a GET request and return an HTML document in the response"""
with open("index.html") as fd:
return Response(
status=200,
content_type='text/html',
body=fd.read()
)

Good

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from dataclasses import dataclass


@dataclass
class Response:
"""An HTTP response"""

status: int
content_type: str
body: str


class View:
"""A simple view that returns plain text responses"""

content_type = "text/plain"

# Good: 把render_body()定義在父類,讓子類去實作 並且這樣就不用重新實作get()方法 甚至直接使用父類的get()方法
def render_body(self) -> str:
"""Render the message body of the response"""
return "Welcome to my web site"

def get(self, request) -> Response:
"""Handle a GET request and return a message in the response"""
return Response(
status=200,
content_type=self.content_type,
body=self.render_body()
)

# 定義子類,繼承自父類 View
class TemplateView(View):
"""A view that returns HTML responses based on a template file."""

content_type = "text/html"
template_file = "index.html"

def render_body(self) -> str:
"""Render the message body as HTML"""
with open(self.template_file) as fd:
return fd.read()

# 創建子類的實例
templateView = TemplateView()

Also Good

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from dataclasses import dataclass, field
from typing import Protocol


@dataclass
class Response:
"""An HTTP response"""

status: int
content_type: str
body: str
headers: dict = field(default_factory=dict)


class View:
"""A simple view that returns plain text responses"""

content_type = "text/plain"

def render_body(self) -> str:
"""Render the message body of the response"""
return "Welcome to my web site"

def get(self, request) -> Response:
"""Handle a GET request and return a message in the response"""
return Response(
status=200,
content_type=self.content_type,
body=self.render_body()
)


class TemplateRenderMixin:
"""A mixin class for views that render HTML documents using a template file

Not to be used by itself!
"""
template_file: str = ""

def render_body(self) -> str:
"""Render the message body as HTML"""
if not self.template_file:
raise ValueError("The path to a template file must be given.")

with open(self.template_file) as fd:
return fd.read()


class ContentLengthMixin:
"""A mixin class for views that injects a Content-Length header in the
response

Not to be used by itself!
"""

def get(self, request) -> Response:
"""Introspect and amend the response to inject the new header"""
response = super().get(request) # 呼叫父類的get()方法把目前的response取出
response.headers['Content-Length'] = len(response.body) # 添加Content-Length header
return response

# 讓這個子類繼承多個Mixin父類
class TemplateView(TemplateRenderMixin, ContentLengthMixin, View):
"""A view that returns HTML responses based on a template file."""

content_type = "text/html"
template_file = "index.html"

T = TemplateView()
T.render_body()
Interface Segregation Principle (ISP)

“Keep interfaces small so that users don’t end up depending on things they don’t need.”, Uncle Bob.

這裡建議使用Abstract Base Class (ABC)來定義一個interface,並且讓子類去實作這個interface。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from abc import ABCMeta, abstractmethod


# Define the Abstract Class for a generic Greeter object
class Greeter(metaclass=ABCMeta):
"""An object that can perform a greeting action."""

@staticmethod # 使用 @staticmethod 裝飾器定義的方法可以直接通過類名來調用,而無需實例化類。
@abstractmethod # 使用 @abstractmethod 裝飾器來定義抽象方法,這樣的方法必須在子類中實現
def greet(name: str) -> None:
"""Display a greeting for the user with the given name"""


class FriendlyActor(Greeter):
"""An actor that greets the user with a friendly salutation"""

@staticmethod
def greet(name: str) -> None:
"""Greet a person by name"""
print(f"Hello {name}!")


def welcome_user(user_name: str, actor: Greeter):
"""Welcome a user with a given name using the provided actor"""
actor.greet(user_name) # 調用父類的greet()方法


welcome_user("Barbara", FriendlyActor())

還要注意的是,如果不是所有子類都要繼承該interface的所有方法,那要盡量把interface拆分成多個小的interface,這樣可以避免子類不需要的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import abc


class DataCarrier(metaclass=abc.ABCMeta):
"""Carries a data payload"""

@property # 可以直接訪問屬性 不用加()
def data(self):
...


class Loadable(DataCarrier):
"""Can load data from storage by name"""

@classmethod # 這個方法可以直接通過類名來調用,而無需實例化類。
@abc.abstractmethod # 這樣的方法必須在子類中實現
def load(cls, name: str):
...

# 把這個拆出來,這樣子類就不需要實作save()方法 等到有需要的時候再繼承就好
class Saveable(DataCarrier):
"""Can save data to storage"""

@abc.abstractmethod
def save(self) -> None:
...


class PDFDocument(Loadable):
"""A PDF document"""

@property
def data(self) -> bytes:
"""The raw bytes of the PDF document"""
... # Code goes here - omitted for brevity

@classmethod
def load(cls, name: str) -> None:
"""Load the file from the local filesystem"""
... # Code goes here - omitted for brevity


def view(request):
"""A web view that handles a GET request for a document"""
requested_name = request.qs['name'] # We want to validate this!
return PDFDocument.load(requested_name).data
Dependency Inversion Principle (DIP)

“Depend upon abstractions, not concrete details”, Uncle Bob.
抽象不應該依賴細節;細節應該依賴抽象

從上面的文章提到,高階模組就像是【要求】功能的一方,低階模組就像是【提供】功能的一方。而依賴反轉原則就是要求高階模組不要依賴低階模組,而是依賴抽象。這樣就可以達到高階模組和低階模組都依賴於抽象,這樣就可以達到解耦的效果。
依賴就「反轉」了。原本是 高階模組 → 低階模組 的關係,變成了 高階模組 → 介面 ← 低階模組。並不是高階去依賴低階,而是低階去依賴高階要求的功能

舉例一下:現在有間小小公司,老闆請來了小明當工程師,並請他開工撰寫產品程式碼。

  • 當「撰寫產品程式」對「工程師」直接依賴的時候,狀況可能是這樣的:
1
2
3
4
5
6
public Product Work()
{
Ming programmer = new Ming();
var product = programmer.Programming();
return product;
}

過一陣子,老闆發現小明寫出來的東西似乎不太行,於是把小明趕走,另外請了小華。
這時候因為「工程師」這個實作類別不一樣了,我們就必須要改一次程式碼:

1
2
3
4
5
6
public Product Work()
{
Hua programmer = new Hua(); // 換人
var product = programmer.Programming();
return product;
}

又過了好一陣子,老闆又另外請了小美來工作。
於是又要再改一次,而且小美的工作方式甚至不叫做 Programming,而是 Coding:

1
2
3
4
5
6
public Product Work()
{
Mei programmer = new Mei(); // 換人 again
var product = programmer.Coding(); // 連方法名稱都不一樣
return product;
}

現在有感覺到一點問題了嗎?
如果一直換人,Work 的程式碼豈不是每次都要修改?甚至根據依賴對象的不同
連使用方式都可能受到影響,很明顯這樣就是所謂不健康的耦合。

Good

我們可以透過介面來解決這個問題,這樣就可以達到解耦的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IProgrammer
{
void Programming();
}
public class Hua : IProgrammer
{
public void Programming() { /* Work */ }
}

public Product Work()
{
IProgrammer programmer = new Ming(); // 就可以自由地換人了
var product = programmer.Programming(); // 只要繼承IProgrammer都是Programming()
return product;
}

補充: Using Generator

情境1: 有一個很大的List
Generator Functions 會回傳 Lazy Iterator,Lazy Iterator Object有幾個特點,你可以如list般進行loop,但是與list不同,lazy iterator不會將資料儲存在記憶體裡。可以透過以下例子看到差異:

1
2
3
4
5
# 這會導致MemorryError 
def csv_reader(file_name):
file = open(file_name)
result = file.read().split("\n") # 這行會將所有資料讀取到記憶體中
return result

但是如果換成以下,就不會發生MemoryError錯誤,反而會印出

1
2
3
4
5
6
7
# 我們將csv_reader()改成generator function,他會回傳一個lazy iterator
def csv_reader(file_name):
for row in open(file_name, "r"):
yield row

# 也可以定義成generator expressio (generator comprehension) 與list comprehension類似但是差異在於用()而不是[]
csv_gen = (row for row in open(file_name))

情境2: 產生無限數列
透過yeild,只會記住上一次的狀態,而不會記住所有的狀態,所以可以用來產生無限數列,甚至可以透過next()來取得下一個數字,如下例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def infinite_sequence():
num = 0
while True:
yield num
num += 1

# 產生無限數列
>>> for i in infinite_sequence():
print(i, end=" ")

# 也可以透過next()來取得下一個數字
>>> gen = infinite_sequence()
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
3

補充: Using Mixin

Mixin 是一種設計模式,主要是為了解決多重繼承的問題。

  • Mixin是一個class,他不會被實例化,而是被其他class繼承,並且提供一些方法給其他class使用。
  • 多個類都需要使用同一個方法時,透過設計Mixin Class讓其他子類繼承,這樣可以避免重複代碼。
  • 不只針對Python其他語言也有Mixin的概念,如Ruby, Scala, Swift等等。
  • 每個Mixin只用於拓展類的一個特定功能,不能影響子類的主要功能,子類也不能依賴Mixin的功能。

我們定義一個簡單的Class

1
2
3
4
5
6
7
8
9
class Person:
def __init__(self, name, gender, age):
self.name = name
self.gender = gender
self.age = age

# 通常可以透過調用實利屬性的方式來訪問
p = Person("Shannon", "F", 18)
print(p.name)

但是我們希望這個Person類可以擁有dict依樣調用屬性的功能,這時候我們可以透過Mixin來解決這個問題

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定義一個Mixin 給各種子類繼承,這個Mixin可以讓子類像訪問dict一樣訪問屬性
class MappingMixin:
def __getitem__(self, key):
return self.__dict__.get(key)

def __setitem__(self, key, value):
return self.__dict__.set(key, value)

# 定義一個Person類,繼承MappingMixin
class Person(MappingMixin):
def __init__(self, name, gender, age):
self.name = name
self.gender = gender
self.age = age

# 现在 Person 拥有另一种调用属性方式了:
p = Person("Shannon", "F", 18)
print(p['name']) # "Shannon"
print(p['age']) # 18

不僅如此,我們還希望可以實現__repr__自訂類的輸出,讓所有的子類都擁有相同的輸出格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 定義一個Mixin,讓子類都擁有相同的輸出格式
class ReprMixin:
def __repr__(self):
s = self.__class__.__name__ + '('
for k, v in self.__dict__.items():
if not k.startswith('_'):
s += '{}={}, '.format(k, v)
s = s.rstrip(', ') + ')' # 将最后一个逗号和空格换成括号
return s

# Person 繼承 MappingMixin 和 ReprMixin 一個類可以繼承多個父類
class Person(MappingMixin, ReprMixin):
def __init__(self, name, gender, age):
self.name = name
self.gender = gender
self.age = age

# 這樣這個Person就混入了兩個功能
p = Person("Shannon", "F", 18)
print(p['name']) # "小陈"
print(p) # Person(name=Shannon, gender=F, age=18)