@zedr, Thank you for making a great document!!!
- ์๊ฐ
- ๋ณ์
- ํจ์
- ํด๋์ค (๊ฐ์ฒด์งํฅ 5์์น)
- 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)
- Translations
Robert C. Martin์ ์ฑ , Clean Code์ ์ฐธ๊ณ ํ์์ต๋๋ค.
์ด ๋ฌธ์๋ Python์ ๋ง๊ฒ ์์ ๋์์ผ๋ฉฐ style guide๊ฐ ์๋๋๋ค.
์ด ๋ฌธ์๋ Python์ผ๋ก ์ฝ์ ์ ์๊ณ (readable) ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๋ฉฐ(reusable), ๋ฆฌํํ ๋ง ๊ฐ๋ฅํ(refactorable) ์ํํธ์จ์ด๋ฅผ ๋ง๋ค์ด ๋ด๊ธฐ ์ํ ๊ฐ์ด๋๋ผ์ธ์ ์ ์ํฉ๋๋ค.
์ด ๋ฌธ์์ ๋ชจ๋ ๊ฒ์ ์์ ํ ๋ฐ๋ฅผ ํ์๋ ์์ผ๋ฉฐ, ๊ฐ ๊ตฌ์ฑ์ ๊ฐ ๋ณดํธ์ ํฉ์์ ๋ฐ๋ผ๊ฐ๋ฉด ๋ฉ๋๋ค.
๋ค์ ๋งํ์ง๋ง ์ด ๋ฌธ์์์ ์ธ๊ธํ๋ ๊ฒ๋ค์ ๋ชจ๋ ์ง์นจ์ผ ๋ฟ์
๋๋ค.
๋ค๋ง, Clean Code์ ์ ์๋ค์ ์ํด ์๋
๊ฐ์ ๊ฒฝํ์ ์ํด ์ ๋ฆฝ๋ ๊ฒ๋ค์
๋๋ค.
clean-code-javascript์ ๋ฌธ์๋ฅผ Python 3.7+
๋ฒ์ ์ ๋ง๊ฒ ์์ ํ์์ต๋๋ค.
(meaningful, pronounceable)
๋์ ์:
import datetime
ymdstr = datetime.date.today().strftime("%y-%m-%d")
์ข์ ์:
import datetime
current_date: str = datetime.date.today().strftime("%y-%m-%d")
๋์ ์: ์๋ ์์ ๋ ๋์ผ ๋์(entity)์ ๋ํด 3๊ฐ์ ๋ค๋ฅธ ์ด๋ฆ์ ์ฌ์ฉํฉ๋๋ค.
def get_user_info(): pass
def get_client_data(): pass
def get_customer_record(): pass
์ข์ ์:
๋ง์ฝ entity๊ฐ ๋์ผํ๋ค๋ฉด, ์ผ๊ด์ฑ ์๊ฒ(consistent) ๋ณ์๋ ํจ์์ ์ด๋ฆ์ ์ง๋ ๊ฒ์ด ์ข์ต๋๋ค.
def get_user_info(): pass
def get_user_data(): pass
def get_user_record(): pass
์ฐธ๊ณ ํ๋ฉด ์ข์ ์:
Python์ ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์
๋๋ค. ํ์ํ ๊ฒฝ์ฐ ์ธ์คํด์ค์ ์์ฑ(attribute), ํ๋กํผํฐ ๋ฉ์๋(property method)๋ ๋ฉ์๋(method)์ ํจ๊ป ์ฝ๋์์ entity์ ๊ตฌ์ฒด์ ์ธ ๊ตฌํ ๋ฐ ํจํค์งํํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
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()
์ฐ๋ฆฌ๋ ์ฝ๋ฉ์ ํ๋ฉฐ ๋ง์ ์ฝ๋๋ฅผ ์ฝ์ต๋๋ค. ๋๋ฌธ์ ์ฐ๋ฆฌ๊ฐ ์์ฑํ๋ ์ฝ๋๋ฅผ ์ฝ๊ธฐ ์ฝ๊ณ ๊ฒ์ ๊ฐ๋ฅํ ์ด๋ฆ์ผ๋ก ์ ์ธํ๋ ๊ฒ์ ์ค์ํฉ๋๋ค.
๋ง์ฝ ๋ณ์๋ฅผ ์ ์ธํ ๋ ์๋ฏธ๊ฐ ์๊ฑฐ๋ ๊ฒ์์ ์ด๋ ค์์ ์ฃผ๋ ์ด๋ฆ์ผ๋ก ์ ์ธํ๋ค๋ฉด, ์ฐ๋ฆฌ์ ์ฝ๋๋ฅผ ์ฝ๋ ๋ค๋ฅธ ์ฌ๋๋ค์ด ํ๋ค์ดํ ๊ฒ์ ๋๋ค.
๊ฒ์ ๊ฐ๋ฅํ(์ ์ถ ๊ฐ๋ฅํ) ์ด๋ฆ์ ์ฌ์ฉํฉ์๋ค.
๋์ ์:
import time
# What is the number 86400 for again?
time.sleep(86400)
์ข์ ์:
import time
# Declare them in the global namespace for the module.
SECONDS_IN_A_DAY = 60 * 60 * 24
time.sleep(SECONDS_IN_A_DAY)
(explanatory)
๋์ ์:
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]}")
๋์์ง๋ ์์ ์:
๋์์ง๋ ์์ง๋ง, ์ฌ์ ํ regex์ ๊ฒฐ๊ณผ์ ์์กดํ๊ณ ์์ต๋๋ค.
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}")
์ข์ ์:
ํ์ ํจํด์ ์ด๋ฆ์ ์ง์ ํจ์ผ๋ก์จ regex ๊ฒฐ๊ณผ์ ๋ํ ์์กด์ฑ์ ์ค์ผ ์ ์์ต๋๋ค.
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']}")
๋ณ์๊ฐ ์๋ฏธํ๋ ๋ฐ๊ฐ ๋ฌด์์ธ์ง๋ฅผ ์ฝ๋๋ฅผ ์์ธํ ๋ณด์ง ์์๋ ์ ์ ์๋๋ก ํ์ธ์.
๋ช ์์ ์ธ ๊ฒ์ด ์๋ฌต์ ์ธ ๊ฒ๋ณด๋ค ์ข์ต๋๋ค.
๋์ ์:
seq = ("Austin", "New York", "San Francisco")
for item in seq:
# do_stuff()
# do_some_other_stuff()
# Wait, what's `item` again?
print(item)
์ข์ ์:
locations = ("Austin", "New York", "San Francisco")
for location in locations:
# do_stuff()
# do_some_other_stuff()
# ...
print(location)
ํด๋์ค/๊ฐ์ฒด ์ด๋ฆ์ด ๋ฌด์ธ๊ฐ๋ฅผ ์ด๋ฏธ ์๋ ค์ฃผ๋ ๊ฒฝ์ฐ, ๋ณ์ ์ด๋ฆ์์ ์ด๋ฅผ ๋ฐ๋ณตํ์ง ๋ง์ธ์.
๋์ ์:
class Car:
car_make: str
car_model: str
car_color: str
์ข์ ์:
class Car:
make: str
model: str
color: str
์ฌ๊ธฐ์ short circuiting
์ ๋
ผ๋ฆฌ ์ฐ์ฐ(and, or)๋ฅผ ์๋ฏธํฉ๋๋ค.
๋ค์๊ณผ ๊ฐ์ ์ํฉ์์:
import hashlib
def create_micro_brewery(name):
name = "Hipster Brew Co." if name is None else name
slug = hashlib.sha1(name.encode()).hexdigest()
# etc.
๋ง์ฝ ์์ ๊ฐ์ด ์กฐ๊ฑด๋ฌธ์ ์ฌ์ฉํ๋ ๊ฒ ๋์ ๋งค๊ฐ๋ณ์๋ง์ ์ฌ์ฉํ๋๋ผ๋ ํจ์์ ๋์์ ์๋ฌด๋ฐ ์ํฅ์ด ์๋ค๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
์ฐ๋ฆฌ๋ ์ ์ฝ๋๋ฅผ ์๋์ ๊ฐ์ด ์์ ํ๊ณ ์ถ์ ๊ฒ์ ๋๋ค.
์ข์ ์:
import hashlib
def create_micro_brewery(name: str = "Hipster Brew Co."):
slug = hashlib.sha1(name.encode()).hexdigest()
# etc.
ํจ์๋ ์ํํธ์จ์ด ์์ง๋์ด๋ง์์ ๊ฐ์ฅ ์ค์ํ rule ์ค ํ๋์ ๋๋ค.
ํจ์๋ค์ด ํ๋ ์ด์์ ์์ ์ ์ํํ๋ค๋ฉด ๊ด๋ฆฌ, ํ ์คํธ ๋ฐ ์ถ๋ก ์ ์ด๋ ค์์ ๊ฒช์ ๊ฒ์ ๋๋ค.
ํจ์๋ฅผ ํ๋์ ์์ ์ผ๋ก ๋ถ๋ฆฌํ๋ค๋ฉด, ๋ฆฌํํ ๋ง(refactoring)์ด ์ฌ์์ง๊ณ ์ฝ๋๋ฅผ ํจ์ฌ ๊นจ๋ํ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค.
๋ง์ฝ ์ด rule๋ฅผ ์์งํ๊ณ ์ค์ฒํ๋ค๋ฉด, ์ฌ๋ฌ๋ถ์ ๋ง์ ๊ฐ๋ฐ์๋ค์ ์์๊ฒ ๋ ๊ฒ์ ๋๋ค.
๋์ ์:
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)
์ข์ ์ 1:
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)
์ ์ฝ๋์์ generator๋ฅผ ์ฌ์ฉํ ์ ์๋ ๋ถ๋ถ์ด ๋ณด์ด์๋์?
์ข์ ์ 2:
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)
๋งค๊ฐ๋ณ์์ ์๊ฐ ๋ง๋ค๋ ๊ฒ์ ์ผ๋ฐ์ ์ผ๋ก ํจ์๊ฐ ๋๋ฌด ๋ง์ ์ผ์ ์ํํ๋ค๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค. (has more than one responsibility)
๋๋ฌธ์ ๋งค๊ฐ๋ณ์์ ๊ฐ์๋ฅผ ์ ํํ๋ค๋ฉด ํจ์๋ฅผ ๋ ์ฝ๊ฒ ํ ์คํธ ํ ์ ์์ต๋๋ค.
๋งค๊ฐ๋ณ์๊ฐ ๋ง์ ํจ์๋ฅผ ๋งค๊ฐ๋ณ์๊ฐ ์ ์ ํจ์๋ก ๋ถํดํ ์ ์๋ค๋ฉด ํด๋ณด์ธ์. ์ด์์ ์ผ๋ก๋ 3๊ฐ ๋ฏธ๋ง์ ๋๋ค.
ํจ์์ ๋ํด ๋จ์ผ ์ฑ ์(single responsibility)์ด ์๋ ๊ฒฝ์ฐ ์ฌ๋ฌ ๊ฐ์ ๋งค๊ฐ๋ณ์๋ฅผ ํ๋์ ํน์ํ ๊ฐ์ฒด๋ก ๋ฌถ์ ์ ์๋์ง๋ ์ดํด๋ณด์ธ์.
ํ๋ก๊ทธ๋จ์์ ๋ค๋ฅธ ๊ณณ์ ๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฌ์ฉํด์ผ ํ๋ ์ํฉ์ด ์จ๋ค๋ฉด ์ด ๊ฐ์ฒด๋ฅผ ์๊ธดํ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๋ํ ์ด ๋ฐฉ๋ฒ์ด ์ฌ๋ฌ ๊ฐ์ ๋งค๊ฐ๋ณ์๋ฅผ ๊ฐ๋ ๊ฒ ๋ณด๋ค ๋ ๋์ ์ด์ ๋
ํจ์ ๋ด๋ถ์ ๋งค๊ฐ๋ณ์๋ฅผ ์ฌ์ฉํ์ฌ ์ํ๋๋ ์ฐ์ฐ๋ค์ ๋ ํ๋์ ํจ์๋ก ๋ง๋ค์ด ๋ณต์ก์ฑ์ ์ค์ผ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
๋์ ์:
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
}
)
์ข์ ์ 1:
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)
์ข์ ์ 2:
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!"
)
)
์ข์ ์ 3:
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!"
)
)
์ข์ ์ 4 (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
)
)
๋์ ์:
class Email:
def handle(self) -> None:
pass
message = Email()
# What is this supposed to do again?
message.handle()
์ข์ ์:
class Email:
def send(self) -> None:
"""Send this message"""
message = Email()
message.send()
๋ง์ฝ ํจ์์ ์ถ์์ ์ธ ์ธต์ด ํ๋ ์ด์ ์๋ค๋ฉด, ํจ์๊ฐ ๋๋ฌด ๋ณต์กํด์ง๋๋ค.
์ถ์์ธต์ด ์ฌ๋ฌ ๊ฐ ์๋ค๋ฉด, ๊ทธ๊ฒ๋ค์ ํจ์๋ก ๋ถํดํ์ฌ ์ฌ์ฌ์ฉ์ฑ์ ๋์ด๊ณ ํ ์คํธ์ ์ฉ์ดํ๋๋ก ํ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๋์ ์:
# 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
์ข์ ์:
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
flag๋ ์ฌ์ฉ์๋ก ํ์ฌ๊ธ ์ด ํจ์๊ฐ ๋๊ฐ์ง ์ด์์ ๊ธฐ๋ฅ์ ์ํํ๋ค๋ ๊ฒ์ผ๋ก ๋ณด์ฌ์ง ์ ์์ต๋๋ค.
ํจ์๋ ํ๊ฐ์ง ์ผ์ ํด์ผํฉ๋๋ค. bool์ ๊ธฐ์ค์ผ๋ก ํจ์์ ๊ธฐ๋ฅ์ด ์์ ํ ๋ฐ๋๋ค๋ฉด ํจ์๋ฅผ ๋ถํ ํด๋ณด์ธ์.
๋์ ์:
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()
์ข์ ์:
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()
์ฌ๊ธฐ์ ๋งํ๋ ๋ถ์์ฉ(side effect)์ ๋ถ์ ์ ์ธ ์๋ฏธ๊ฐ ์๋๋๋ค.
ํจ์๋ ์ผ๋ฐ์ ์ผ๋ก ๋งค๊ฐ๋ณ์๋ฅผ ๋ฐ์ ํ ์ผ๋ จ์ ์์ ์ ๊ฑฐ์ณ ๊ฐ์ ๋ฐํํฉ๋๋ค.
๋ง์ฝ ๊ฐ์ ๋ฐํํ๋ ๊ฒ ์ด์ธ์ ๋ค๋ฅธ ์์ ์ ์ถ๊ฐ๋ก ์ํํ๋ ๊ฒฝ์ฐ ์ด ํ์๋ฅผ ๋ถ์์ฉ์ด๋ผ ๋ถ๋ฆ ๋๋ค.
์๋ฅผ ๋ค์ด ๋ถ์์ฉ์ผ๋ก ํ์ผ์ ๊ธ์ ์ธ ์๋ ์์ผ๋ฉฐ, ํ์ผ์ ํน์ ๋ณ์๋ฅผ ์์ ํ ์๋ ์๊ณ , ์ค์๋ก ๋ชจ๋ ๋์ ๋ฏ์ ์ฌ๋์๊ฒ ์ก๊ธํ ์๋ ์์ต๋๋ค.
๋ง์ฝ ๋ถ์์ฉ์ ๊ผญ ํ์๋ก ํ๋ค๋ฉด, ๋ถ์์ฉ์ด ์ ๋ฐ๋๋ ์์น๋ฅผ ํ์ํด์ฃผ๋ ๊ฒ์ด ์ข์ต๋๋ค.
๋ํ ๋ค๋ฅธ ํจ์๋ ํด๋์ค๊ฐ ๋์์ ๋์ผํ ํ์ผ์ ์กฐ์ํ์ง ์๋๋ก ํ๊ณ ํน์ ํจ์๋ฅผ ํตํด ํ์ผ์ ์ด ํ์ผ์ ์กฐ์ํ๋๋ก ํฉ์๋ค.
์ฃผ์ ์์ ์ ๊ฐ์ฒด ๊ฐ ์ํ ๊ณต์ , ๊ฐ๋ณ ๋ฐ์ดํฐ ๋ฑ์ ์ฌ์ฉํ์ฌ ๋ชจ๋ ํจ์ ๋๋ ๋ณ์๊ฐ ์ด๋ฌํ ๋ฐ์ดํฐ(ํ์ผ ํน์ ํ์ผ ๋ด ๋ฐ์ดํฐ)๋ฅผ ์กฐ์ํ ์ ์๊ฒ ๋๋ ์ผ๋ฐ์ ์ธ ํจ์ ์ ํผํ ํ์๊ฐ ์์ต๋๋ค.
๋ง์ฝ ์ด๊ฒ์ ์ ์งํจ๋ค๋ฉด, ๋ค๋ฅธ ํ๋ก๊ทธ๋๋จธ๋ค๋ณด๋ค ์ค๋ฅ๋ฅผ ์ฐพ๊ธฐ ๋ ์์ํด์ง ๊ฒ์ ๋๋ค.
๋์ ์:
# 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?
์ข์ ์ 1:
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
์ข์ ์ 2:
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"]
์ค๋ช ํ๊ธฐ์ ์์ ์ฑ ์(responsibility)๋ฅผ ์ดํด๋ฅผ ์ํด ๊ธฐ๋ฅ์ผ๋ก ํด์ํ์์ ๋ฏธ๋ฆฌ ์๋ ค๋๋ฆฌ๊ฒ ์ต๋๋ค.
Robert C. Martin์ด ๋งํ๊ธฐ๋ฅผ...:
Class๊ฐ ๋ณ๊ฒฝ๋ ์ด์ ๋ ๋จ ํ๋์ฌ์ผ ํ๋ค. (A class should have only one reason to change.)
"๋ณ๊ฒฝ๋์ผ ํ ์ด์ "๋ ํด๋์ค ๋๋ ํจ์๊ฐ ๋ด๋นํ๋ ๊ธฐ๋ฅ์ ๋์ํฉ๋๋ค.
๋ค์ ์์ ์์๋ HTML ์ฃผ์์ ๋ง๋ค๊ณ ์ฃผ์์ pip์ ๋ฒ์ ์ ๊ธฐ๋กํฉ๋๋ค:
๋์ ์:
from importlib import metadata
class VersionCommentElement:
"""An element that renders an HTML comment with the program's version number
"""
def get_version(self) -> str:
"""Get the package version"""
return metadata.version("pip")
def render(self) -> None:
print(f'<!-- Version: {self.get_version()} -->')
VersionCommentElement().render()
์ ํด๋์ค๋ ๋๊ฐ์ง ๊ธฐ๋ฅ์ด ์์ต๋๋ค.
- pip ๋ฒ์ ์ ๋ณด๋ฅผ ํ๋ํฉ๋๋ค.
- HTML ์ฃผ์์ ์์ฑํฉ๋๋ค.
๋ค๋ง ์ ์ฝ๋์์ ํน์ ๊ธฐ๋ฅ์ ๋ณ๊ฒฝํ๋ฉด ๋ค๋ฅธ ๊ธฐ๋ฅ์ ์ํฅ์ ๋ฏธ์นฉ๋๋ค.
์ฐ๋ฆฌ๋ ์ด ๋ ๊ธฐ๋ฅ์ ๋ถํดํ ์ ์์ต๋๋ค.
์ข์ ์:
from importlib import metadata
def get_version(pkg_name: str) -> str:
"""Retrieve the version of a given package"""
return metadata.version(pkg_name)
class VersionCommentElement:
"""An element that renders an HTML comment with the program's version number
"""
def __init__(self, version: str):
self.version = version
def render(self) -> None:
print(f'<!-- Version: {self.version} -->')
VersionCommentElement(get_version("pip")).render()
์์ ๊ฐ์ด ์์ฑํ๋ฉด ์ด ํด๋์ค๋ HTML ์์๋ฅผ ์์ฑํ๋ ๊ฒ์๋ง ์ง์คํ๊ฒ ๋ฉ๋๋ค.
์ธ์คํด์คํํ ๋ ๋ฒ์ ๋ฒํธ๊ฐ ์ด๊ธฐ ๋งค๊ฐ๋ณ์๋ก ์ ๋ฌ๋ฉ๋๋ค. (get_version()
์ ํตํด ๋ฒ์ ์ ๋ณด๋ฅผ ์ป์)
ํด๋์ค ๋ฐ ํจ์๋ ์๋ก ๊ฒฉ๋ฆฌ๋์ด ์์ผ๋ฉฐ ๋ฒ์ ์ฌํญ์ด ๋ค๋ฅธ ํญ๋ชฉ์๋ ์ํฅ์ ๋ฏธ์น์ง ์์ต๋๋ค.
๋ํ get_version()
์ ์ฌ์ฌ์ฉ๋ ์ ์์ต๋๋ค.
์ํํธ์จ์ด์ ๊ฐ์ฒด(ํด๋์ค, ํจ์ ๋ฑ)๋ ํ์ฅ(extension)์ ๋ํด ์ด๋ ค ์์ด์ผ ํ์ง๋ง, ์์ (modification)์๋ ๋ซํ์์ด์ผ ํฉ๋๋ค.
ํด๋์ค ๊ฐ์ ๊ฐ์ฒด๋ ๋ด๋ถ ๋
ผ๋ฆฌ๋ฅผ ์์ ํ์ง ์๊ณ ์๋ก์ด ๊ธฐ๋ฅ์ ์ถ๊ฐํ ์ ์๋๋ก ๋ณด์ฅํด์ผํฉ๋๋ค.
(์๋ ์ฝ๋๋ฅผ ์์ ํ์ง ์์ผ๋ฉด์ ์ฝ๋๋ฅผ ์ถ๊ฐํ ์ ์์ด์ผ ํ๋ค๋ ์๋ฏธ์ ๊ฐ์ต๋๋ค.)
์ฆ, ๊ฐ์ฒด๋ ์ค๊ณ ์ด๊ธฐ์ ํ์ฅ์ฑ์ ๋ณด์ฅํด์ผ ํฉ๋๋ค.
๋ค์ ์์์๋ HTTP ์์ฒญ์ ์๋ตํ๋ ๊ฐ๋จํ ์น ํ๋ ์์ํฌ๋ฅผ ๊ตฌํํ๋ ์ฝ๋์ ๋๋ค.
HTTP ์๋ฒ์์ GET ์์ฒญ์ ๋ฐ์ผ๋ฉด View
ํด๋์ค .get()
๋ฉ์๋๊ฐ ํธ์ถ๋ฉ๋๋ค.
View
๋ ๋จ์ํ text/plain
๋ง ๋ฐํํฉ๋๋ค.
ํ์ง๋ง ์ฐ๋ฆฌ๋ text/HTML
์ ํํ๋ก ๋ฐ๊ธฐ๋ฅผ ์ํฉ๋๋ค.
๊ทธ๋์ ์ฐ๋ฆฌ๋ View
ํด๋์ค๋ฅผ ์์๋ฐ์ TemplateView
ํด๋์ค๋ฅผ ๋ง๋ค์์ต๋๋ค.
๋์ ์:
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."""
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()
)
์๋ก์ด ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด TemplateView
๋ View
๋ฅผ ์์๋ฐ๊ณ .get()
๋ฉ์๋๋ฅผ ๋ค์ ์ผ์ต๋๋ค.
์ ์ฝ๋๋ ๋ถ๋ชจ ํด๋์ค์ .get()
์ ๋ณ๊ฒฝํ์ง ์๊ณ ์์ ํด๋์ค์์ ์ค๋ฒ๋ผ์ด๋ฉ ํ ๊ฒฝ์ฐ์
๋๋ค.
๋ง์ฝ ์์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๊ธฐ๋ฅ์ด ์ฌ๋ฌ ๊ฐ๋ก ํ์๋๋ค๋ฉด,
ํ
์คํธ๋ฅผ ์ํํ ๋ View
ํด๋์ค์ ๋ชจ๋ ์์ ํด๋์ค์ ๋ํด ํ
์คํธ ๊ธฐ๋ฅ์ ์ถ๊ฐํด์ผํ ๊ฐ๋ฅ์ฑ์ด ์์ต๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ์ฝ๋๋ฅผ ๋ค์ ์ค๊ณํ๊ณ View
ํด๋์ค๊ฐ ๊นจ๋ํ๊ฒ ํ์ฅ๋๋๋ก ํฉ์๋ค.
์ข์ ์ 1:
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"
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 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()
์๋ต ๋ด์ฉ์ ๋ณ๊ฒฝํ๋ ค๋ฉด render_body()
๋ฅผ ์ฌ์ ์ํด์ผ ํ์ง๋ง
์ด ๋ฉ์๋๋ ํ์ ์ ํ์ ์ฌ์ ์ํ๋๋ก ์์ฒญํ๋ ์ ์ ์๋ ๋จ์ผ ์ฑ
์(single reponsibility)์ด ์์ต๋๋ค.
๊ทธ๋ฌ๋ ์ด ๋ฐฉ๋ฒ์ ์์ ํด๋์ค๊ฐ ๊ธฐ๋ฅ์ ํ์ฅํ๊ธฐ ์ํด ์ฌ์ ํ ์ฌ์ ์ํด์ผ ํฉ๋๋ค.
์์(inheritance)๊ณผ ์ปดํฌ์ง์
(composition)์ ์ฅ์ ์ ๋ชจ๋ ์ฌ์ฉํ๋ ๋ ๋ค๋ฅธ ์ข์ ๋ฐฉ๋ฒ์
Mixins์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์
๋๋ค.
Mixins์ ๋ค๋ฅธ ๊ด๋ จ ํด๋์ค๋ค๊ณผ๋ ๋ ๋ฆฝ์ ์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅํ bare-bones classes์ ๋๋ค.
target์ ๋์(behaviour)์ ๋ณ๊ฒฝํ๊ธฐ ์ํด ๋ค์ค ์์์ ์ฌ์ฉํ์ฌ target ํด๋์ค์ "mixed-in" ๋ฉ๋๋ค.
Rules:
- Mixins๋ ํญ์
object
์์ ์์๋์ด์ผ ํฉ๋๋ค. - Mixins๋ ํญ์ target ํด๋์ค ์์ ์์นํด์ผ ํฉ๋๋ค.
- e.g.
Foo(MixinA, MixinB, TargetClass)
- e.g.
์ข์ ์ 2:
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) # type: ignore
response.headers['Content-Length'] = len(response.body)
return response
class TemplateView(TemplateRenderMixin, ContentLengthMixin, View):
"""A view that returns HTML responses based on a template file."""
content_type = "text/html"
template_file = "index.html"
์ ์ฝ๋์์ ๋ณผ ์ ์๋ฏ์ด, Mixins
๋ ๊ด๋ จ ๊ธฐ๋ฅ์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ํด๋์ค๋ก ์บก์ํํจ์ผ๋ก์จ
๋ ์ฝ๊ฒ ํจํค์งํ ์ ์๊ฒ ๋์์ผ๋ฉฐ, ๋จ์ผ ์ฑ ์ ์์น(SRP)์๋ ๋ถํฉํฉ๋๋ค.
Django
๋ ์ฌ๋ฌ ๊ฐ์ง์ View
ํด๋์ค๋ฅผ ๊ตฌ์ฑํ๊ธฐ ์ํด Mixins๋ฅผ ๋ง์ด ์ฌ์ฉํ์ต๋๋ค.
FIXME: typing.Protocol
์ ์ฌ์ฉ ๋ฐฉ์์ด ์ ๋ฆฝ๋์๊ธฐ ๋๋ฌธ์ Mixins
์ type ๊ฒ์ฌ๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
"๋ถ๋ชจ ํด๋์ค์ ํฌ์ธํฐ๋ ์ฐธ์กฐ๋ฅผ ์ฌ์ฉํ๋ ํจ์๋
๋ถ๋ชจ ํด๋์ค๋ก๋ถํฐ ํ์๋ ์์ ํด๋์ค์ ๋ํด ๋ชฐ๋ผ๋ ์ฌ์ฉํ ์ ์์ด์ผ ํด.", Uncle Bob.
์ด ์์น์ A behavioral notion of subtyping (1994) ๋ ผ๋ฌธ์ ์ ์ Jeannette Wing๊ณผ ํ๋ ฅํ Barbara Liskov์ ์ด๋ฆ์ ๋ฐ์ ๋ช ๋ช ๋์์ต๋๋ค.
์ด ๋ ผ๋ฌธ์ ํต์ฌ ์์น์ "subtype์ด supertype์ ๋์ผํ ๋ฐฉ๋ฒ๊ณผ ๋์ผํ ๊ธฐ๋ฅ๊ณผ ๋์ผ ํ๋์ ์ ์งํด์ผ ํ๋ค"๋ ๊ฒ์ ๋๋ค.
๋ค์ ๋งํด supertype์ ํจ์๋ ๋ณ๋์ ์์ ์์ด ๋ชจ๋ subtype์ ์์ฉํ ์ ์์ด์ผ ํฉ๋๋ค.
์๋์ ์ฝ๋์์ ์ด๋ค ๋ฌธ์ ๊ฐ ์๋์ง ํ์ธํด๋ณด๋๋ก ํฉ์๋ค.
๋์ ์:
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"
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 TemplateView(View):
"""A view that returns HTML responses based on a template file."""
content_type = "text/html"
def get(self, request, template_file: str) -> Response: # type: ignore
"""Render the message body as HTML"""
with open(template_file) as fd:
return Response(
status=200,
content_type=self.content_type,
body=fd.read()
)
def render(view: View, request) -> Response:
"""Render a View"""
return view.get(request)
render()
๋ฉ์๋๋ View
ํด๋์ค ๋ฐ ํ์ ํด๋์ค์ธ TemplateView
์ ํจ๊ป ์ฌ์ฉํ ์ ์์ด์ผ ํฉ๋๋ค.
ํ์ง๋ง TemplateView
๋ ์์ ์ .get()
๋ฉ์๋์ signature(๋ฉ์๋์ ์
/์ถ๋ ฅ)์ ๋ณ๊ฒฝํ์ต๋๋ค.
TemplateView
์ render()
๋ฅผ ์ฌ์ฉํ ๊ฒฝ์ฐ ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒ์
๋๋ค.
๋ง์ฝ ์ฐ๋ฆฌ๊ฐ render()
๋ฅผ View
์ View
์ ํ์ ํด๋์ค์์ ์ฌ์ฉํ ์ ์๊ธฐ๋ฅผ ์ํ๋ค๋ฉด,
์ฐ๋ฆฌ๋ ์ธ๋ถ ์ธํฐํ์ด์ค๊ฐ ์์๋์ง ์๋๋ก ์ฃผ์ํด์ผ ํ ํ์๊ฐ ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ ์ฃผ์ด์ง ํด๋์ค์์ ๊ตฌ์ฑ์ ์ด๋ป๊ฒ ์ ์ ์์๊น์?
mypy์ ๊ฐ์ type ๊ฒ์ฌ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ๋ฉด
์ด์ ๋น์ทํ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๋์ ์ค๋ฅ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
error: Signature of "get" incompatible with supertype "View"
<string>:36: note: Superclass:
<string>:36: note: def get(self, request: Any) -> Response
<string>:36: note: Subclass:
<string>:36: note: def get(self, request: Any, template_file: str) -> Response
โ์ฌ์ฉ์๊ฐ ํ์์๋ ๊ฒ์ ์์กดํ์ง ์๋๋ก ์ธํฐํ์ด์ค๋ฅผ ๊ฐ๊ฒฐํ๊ฒ ๋ง๋๋ ๊ฑด ์ด๋?", Uncle Bob.
Java๋ Go์ ๊ฐ์ ์ ๋ช ํ ๊ฐ์ฒด ์งํฅ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด์์๋ ์ธํฐํ์ด์ค(interface)๋ผ๋ ๊ฐ๋ ์ด ์์ต๋๋ค.
์ธํฐํ์ด์ค ํด๋์ค๋ ๊ณต๊ฐ ๋ฉ์๋์ ์์ฑ์ ๊ตฌํํ์ง ์๊ณ ์ ์ํฉ๋๋ค.
ํจ์์ signature(ํจ์์ ์ /์ถ๋ ฅ)๋ฅผ ์ ์ํ๊ณ ์ถ์ง๋ง ๊ตฌ์ฒด์ ์ผ๋ก ๊ตฌํํ๊ณ ์ถ์ง ์์ ๋ ์ธํฐํ์ด์ค๋ ๋งค์ฐ ์ ์ฉํ๊ฒ ์ฌ์ฉ๋ฉ๋๋ค.
์ฐ๋ฆฌ๋ "๋น์ ์ด ๋์๊ฒ ์ ๋ฌํ ๋์์ ์ธ๋ถ ์ฌํญ์ ๋ํด์๋ ๊ด์ฌ์ด ์๊ณ , ๋ด๊ฐ ์ฌ์ฉํ ์ ์๋ ๋ฐฉ๋ฒ์ด๋ ์์ฑ์๋ง ๊ด์ฌ์ด ์๋ค."๊ณ ๋งํ ์ ์์ต๋๋ค.
Python์๋ ์ธํฐํ์ด์ค๊ฐ ์์ต๋๋ค.
๋ค๋ง ์ธํฐํ์ด์ค์๋ ์ฝ๊ฐ ๋ค๋ฅด์ง๋ง ์ถ์ ํด๋์ค๋ฅผ ์ฌ์ฉํ์ฌ ๋์ผํ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์์ต๋๋ค.
์ข์ ์:
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
@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)
welcome_user("Barbara", FriendlyActor())
์ด์ ๋ค์์ ์๋๋ฆฌ์ค๋ฅผ ์์ํด๋ด ์๋ค.
PDF ๋ฌธ์๊ฐ ๋ช ๊ฐ ์๋๋ฐ, ์ฐ๋ฆฌ ์น ์ฌ์ดํธ ์ฌ์ฉ์์๊ฒ PDF ํ์ผ์ ์ ๊ณตํ๊ณ ์ถ์ต๋๋ค.
์ฐ๋ฆฌ๋ ํ์ด์ฌ ์น ํ๋ ์์ํฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฌํ ๋ฌธ์๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํ ํด๋์ค๋ฅผ ์ค๊ณํ๊ณ ์ ํฉ๋๋ค.
๊ทธ๋์ ์ฐ๋ฆฌ๋ ๋ฌธ์์ ์ถ์ ํด๋์ค๋ฅผ ์ค๊ณํ๋๋ฐ, ์ด ํด๋์ค์ ์ฌ์ฉํ ์ ์๋ ๋ชจ๋ ๊ธฐ๋ฅ๋ค์ ์ ์ด๋์์ต๋๋ค.
์๋ฌ ๋ฐ์:
import abc
class Persistable(metaclass=abc.ABCMeta):
"""Serialize a file to data and back"""
@property
@abc.abstractmethod
def data(self) -> bytes:
"""The raw data of the file"""
@classmethod
@abc.abstractmethod
def load(cls, name: str):
"""Load the file from disk"""
@abc.abstractmethod
def save(self) -> None:
"""Save the file to disk"""
# We just want to serve the documents, so our concrete PDF document
# implementation just needs to implement the `.load()` method and have
# a public attribute named `data`.
class PDFDocument(Persistable):
"""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):
"""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
ํ์ง๋ง ์๋๋๋ผ๊ณ ์! .save()
๋ฉ์๋๋ฅผ ๊ตฌํํ์ง ์์ผ๋ฉด ์์ธ๊ฐ ๋ฐ์ํฉ๋๋ค.
Can't instantiate abstract class PDFDocument with abstract method save.
์ด๊ฑด ์ง์ฆ๋๋ค์. ์ฐ๋ฆฌ๋ .save()
๋ฅผ ๊ตฌํํ ํ์๊ฐ ์์ต๋๋ค.
์ฐ๋ฆฌ๋ ์๋ฌด๊ฒ๋ ํ์ง ์๊ฑฐ๋ NotImplementedError
๋ฅผ ๋ฐ์์ํค๋ ๋๋ฏธ ๋ฉ์๋๋ฅผ ๊ตฌํํ ์ ์์ง๋ง, ๊ทธ๊ฒ๋ํ ์ธ๋ชจ์๋ ์ฝ๋๊ฐ ๋ฉ๋๋ค.
๋์์ ๋ง์ฝ ์ฐ๋ฆฌ๊ฐ ์ถ์ ํด๋์ค์์ .save()
๋ฅผ ์ ๊ฑฐํ๋ค๋ฉด, ์ฌ์ฉ์๊ฐ ๋ฌธ์๋ฅผ save ํ ๋ ๋ค์ ์ถ๊ฐํด์ผ ํฉ๋๋ค.
๋ฌธ์ ๋ฅผ ์์ฝํ๋ฉด, ์ฐ๋ฆฌ๋ ์ธํฐํ์ด์ค๋ฅผ ์ผ๊ณ , ์ด ์ธํฐํ์ด์ค์๋ ํ์ฌ ์ฌ์ฉํ ์ ์๋ ๋ช๊ฐ์ง ํน์ฑ์ด ์๋ค๋ ๊ฒ์ ๋๋ค.
์ด ๋ฌธ์ ์ ๋ํ ํด๊ฒฐ๋ฐฉ์์ ์ด ์ธํฐํ์ด์ค๋ฅผ ๋ ์์ ์ธํฐํ์ด์ค๋ก ๋ถํดํ๊ณ , ๊ฐ๊ฐ์ ์๋ก์ด ์ธํฐํ์ด์ค๊ฐ ์ผ๋ถ ๋ด์ฉ์ ๋ด๋นํ๋๋ก ๋ง๋๋ ๊ฒ์ ๋๋ค.
์ข์ ์:
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):
...
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
"๊ตฌ์ฒด์ ์ธ ์ธ๋ถ ์ฌํญ(details)๋ณด๋ค๋ ์ถ์(abstractions)์ ์์กดํ๋ ๊ฑด ์ด๋?", Uncle Bob.
CSV ํ์ผ์ ํ์ ์ฆ์ ์คํธ๋ฆฌ๋ฐํ๋ HTTP Response๋ฅผ ๋ฐํํ๋ web view๋ฅผ ์์ฑํ๊ณ ์ถ๋ค๊ณ ์๊ฐํด๋ณด์ธ์.
์ฐ๋ฆฌ๋ ํ์ด์ฌ ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ์ ๊ณตํ๋ CSV writer๋ฅผ ์ฌ์ฉํ๊ณ ์ ํฉ๋๋ค.
Bad
import csv
from io import StringIO
class StreamingHttpResponse:
"""A streaming HTTP response"""
... # implementation code goes here
def some_view(request):
rows = (
['First row', 'Foo', 'Bar', 'Baz'],
['Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"]
)
# Define a generator to stream data directly to the client
def stream():
buffer_ = StringIO()
writer = csv.writer(buffer_, delimiter=';', quotechar='"')
for row in rows:
writer.writerow(row)
buffer_.seek(0)
data = buffer_.read()
buffer_.seek(0)
buffer_.truncate()
yield data
# Create the streaming response object with the appropriate CSV header.
response = StreamingHttpResponse(stream(), content_type='text/csv')
response[
'Content-Disposition'] = 'attachment; filename="somefilename.csv"'
return response
์ฒซ ๊ตฌํ์ CSV writer ์ธํฐํ์ด์ค๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
์ผ๋ถ ํ์ ์์ ์ ํ์ผ์ฒ๋ผ String I/O ๊ฐ์ฒด๋ฅผ ์กฐ์ํ์ฌ writer์ ๋ฐ์ดํฐ๋ฅผ ์ผ์ต๋๋ค.
์ด ๋ฐฉ๋ฒ์ ๋ฒ์กํ๊ณ ์ฐ์ํ์ง ์์ต๋๋ค.
๋ ์ข์ ๋ฐฉ๋ฒ์ writer๊ฐ .write()
๋ฉ์๋๋ฅผ ํฌํจํ๋ ๊ฐ์ฒด๋ง ํ์๋ก ํ๋ค๋ ๊ฒ์ ์ดํดํ๋ ๊ฒ์
๋๋ค.
StreamingHttpResponse
ํด๋์ค๊ฐ ์ฆ์ ํด๋ผ์ด์ธํธ๋ก ๋ค์ ์คํธ๋ฆฌ๋ฐํ ์ ์๋๋ก ์๋ก์ด ํ ๋ฐ์ดํฐ๋ฅผ ์ฆ์ ๋ฐํํ๋ dummy ๊ฐ์ฒด๋ฅผ ์ ๋ฌํ๋ ๊ฒ์ ์ด๋ค๊ฐ์?
Good
import csv
class Echo:
"""An object that implements just the write method of the file-like
interface.
"""
def write(self, value):
"""Write the value by returning it, instead of storing in a buffer."""
return value
def some_streaming_csv_view(request):
"""A view that streams a large CSV file."""
rows = (
['First row', 'Foo', 'Bar', 'Baz'],
['Second row', 'A', 'B', 'C', '"Testing"', "Here's a quote"]
)
writer = csv.writer(Echo(), delimiter=';', quotechar='"')
return StreamingHttpResponse(
(writer.writerow(row) for row in rows),
content_type="text/csv",
headers={
'Content-Disposition': 'attachment; filename="somefilename.csv"'},
)
์์ ๊ฐ์ด ๊ตฌํํ๋ฉด ์ด์ ์ ๊ฒ๋ณด๋ค ํจ์ฌ ๋ซ๊ณ ์ฐ์ํด์ง๋๋ค.
๋ ์ ์ ์ฝ๋๋ก ๋์ผํ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ค๋ ๊ฒ์ ์ฅ์ ์ด ๋ถ๋ช ํฉ๋๋ค.
์ฐ๋ฆฌ๋ writer ํด๋์ค์์ .write()
๋ผ๋ ์ถ์์ ์ธ ๋ฐฉ๋ฒ์๋ง ๊ด์ฌ์ด ์๊ณ ๋ด๋ถ ์ธ๋ถ ์ฌํญ์๋ ๊ด์ฌ์ด ์๋ค๋ ๊ฒ์ ํ์ฉํ์ต๋๋ค.
์ด ์์ ๋ a submission made to the Django document์์ ๊ฐ์ง๊ณ ์จ ๊ฒ์ ๋๋ค.
์ํคํผ๋์์ ์ค๋ณต ๋ฐฐ์ ์์น ๋ฌธ์๋ฅผ ์ดํด๋ณด๊ณ ์ค์ธ์.
์ค๋ณต ์ฝ๋๋ ์ฝ๋ ๋ก์ง์ ์์ ํ ๋ ์ค๋ณต๋๋ ๋ถ๋ถ๋ ๋์์ ์์ ํด์ผ ํ๋ค๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค.
์ค๋ณต ์ฝ๋๊ฐ ๋ง์ผ๋ฉด ๋ง์ ์๋ก ์์ ์์ ๋์ด ๋ง์์ง ์ ๋ฐ์ ์๊ณ ์ค๋ฅ ๋ฐ์ ๊ฐ๋ฅ์ฑ ๋ํ ๋์์ง๊ฒ ๋ฉ๋๋ค.
์๋น์ ์ด์ํ๊ณ ํ ๋งํ , ์ํ, ๋ง๋, ํฅ์ ๋ฃ ๋ฑ ์ฌ๊ณ ๋ฅผ ์กฐ์ฌํ๋ค๊ณ ์๊ฐํด๋ด ์๋ค.
๋ฆฌ์คํธ๊ฐ ์ฌ๋ฌ ๊ฐ ์์ผ๋ฉด ํ ๋งํ ํ๋๋ก ํ ๋งํ ๊ฐ ๋ค์ด๊ฐ ์๋ฆฌ๋ฅผ ๋ง๋ค์์ ๋ ์ฌ๋ฌ ๊ฐ์ ๋ฆฌ์คํธ๋ฅผ ์ ๋ถ ์ ๋ฐ์ดํธ ํด์ผ ํฉ๋๋ค.
๋ฐ๋๋ก ๋ชฉ๋ก์ด ํ๋๋ง ์์ผ๋ฉด ํ๋๋ง ์ ๋ฐ์ดํธ ํ๋ฉด ๋ฉ๋๋ค.
๊ณตํต์ ์ด ๋ง์ง๋ง ์ฝ๋ ์์ ์ฝ๊ฐ ๋ค๋ฅธ ๊ฒ์ด ์์ด์ ์ค๋ณต ์ฝ๋๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ๊ฐ ์ข ์ข ์์ต๋๋ค.
ํ์ง๋ง ๊ทธ ์ฐจ์ด๋ก ์ธํด ๋์ผํ ์์ ์ ์ํํ๋ ๋ ๊ฐ ์ด์์ ๊ฐ๋ณ ํจ์๊ฐ ํ์ํฉ๋๋ค.
์ค๋ณต ์ฝ๋๋ฅผ ์ ๊ฑฐํ๋ ค๋ฉด ๋จผ์ ๊ณตํต ๋ถ๋ถ์ ์ถ์ํํ ๋ค์ ํ๋์ ํจ์/๋ชจ๋/ํด๋์ค๋ก ๋ค๋ฅธ ๋ถ๋ถ์ ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค.
์ถ์์ ์ฌ๊ณ ๋ฅผ ์ ํ๋ ๊ฒ์ ํ๋ก๊ทธ๋๋จธ์๊ฒ ์์ด ๋งค์ฐ ์ค์ํ ์คํฌ ์ค ํ๋์ ๋๋ค.
๋์ ์ถ์์ ์ฌ๊ณ ๋ก ์ธํ ํผํด๋ ๋๋๋ก ์ค๋ณต ์ฝ๋๋ณด๋ค ๋ ์ฌ๊ฐํ ๋ฌธ์ ์ ์ง๋ฉดํ ์ ์์ต๋๋ค.
๋ง์ฝ ์ถ์์ ์ฌ๊ณ ๋ฅผ ์ ํ ์ ์๋ค๋ฉด, ๊ทธ๋ ๊ฒ ํ์ ์ผ ํฉ๋๋ค! ์ค๋ณต ์ฝ๋๋ฅผ ์์ฑํ์ง ๋ง์๋ค.
์ด๋ฅผ ์งํค์ง ์๋๋ค๋ฉด ๋ก์ง์ ๋ณ๊ฒฝํ๊ณ ์ ํ ๋ ๋ณ๊ฒฝํด์ผํ ๋ถ๋ถ์ด ๋ง๋ค๋ ๊ฒ์ ๊ณง ์๊ฒ ๋ ๊ฒ์ ๋๋ค.
์ถ์์ ์ฌ๊ณ ๋ฅผ ์ ํ๋ค๋ ๊ฒ:
๋ฒ์ญ์ ํ๋ฉฐ ์ ์ผ ์ดํดํ๊ธฐ ์ด๋ ค์ ๋ ๋ถ๋ถ์ด ๋ฐ๋ก abstraction์ด๋ผ๋ ๋ง์ธ๋ฐ,
๋งฅ๋ฝ์ ๋ณด๋ฉด ๊ณตํต์ ์ด๊ณ ๋ณธ์ง์ ์ธ ๋ถ๋ถ๋ง ์ถ์ถํ๊ณ ๊ฐ๋ณ์ ์ธ ๋ถ๋ถ์ ๋ฒ๋ฆฐ๋ค๋ ์๋ฏธ ๊ฐ์ต๋๋ค.
๋์ ์:
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)
์ด ๋ฌธ์๋ ๋ค์ํ ์ธ์ด๋ก ๋ฒ์ญ๋์์ต๋๋ค:
- ๐จ๐ณ ** Chinese** yinruiqing/clean-code-python
- ๐ต๐น ๐ง๐ท ** Portugese** fredsonchaves07/clean-code-python
- ๐ฎ๐ท ** Persian:** SepehrRasouli/clean-code-python