title: "Clean-Code-Python"
date: 2022-03-02T15:05:27+09:00
tags: ["Python"]
author: ["신승혁"]
원작자에게 허락을 맡고 개인적으로 번역한 글입니다. 출처
소프트웨어 엔지니어링의 원칙인 로버트 마틴의 클린코드를 파이썬에 적용한 내용입니다. 파이썬에서 읽을 수 있고 재사용 가능하며 리팩터블할 수 있는 소프트웨어를 만드는 안내서입니다.
여기에 있는 모든 원칙이 엄격하게 지켜질 필요는 없으며, 더 적은 수의 원칙을 보편적으로 사용할 수 있습니다. 여기에 소개된 내용은 가이드라인일 뿐 그 이상은 아니지만, 클린 코드 저자들에 의해 수년간의 집단 경험을 통해 문서화 된것입니다.
clean-code-javascript에서 영감을 받았습니다.
Python3.7+ 대상
Bad:
import datetime
ymdstr = datetime.date.today().strftime("%y-%m-%d")
Good:
import datetime
current_date: str = datetime.date.today().strftime("%y-%m-%d")
Bad:
이 예에서는 동일한 기본 엔티티에 대해 세 가지 다른 이름을 사용합니다:
def get_user_info(): pass
def get_client_data(): pass
def get_customer_record(): pass
Good:
엔티티가 동일하면 기능에서 해당 엔티티를 일관되게 참조해야 합니다:
def get_user_info(): pass
def get_user_data(): pass
def get_user_record(): pass
Even better
파이썬은 (또한) 객체 지향 프로그래밍 언어이기도 합니다. 타당하다면 구체적인 구현과 함께 기능을 패키징합니다.
인스턴스(instance)의 프로퍼티, 프로퍼티 메서드 또는 메서드로:
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()
우리는 우리가 코드를 작성하는 시간보다 읽는 시간이 더 많을겁니다. 우리가 읽을수 있고 검색가능한 코드를 만드는것은 중요합니다.
프로그램의 의미가 있는 변수들의 이름을 붙히지 않음으로써 우리는 코드를 읽는 사람들에게 상처를 주곤합니다.
이름을 검색 가능하게 만드세요.
Bad:
import time
# What is the number 86400 for again?
time.sleep(86400)
Good:
import time
# Declare them in the global namespace for the module.
SECONDS_IN_A_DAY = 60 * 60 * 24
time.sleep(SECONDS_IN_A_DAY)
Bad:
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:
더 좋긴 하지만, 여전히 정규식에 많이 의존하고 있습니다.
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:
하위 패턴의 이름을 지정하여 정규식에 대한 의존성을 줄입니다.
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']}")
변수가 의미하는 바를 당신의 코드를 읽는 사람에게 번역하도록 강요하지 마세요.
명시하는 것이 암묵적인 것보다 낫습니다.
Bad:
seq = ("Austin", "New York", "San Francisco")
for item in seq:
#do_stuff()
#do_some_other_stuff()
# Wait, what's `item` again?
print(item)
Good:
locations = ("Austin", "New York", "San Francisco")
for location in locations:
#do_stuff()
#do_some_other_stuff()
# ...
print(location)
클래스/객체 이름이 알려주는 내용이 있으면 변수 이름에 이 내용을 반복하지 마십시오.
Bad:
class Car:
car_make: str
car_model: str
car_color: str
Good:
class Car:
make: str
model: str
color: str
까다롭게
왜 이렇게 작성합니까?:
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:
import hashlib
def create_micro_brewery(name: str = "Hipster Brew Co."):
slug = hashlib.sha1(name.encode()).hexdigest()
# etc.
함수 매개 변수의 양을 제한하는 것은 함수를 더 쉽게 테스트할 수 있기 때문에 매우 중요합니다. 세 개 이상이면 조합 폭발로 이어지며, 각각의 개별 논증으로 수많은 다른 사례를 테스트해야 합니다.
인수가 하나도 없는 것이 이상적인 경우입니다. 1~2개의 인수느 괜찮고, 세개는 피해야 하고 그 이상의 것은 통합되어야 합니다.(객체 등으로). 보통, 만약 여러분이 두 개 이상의 인수를 가지고 있다면, 여러분의 기능은 너무 많은 것을 하려고 하는 것입니다. 그렇지 않은 경우에는 대부분 더 높은 수준의 객체가 인수로 충분합니다.
Bad:
def create_menu(title, body, button_text, cancellable):
pass
Java-esque:
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
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
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
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
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
)
)
이것은 소프트웨어 공학에서 단연코 가장 중요한 규칙입니다. 함수가 한 가지 이상의 일을 할 때는 구성, 테스트, 추론하기가 더 어려워진다. 함수를 하나의 동작으로 분리할 수 있으면 쉽게 리팩터링할 수 있으며 코드가 훨씬 깔끔하게 읽힙니다.
이 가이드에서 이것만 지키더라도, 당신은 많은 개발자보다 앞서게 될 것입니다.
Bad:
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:
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
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)
Bad:
class Email:
def handle(self) -> None:
pass
message = Email()
# What is this supposed to do again?
message.handle()
Good:
class Email:
def send(self) -> None:
"""Send this message"""
message = Email()
message.send()
당신이 하나 이상의 추상화를 가지고 있을 때, 당신의 함수는 보통 너무 많은 일을 하고 있습니다.
함수를 분할하면 재사용이 가능하고 테스트가 쉬워집니다.
(*역: 예로 마틴 파울러의 리팩토링 책에서는 단 한 줄짜리 함수도 규칙에 맞게 분리된다면 문제가 없다고 이야기하고 있습니다.)
Bad:
# 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:
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
플래그는 사용자에게 이 기능이 하나 이상의 기능을 수행함을 알려줍니다. 함수는 한 가지 일을 해야 한다. 함수가 Boolean을 기준으로 다른 코드 경로를 따르는 경우 함수를 분할합니다.
(*역: SOLID 원칙에서 Single-responsibility Principle (SRP)에 해당하는 내용입니다.)
Bad:
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:
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()
함수는 값을 가져다가 다른 값을 반환하는 것 외에 다른 작업을 하면 부작용이 발생합니다. 부작용의 예로는, 파일을 쓸때나, 전역 변수를 수정하거나, 실수로 모든 돈을 낯선 사람에게 송금하는 것일 수 있습니다.
그러나 때때로 예를 들어 이전 예제에서와 프로그램에 부작용이 있을 수 밖에 없는 경우가 있습니다. 오류를 파일에 써야 할 수도 있습니다. 이러한 경우 부작용을 통합하는 위치를 중앙에 표시하고 표시해야 합니다. 특정 파일에 쓰는 여러 기능과 클래스로 구현하는것보단 하나의 서비스로 구현해야 합니다.(*역: 응집력에 관한 이야기 입니다.)
요점은 구조 없이 객체 간에 상태를 공유하거나, 무엇이든 쓸 수 있는 변경 가능한 데이터 유형을 사용하거나, 클래스의 인스턴스를 사용하고, 부작용이 발생하는 곳을 중앙 집중화하지 않는 것과 같은 일반적인 함정을 피하는 것입니다.
이 규칙을 지킬수만 있다면 대다수의 다른 프로그래머보다 더 행복할 것입니다.
Bad:
# 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:
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
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"]
Coming soon
Coming soon
DRY원칙을 지키기 위해 노력하세요.
코드가 중복되지 않도록 최선을 다하세요. 중복 코드는 어떤 논리를 바꿔야 할 경우 변경할 곳이 두 곳 이상 있다는 것을 의미하기 때문에 좋지 않습니다.
당신이 식당을 운영하고 당신의 모든 토마토, 양파, 마늘, 향신료 등 당신의 재고 목록을 추적한다고 상상해 보세요. 만약 당신이 이것을 보관하고 있는 여러 목록이 있다면, 당신이 토마토가 들어있는 요리를 제공할 때 모든 목록이 업데이트되어야 합니다. 목록이 하나뿐이면 업데이트할 장소가 하나뿐입니다!
두 개 이상의 약간 다른 공통점을 가지고 있기 때문에 중복 코드가 있는 경우가 많지만, 그 차이점 때문에 어쩔 수 없습니다.
거의 같은 일을 하는 두 개 이상의 분리된 기능을 가지고 있습니다. 중복 코드를 제거하는 것은 하나의 함수/모듈/클래스만으로 이러한 다른 것들을 처리할 수 있는 추상화를 만드는 것을 의미합니다.
추상화를 올바르게 하는 것은 매우 중요합니다. 잘못된 추상화는 중복 코드보다 더 나쁠 수 있으므로 주의해야합니다! 하지만 할 수 있다면
추상화를 시도해보는 것도 좋습니다!
반복하지 마십시오, 그렇지 않으면 한 가지를 변경하고 싶을 때마다 여러 장소를 업데이트하고 있는 자신을 발견할 수 있습니다.
*역 :<클린 아키텍처> 책에서 이에 대한 좋은 내용을 언급하는데, 그 내용은 중복에도 종류가 있다는 것이다.
- 진짜 중복 :
한 인스턴스가 변경되면, 동일한 변경을 그 인스턴스의 모든 복사본에 반드시 적용해야한다.- 우발적 중복(거짓된 중복) :
중복으로 보이는 두 코드의 영역이 각자의 경로로 발전한다면, 즉 서로 다른 속도와 다른 이유로 변경된다면 이 두 코드는 진짜 중복이 아니다.
Bad:
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:
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)