원작자에게 허락을 맡고 개인적으로 번역한 글입니다. 출처
clean-code-python#
- 소개
- 변수
- 함수
- 객체와 자료구조
- 클래스
- S: Single Responsibility Principle (SRP)
- O: Open/Closed Principle (OCP)
- L: Liskov Substitution Principle (LSP)
- I: Interface Segregation Principle (ISP)
- D: Dependency Inversion Principle (DIP)
- Don’t repeat yourself (DRY)
소프트웨어 엔지니어링의 원칙인 로버트 마틴의 클린코드를 파이썬에 적용한 내용입니다. 파이썬에서 읽을 수 있고 재사용 가능하며 리팩터블할 수 있는 소프트웨어를 만드는 안내서입니다.
여기에 있는 모든 원칙이 엄격하게 지켜질 필요는 없으며, 더 적은 수의 원칙을 보편적으로 사용할 수 있습니다. 여기에 소개된 내용은 가이드라인일 뿐 그 이상은 아니지만, 클린 코드 저자들에 의해 수년간의 집단 경험을 통해 문서화 된것입니다.
clean-code-javascript에서 영감을 받았습니다.
Python3.7+ 대상
의미 있고 발음 가능한 변수 이름 사용#
Bad:
1
2
3
4
| import datetime
ymdstr = datetime.date.today().strftime("%y-%m-%d")
|
Good:
1
2
3
4
| import datetime
current_date: str = datetime.date.today().strftime("%y-%m-%d")
|
⬆ back to top
같은 유형의 변수에 동일한 어휘 사용#
Bad:
이 예에서는 동일한 기본 엔티티에 대해 세 가지 다른 이름을 사용합니다:
1
2
3
| def get_user_info(): pass
def get_client_data(): pass
def get_customer_record(): pass
|
Good:
엔티티가 동일하면 기능에서 해당 엔티티를 일관되게 참조해야 합니다:
1
2
3
| def get_user_info(): pass
def get_user_data(): pass
def get_user_record(): pass
|
Even better
파이썬은 (또한) 객체 지향 프로그래밍 언어이기도 합니다. 타당하다면 구체적인 구현과 함께 기능을 패키징합니다.
인스턴스(instance)의 프로퍼티, 프로퍼티 메서드 또는 메서드로:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| from typing import Union, Dict
class Record:
pass
class User:
info : str
@property
def data(self) -> Dict[str, str]:
return {}
def get_record(self) -> Union[Record, None]:
return Record()
|
⬆ back to top
검색 가능한 이름 사용#
우리는 우리가 코드를 작성하는 시간보다 읽는 시간이 더 많을겁니다. 우리가 읽을수 있고 검색가능한 코드를 만드는것은 중요합니다.
프로그램의 의미가 있는 변수들의 이름을 붙히지 않음으로써 우리는 코드를 읽는 사람들에게 상처를 주곤합니다.
이름을 검색 가능하게 만드세요.
Bad:
1
2
3
4
5
| import time
# What is the number 86400 for again?
time.sleep(86400)
|
Good:
1
2
3
4
5
6
| import time
# Declare them in the global namespace for the module.
SECONDS_IN_A_DAY = 60 * 60 * 24
time.sleep(SECONDS_IN_A_DAY)
|
⬆ back to top
설명 변수 사용#
Bad:
1
2
3
4
5
6
7
8
9
| import re
address = "One Infinite Loop, Cupertino 95014"
city_zip_code_regex = r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$"
matches = re.match(city_zip_code_regex, address)
if matches:
print(f"{matches[1]}: {matches[2]}")
|
Not bad:
더 좋긴 하지만, 여전히 정규식에 많이 의존하고 있습니다.
1
2
3
4
5
6
7
8
9
10
| import re
address = "One Infinite Loop, Cupertino 95014"
city_zip_code_regex = r"^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$"
matches = re.match(city_zip_code_regex, address)
if matches:
city, zip_code = matches.groups()
print(f"{city}: {zip_code}")
|
Good:
하위 패턴의 이름을 지정하여 정규식에 대한 의존성을 줄입니다.
1
2
3
4
5
6
7
8
9
| import re
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)
if matches:
print(f"{matches['city']}, {matches['zip_code']}")
|
⬆ back to top
Mental Mapping 방지#
변수가 의미하는 바를 당신의 코드를 읽는 사람에게 번역하도록 강요하지 마세요.
명시하는 것이 암묵적인 것보다 낫습니다.
Bad:
1
2
3
4
5
6
7
8
| seq = ("Austin", "New York", "San Francisco")
for item in seq:
#do_stuff()
#do_some_other_stuff()
# Wait, what's `item` again?
print(item)
|
Good:
1
2
3
4
5
6
7
| locations = ("Austin", "New York", "San Francisco")
for location in locations:
#do_stuff()
#do_some_other_stuff()
# ...
print(location)
|
⬆ back to top
불필요한 컨텍스트 추가 안 함#
클래스/객체 이름이 알려주는 내용이 있으면 변수 이름에 이 내용을 반복하지 마십시오.
Bad:
1
2
3
4
| class Car:
car_make: str
car_model: str
car_color: str
|
Good:
1
2
3
4
| class Car:
make: str
model: str
color: str
|
⬆ back to top
단락 또는 조건 대신 기본 인수 사용#
까다롭게
왜 이렇게 작성합니까?:
1
2
3
4
5
6
7
| import hashlib
def create_micro_brewery(name):
name = "Hipster Brew Co." if name is None else name
slug = hashlib.sha1(name.encode()).hexdigest()
# etc.
|
… 기본인수를 지정할수 있는데, 또한 인수로 문자열을 받을 것을 명확히 합니다.
Good:
1
2
3
4
5
6
| import hashlib
def create_micro_brewery(name: str = "Hipster Brew Co."):
slug = hashlib.sha1(name.encode()).hexdigest()
# etc.
|
⬆ back to top
함수 인수(2개 이하가 이상적입니다)#
함수 매개 변수의 양을 제한하는 것은 함수를 더 쉽게 테스트할 수 있기 때문에 매우 중요합니다. 세 개 이상이면 조합 폭발로 이어지며, 각각의 개별 논증으로 수많은 다른 사례를 테스트해야 합니다.
인수가 하나도 없는 것이 이상적인 경우입니다. 1~2개의 인수느 괜찮고, 세개는 피해야 하고 그 이상의 것은 통합되어야 합니다.(객체 등으로). 보통, 만약 여러분이 두 개 이상의 인수를 가지고 있다면, 여러분의 함수는 너무 많은 것을 하려고 하는 것입니다. 그렇지 않은 경우에는 대부분 더 높은 수준의 객체를 인수로 사용하는 것이 좋습니다.
Bad:
1
2
| def create_menu(title, body, button_text, cancellable):
pass
|
Java-esque:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| class Menu:
def __init__(self, config: dict):
self.title = config["title"]
self.body = config["body"]
# ...
menu = Menu(
{
"title": "My Menu",
"body": "Something about my menu",
"button_text": "OK",
"cancellable": False
}
)
|
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
| 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) -> None:
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)
|
Fancy
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
| 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!"
)
)
|
Even fancier
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
| from 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!"
)
)
|
Even fancier, Python3.8+ only
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
| from typing import TypedDict
class MenuConfig(TypedDict):
"""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
def create_menu(config: MenuConfig):
title = config["title"]
# ...
create_menu(
# You need to supply all the parameters
MenuConfig(
title="My delicious menu",
body="A description of the various items on the menu",
button_text="Order now!",
cancellable=True
)
)
|
⬆ back to top
함수는 한 가지 일을 해야 한다.#
이것은 소프트웨어 공학에서 단연코 가장 중요한 규칙입니다. 함수가 한 가지 이상의 일을 할 때는 구성, 테스트, 추론하기가 더 어려워진다. 함수를 하나의 동작으로 분리할 수 있으면 쉽게 리팩터링할 수 있으며 코드가 훨씬 깔끔하게 읽힙니다.
이 가이드에서 이것만 지키더라도, 당신은 많은 개발자보다 앞서게 될 것입니다.
Bad:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from typing import List
class Client:
active: bool
def email(client: Client) -> None:
pass
def email_clients(clients: List[Client]) -> None:
"""Filter active clients and send them an email.
"""
for client in clients:
if client.active:
email(client)
|
Good:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| from typing import List
class Client:
active: bool
def email(client: Client) -> None:
pass
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 get_active_clients(clients):
email(client)
|
이제 제너레이터를 사용할 수 있는 기회가 보이나요?
Even better
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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)
def email_client(clients: Iterator[Client]) -> None:
"""Send an email to a given list of clients.
"""
for client in active_clients(clients):
email(client)
|
⬆ back to top
함수 이름에는 해당 기능이 명시되어 있어야 합니다.#
Bad:
1
2
3
4
5
6
7
| class Email:
def handle(self) -> None:
pass
message = Email()
# What is this supposed to do again?
message.handle()
|
Good:
1
2
3
4
5
6
| class Email:
def send(self) -> None:
"""Send this message"""
message = Email()
message.send()
|
⬆ back to top
함수는 추상화의 한 수준이어야 합니다#
당신이 하나 이상의 추상화를 가지고 있을 때, 당신의 함수는 보통 너무 많은 일을 하고 있습니다.
함수를 분할하면 재사용이 가능하고 테스트가 쉬워집니다.
(*역: 예로 마틴 파울러의 리팩토링 책에서는 단 한 줄짜리 함수도 규칙에 맞게 분리된다면 문제가 없다고 이야기하고 있습니다.)
Bad:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # type: ignore
def parse_better_js_alternative(code: str) -> None:
regexes = [
# ...
]
statements = code.split('\n')
tokens = []
for regex in regexes:
for statement in statements:
pass
ast = []
for token in tokens:
pass
for node in ast:
pass
|
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
| from typing import Tuple, List, Dict
REGEXES: Tuple = (
# ...
)
def parse_better_js_alternative(code: str) -> None:
tokens: List = tokenize(code)
syntax_tree: List = parse(tokens)
for node in syntax_tree:
pass
def tokenize(code: str) -> List:
statements = code.split()
tokens: List[Dict] = []
for regex in REGEXES:
for statement in statements:
pass
return tokens
def parse(tokens: List) -> List:
syntax_tree: List[Dict] = []
for token in tokens:
pass
return syntax_tree
|
⬆ back to top
플래그를 함수 매개 변수로 사용하지 말 것#
플래그는 사용자에게 이 기능이 하나 이상의 기능을 수행함을 알려줍니다. 함수는 한 가지 일을 해야 한다. 함수가 Boolean을 기준으로 다른 코드 경로를 따르는 경우 함수를 분할합니다.
(*역: SOLID 원칙에서 Single-responsibility Principle (SRP)에 해당하는 내용입니다.)
Bad:
1
2
3
4
5
6
7
8
9
| from tempfile import gettempdir
from pathlib import Path
def create_file(name: str, temp: bool) -> None:
if temp:
(Path(gettempdir()) / name).touch()
else:
Path(name).touch()
|
Good:
1
2
3
4
5
6
7
8
9
10
| from tempfile import gettempdir
from pathlib import Path
def create_file(name: str) -> None:
Path(name).touch()
def create_temp_file(name: str) -> None:
(Path(gettempdir()) / name).touch()
|
⬆ back to top
사이드 이펙트(부작용)를 피하세요. (*역: 명령-질의 원칙의 내용입니다.)#
함수는 다른 값을 가져와서 반환하는 것 외에 다른 기능을 수행하는 경우 부작용을 낳습니다. 부작용은 파일에 쓰는 것일 수도 있고, 몇몇 전역 변수의 값을 수정하는 것, 실수로 모든 돈을 낯선 사람에게 보내는 것이 될 수 있습니다.
때때로 프로그램에서 부작용을 가질 필요가 있습니다. 앞의 예와 같이, 한 파일에 쓸 필요가 있을 수 있습니다. 우리가 하고 싶은 것은 이 일을 하는 곳을 모으는 것입니다. 특정 파일에 쓰기 위한 여러 개의 함수와 클래스를 갖지 마세요. 그 역할을 하는 하나의 서비스만 가지세요. 하나, 단 하나만요.
요점은 일반적인 함정을 피하라는 것입니다. 구조가 없는 객체 간에 상태를 공유하는 것이나, 어떤 것으로도 쓰일 수 있는 변동성 데이터를 사용하는 것, 부작용이 발생할 수 있는 곳을 모으지 않는 것 같은 함정에서요. 만약 우리가 함정을 피할 수 있다면, 다른 대부분의 프로그래머보다 더 행복해질 겁니다.
Bad:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # 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
| 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"]
|
⬆ back to top
객체와 자료구조#
Coming soon
⬆ back to top
클래스#
Single Responsibility Principle (SRP)#
Open/Closed Principle (OCP)#
Liskov Substitution Principle (LSP)#
Interface Segregation Principle (ISP)#
Dependency Inversion Principle (DIP)#
Coming soon
⬆ back to top
Don’t repeat yourself (DRY)#
DRY원칙을 지키기 위해 노력하세요.
코드가 중복되지 않도록 최선을 다하세요. 중복 코드는 어떤 논리를 바꿔야 할 경우 변경할 곳이 두 곳 이상 있다는 것을 의미하기 때문에 좋지 않습니다.
당신이 식당을 운영하고 당신의 모든 토마토, 양파, 마늘, 향신료 등 당신의 재고 목록을 추적한다고 상상해 보세요. 만약 당신이 이것을 보관하고 있는 여러 목록이 있다면, 당신이 토마토가 들어있는 요리를 제공할 때 모든 목록이 업데이트되어야 합니다. 목록이 하나뿐이면 업데이트할 장소가 하나뿐입니다!
두 개 이상의 약간 다른 공통점을 가지고 있기 때문에 중복 코드가 있는 경우가 많지만, 그 차이점 때문에 어쩔 수 없습니다.
거의 같은 일을 하는 두 개 이상의 분리된 기능을 가지고 있습니다. 중복 코드를 제거하는 것은 하나의 함수/모듈/클래스만으로 이러한 다른 것들을 처리할 수 있는 추상화를 만드는 것을 의미합니다.
추상화를 올바르게 하는 것은 매우 중요합니다. 잘못된 추상화는 중복 코드보다 더 나쁠 수 있으므로 주의해야합니다! 하지만 할 수 있다면
추상화를 시도해보는 것도 좋습니다!
반복하지 마십시오, 그렇지 않으면 한 가지를 변경하고 싶을 때마다 여러 장소를 업데이트하고 있는 자신을 발견할 수 있습니다.
*역 :<클린 아키텍처> 책에서 이에 대한 좋은 내용을 언급하는데, 그 내용은 중복에도 종류가 있다는 것이다.
- 진짜 중복 :
한 인스턴스가 변경되면, 동일한 변경을 그 인스턴스의 모든 복사본에 반드시 적용해야한다.
- 우발적 중복(거짓된 중복) :
중복으로 보이는 두 코드의 영역이 각자의 경로로 발전한다면, 즉 서로 다른 속도와 다른 이유로 변경된다면 이 두 코드는 진짜 중복이 아니다.
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
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
| from typing import List, Dict
from dataclasses import dataclass
@dataclass
class Developer:
def __init__(self, experience: float, github_link: str) -> None:
self._experience = experience
self._github_link = github_link
@property
def experience(self) -> float:
return self._experience
@property
def github_link(self) -> str:
return self._github_link
@dataclass
class Manager:
def __init__(self, experience: float, github_link: str) -> None:
self._experience = experience
self._github_link = github_link
@property
def experience(self) -> float:
return self._experience
@property
def github_link(self) -> str:
return self._github_link
def get_developer_list(developers: List[Developer]) -> List[Dict]:
developers_list = []
for developer in developers:
developers_list.append({
'experience' : developer.experience,
'github_link' : developer.github_link
})
return developers_list
def get_manager_list(managers: List[Manager]) -> List[Dict]:
managers_list = []
for manager in managers:
managers_list.append({
'experience' : manager.experience,
'github_link' : manager.github_link
})
return managers_list
## create list objects of developers
company_developers = [
Developer(experience=2.5, github_link='https://github.com/1'),
Developer(experience=1.5, github_link='https://github.com/2')
]
company_developers_list = get_developer_list(developers=company_developers)
## create list objects of managers
company_managers = [
Manager(experience=4.5, github_link='https://github.com/3'),
Manager(experience=5.7, github_link='https://github.com/4')
]
company_managers_list = get_manager_list(managers=company_managers)
|
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
| from typing import List, Dict
from dataclasses import dataclass
@dataclass
class Employee:
def __init__(self, experience: float, github_link: str) -> None:
self._experience = experience
self._github_link = github_link
@property
def experience(self) -> float:
return self._experience
@property
def github_link(self) -> str:
return self._github_link
def get_employee_list(employees: List[Employee]) -> List[Dict]:
employees_list = []
for employee in employees:
employees_list.append({
'experience' : employee.experience,
'github_link' : employee.github_link
})
return employees_list
## create list objects of developers
company_developers = [
Employee(experience=2.5, github_link='https://github.com/1'),
Employee(experience=1.5, github_link='https://github.com/2')
]
company_developers_list = get_employee_list(employees=company_developers)
## create list objects of managers
company_managers = [
Employee(experience=4.5, github_link='https://github.com/3'),
Employee(experience=5.7, github_link='https://github.com/4')
]
company_managers_list = get_employee_list(employees=company_managers)
|
⬆ back to top