일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 프로그래머스
- 그리디
- 머신러닝
- 이것이 코딩테스트다 with 파이썬
- 1단계
- 2단계
- MySQL
- 3단계
- pytorch
- 자료구조 및 실습
- ssd
- 모두를 위한 딥러닝 강좌 시즌1
- ubuntu
- 실전알고리즘
- test-helper
- cs
- AWS
- SWEA
- 전산기초
- Python
- 구현
- CS231n
- STL
- C++
- 파이썬
- Object detection
- 백준
- 코드수행
- docker
- 딥러닝
- Today
- Total
곰퓨타의 SW 이야기
CH 3. 좋은 코드의 일반적인 특징 본문
1. 계약에 의한 디자인
Contract
디자인할 때, 예상되는 입출력, 부작용에 대해서 명시하는 부분
DbC (Design by Contract) : 코드에 암묵적으로 기대하는 바를 추가하고, 잘못된 경우 명시적으로 예외 발생
소프트웨어 컴포넌트 간의 통신 중에 반드시 지켜야 할 몇 가지 규칙을 강제하는 것
- 사전 조건 : 코드 실행 전 확인해야할 것 (ex. 데이터베이스, 파일, 이전에 호출된 다른 메서드 검사 등)
- 사후 조건 : 함수 변환값의 유효성 검사
- 불변식 (invariant) : docstring으로 불변식에 대해 문서화
- 부작용 (side-effect) : docstring에 코드의 부작용 언급
계약하는 이유 : 오류를 쉽게 찾아낼 수 있음 + 잘못된 가정 하에 코드의 핵심 부분이 실행되는 것을 방지하기 위함
Pythonic Contract ? Function과 class에 예외를 발생시키는 것 (필요 시, 사용자 정의 예외)
2. 방어적 (defensive) 프로그래밍
예외 발생 및 실패 조건을 기술하는 것이 아닌,
객체, 함수 또는 메서드와 같은 코드의 모든 부분을 유효하지 않은 것으로부터 스스로 보호할 수 있게 하는 것
- Error handling : 예상할 수 있는 시나리오의 오류 처리 방법
- Assertion : 발생핮 않아야 하는 오류 처리 방법
Error handling
목적
- 에러에 대해 실행을 계속할 수 있을지, 극복할 수 없어 프로그램을 중단할 지 결정하기 위함
에러 처리 방법
- 값 대체 (value substitution) : 정합성을 깨지 않는 다른 값으로 대체 기본 값 혹은 잘 알려진 상수 (0), 초기값 설정해줌.
- 에러 로깅
- 에러 처리
- 올바른 수준의 추상화 단계에서 예외 처리, 엔드 유저에게 Traceback 노출 금지, 비어 있는 except 블록 지양, 원본 예외 포함
애플리케이션에서 디코딩한 데이터를 -> 외부 컴포넌트에 전달하는 객체 DataTransport가 있다고 가정해보자.
(기존)
class DataTransport:
"""An example of an object handling exceptions of different levels."""
_RETRY_BACKOFF: int = 5
_RETRY_TIMES: int = 3
def __init__(self, connector: Connector) -> None:
self._connector = connector
self.connection = None
def deliver_event(self, event: Event):
try:
self.connect()
data = event.decode()
self.send(data)
except ConnectionError as e:
logger.info("connection error detected: %s", e)
raise
except ValueError as e:
logger.error("%r contains incorrect data: %s", event, e)
raise
def connect(self):
for _ in range(self._RETRY_TIMES):
try:
self.connection = self._connector.connect()
except ConnectionError as e:
logger.info(
"%s: attempting new connection in %is",
e,
self._RETRY_BACKOFF,
)
time.sleep(self._RETRY_BACKOFF)
else:
return self.connection
raise ConnectionError(
f"Couldn't connect after {self._RETRY_TIMES} times"
)
def send(self, data: bytes):
return self.connection.send(data)
def deliver_event(self, event)에서 ValueError와 ConnectionError는 연관이 없다. 이는 책임을 분리할 필요가 있다.
(new)
def connect_with_retry(
connector: Connector, retry_n_times: int, retry_backoff: int = 5
):
"""Tries to establish the connection of <connector> retrying
<retry_n_times>, and waiting <retry_backoff> seconds between attempts.
If it can connect, returns the connection object.
If it's not possible to connect after the retries have been exhausted, raises ``ConnectionError``.
:param connector: An object with a ``.connect()`` method.
:param retry_n_times int: The number of times to try to call
``connector.connect()``.
:param retry_backoff int: The time lapse between retry calls.
"""
for _ in range(retry_n_times):
try:
return connector.connect()
except ConnectionError as e:
logger.info(
"%s: attempting new connection in %is", e, retry_backoff
)
time.sleep(retry_backoff)
exc = ConnectionError(f"Couldn't connect after {retry_n_times} times")
logger.exception(exc)
raise exc
class DataTransport:
"""An example of an object that separates the exception handling by
abstraction levels.
"""
_RETRY_BACKOFF: int = 5
_RETRY_TIMES: int = 3
def __init__(self, connector: Connector) -> None:
self._connector = connector
self.connection = None
def deliver_event(self, event: Event):
self.connection = connect_with_retry(
self._connector, self._RETRY_TIMES, self._RETRY_BACKOFF
)
self.send(event)
def send(self, event: Event):
try:
return self.connection.send(event.decode())
except ValueError as e:
logger.error("%r contains incorrect data: %s", event, e)
raise
Assertion
- 절대로 일어나야 하지 않아야 하는 상황에서 사용된다.
- 이는 더이상 처리가 불가능한 상황을 의미하므로 catch 후에 프로그램을 계속 실행하면 안된다.
- 항상 변하지 않는 고정되 조건에 대해 검증할 때 사용된다.
- Assertion Error은 예외를 처리하지 않는다.
- 참이어야 하는 Boolean 조건
# unpythonic
try :
assert condition.holds(), "Invalid condition"
except AssertError :
alternatie_procedure()
# pythonic
result = condition.holds()
assert result > 0, f"Error on holds: {result}"
Unpythonic 예제에서,
AssertionError 처리하는 것 외에도 assert 문장이 함수여서 문제이다.
3. 관심사의 분리
- 책임이 다르면 컴포넌트, 계층, 모듈로 분리되어야 한다.
- 파급 (ripple) 효과 : 어느 지점에서의 변화가 전체로 전파되는 것
- 파급 효과 ↓ -> 유지 보수성 ↑
- 응집력(cohesion)과 결합력 (coupling)
- 응집력 : 객체가 작고 잘 정의된 목적을 가져야 하며 가능한 작아야 한다!
- 응집력 ↑ -> 재사용성 ↑
- 결합력 : 두 개 이상의 객체가 서로 어떻게 의존하는지를 나타낸다!
- 결합력 ↓ -> 재사용성 ↑
- 응집력 : 객체가 작고 잘 정의된 목적을 가져야 하며 가능한 작아야 한다!
4. 개발 지침 약어
중복을 피할 것! : 코드에 있는 지식은 단 한번, 단 한 곳에 정의되어야 한다.
- DRY / do not repeat yourself
- OAOO / once and only once
단순할수록 좋다! : 과잉개발 멈춰!
- YAGNI / you ain't gonna need it
- KIS / Keep it simple
명시적인 것이 좋다!
- EAFP / easier to ask forgiveness than permission : 일단 코드 실행 후, 동작하지 않으면 대응! -> EAFP가 좀 더 pythonic 하다. 예외 처리가 필욯나 부분으로 바로 이동하므로 가독성이 높다!
- LBYL / look before you leap : 도약하기 전에 미리 살펴라!
# LBYL
if os.path.exists(filename) :
with open(filename) as f :
...
# EAFP
try :
with open(filename) as f:
...
except FileNotFoundError as e :
logger.error(e)
5. 상속
나쁜 경우
대부분의 Method를 재정의하거나 대체하는 경우
* Method 의 전체를 사용하는지 생각해볼 필요가 있음.
- Super Class (상위클래스)는 잘 정의된 인터페이스 대신 막연한 정의와 너무 많은 책임을 가졌다.
- inherited Class (하위 클래스)는 확장하려고 하는 상위 클래스의 적절한 세분화가 아니다.
좋은 경우
Public method와 attribute를 interface로 잘 정의한 컴포넌트
Component class의 기능을 상속받으면서,
추가 기능을 더하는 경우 or 특정 기능만 수정하는 경우
ex ) BaseHttpRequestHandler
except Exception : 모든 예외가 Exception을 상속받았으므로, 모든 에러 캐치가 가능하다.
예시
(bad)
import collections
from datetime import datetime
class TransactionalPolicy(collections.UserDict):
"""Example of an incorrect use of inheritance."""
def change_in_policy(self, customer_id, **new_policy_data):
self[customer_id].update(**new_policy_data)
- clear, copy, data, fromkeys, get, items, .., 매직 메서드 & 특수 메서드 등 불필요한 메서드가 많다!
- TransactionPolicy는 특정 도메인의 정보를 나타내는 것이므로 일부분에 사용되는 엔티티여야 한다!
(good)
from datetime import datetime
class TransactionalPolicy:
"""Example refactored to use composition."""
def __init__(self, policy_data, **extra_data):
self._data = {**policy_data, **extra_data}
def change_in_policy(self, customer_id, **new_policy_data):
self._data[customer_id].update(**new_policy_data)
def __getitem__(self, customer_id):
return self._data[customer_id]
def __len__(self):
return len(self._data)
- 컴포지션 사용 -> Trans.Policy 자체가 사전이 되는 것이 아닌 사전을 활용하기!
- Class의 일부만 사용하고 싶은 (상속이 싫은 경우)
- 상속의 경우, 부모가 바뀌면 자식도 업데이트가 필요하다.
파이썬의 다중 상속
- 메서드 결정 순서 : MRO 알고리즘 사용
- 호출된 자식 클래스 먼저 확인 -> 상속된 클래스를 나열한 순서대로 확인한다.
- [cls.__name__ for cls in ConcreteModuleA12.mro()]와 같은 방식으로 호출 결정 순서 확인 가능
- 예시는 "ConcreteModuleA12", "BaseModule1", "BaseModule2", "BaseModule", "object" 순서로 호출된다.
class BaseModule:
module_name = "top"
def __init__(self, module_name):
self.name = module_name
def __str__(self):
return f"{self.module_name}:{self.name}"
class BaseModule1(BaseModule):
module_name = "module-1"
class BaseModule2(BaseModule):
module_name = "module-2"
class BaseModule3(BaseModule):
module_name = "module-3"
class ConcreteModuleA12(BaseModule1, BaseModule2):
"""Extend 1 & 2
>>> str(ConcreteModuleA12('name'))
'module-1:name'
"""
class ConcreteModuleB23(BaseModule2, BaseModule3):
"""Extend 2 & 3
>>> str(ConcreteModuleB23("test"))
'module-2:test'
"""
- 믹스인 (Mixin)
- 코드 재사용을 위해 일반적인 행동을 캡슐화해놓은 부모 클래스
- 다른 클래스 & 믹스인 클래스를 다중 상속
- 믹스인 클래스의 메서드와 속성을 다른 클래스에서 활용한다.
class BaseTokenizer: # 문자열을 받아서 하이픈으로 구분된 값을 반환하는 파서
"""
>>> tk = BaseTokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0")
>>> list(tk)
['28a2320b', 'fd3f', '4627', '9792', 'a2b38e3c46b0']
"""
def __init__(self, str_token):
self.str_token = str_token
def __iter__(self):
yield from self.str_token.split("-")
class UpperIterableMixin: # 특정 클래스에 대해 기본 클래스를 변경하지 않고 대문자로 변환
def __iter__(self):
return map(str.upper, super().__iter__())
# mixin을 통해 UpperIterableMixin + BaseTokenizer 모두 수행
class Tokenizer(UpperIterableMixin, BaseTokenizer):
"""
>>> tk = Tokenizer("28a2320b-fd3f-4627-9792-a2b38e3c46b0")
>>> list(tk)
['28A2320B', 'FD3F', '4627', '9792', 'A2B38E3C46B0']
"""
# >>> [cls.__name__ for cls in Toknizer.mro()]
['Tokenizer', 'UpperIterableMixin', 'BaseTokenizer', 'object']
6. 함수 메서드의 인자
파이썬의 함수 인자 동작 방식
- Pass by value : 모든 인자가 값에 의해 전달된다.
- 변경 가능한 객체를 전달하고 함수 내부에서 값을 변경하면 결과에서 실제 값이 변경될 수 있다.
>>> def function(arg) :
... arg += "in function"
... print(arg)
...
변경 불가능한 경우 (ex. String)
>>> immutable = "hello"
>>> function(immutable)
helloin function
>>> immutable
'hello'
변경 가능한 경우 (ex. List)
Pass by value 시 주소 값이 전달된다!
>>> mutable = list("hello")
>>> mutable
['h', 'e', 'l', 'l', 'o']
>>> function(mutable)
['h', 'e', 'l', 'l', 'o', ' ', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n']
>>> mutable
['h', 'e', 'l', 'l', 'o', ' ', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n']
가변인자
인자의 개수가 변하는 인자
- 별표 (*, **) 를 통해 여러 인자를 packing 하여 함수에 전달하고 부분적으로 unpacking 할 수 있다.
>>> def f(first, second, third) :
... print(first)
... print(second)
... print(third)
...
>>> l = [1,2,3]
>>> f(*l)
1
2
3
>>> a,b,c = [1,2,3]
>>> a
1
>>> b
2
>>> c
3
>>> def show(e, rest) :
... print("요소 : {0} - 나머지 : {1}".format(e, rest))
...
>>> first, *rest = [1,2,3,4,5]
>>> show(first, rest)
요소 : 1 - 나머지 : [2, 3, 4, 5]
>>> first, *middle, last = range(6)
>>> first
0
>>> middle
[1,2,3,4]
>>> last
5
cf ) keyword argument (**) : 임의의 인자 허용 (dict.)
>>> def function(**kwargs) :
... print(kwargs)
...
>>> function(key="value")
{'key' : 'value'}
- 변수 unpacking을 사용하기에 "반복"은 좋다
"""Clean Code in Python - Chapter 3: General Traits of Good Code
> Packing / unpacking
"""
from dataclasses import dataclass
USERS = [(i, f"first_name_{i}", f"last_name_{i}") for i in range(1_000)]
@dataclass
class User:
user_id: int
first_name: str
last_name: str
# DB 레코드로부터 User를 생성하는 파이썬스럽지 않은 나쁜 코드
def bad_users_from_rows(dbrows) -> list:
"""A bad case (non-pythonic) of creating ``User``s from DB rows."""
return [User(row[0], row[1], row[2]) for row in dbrows]
# DB 레코드로부터 User 만들기
def users_from_rows(dbrows) -> list:
"""Create ``User``s from DB rows."""
return [
User(user_id, first_name, last_name)
for (user_id, first_name, last_name) in dbrows
]
# DB 레코드로부터 User 만들기 : positional parameter 넘기는 경우
def users_from_rows2(dbrows) -> list:
"""Create ``User``s from DB rows."""
return [User(*row) for row in dbrows]
기본함수
위치 전용, 키워드 전용 모두 가능하다!
>>> def my_function(x,y):
... print(f"{x=}, {y=}")
...
>>> my_function(1,2)
x=1, y=2
>>> my_function(x=1, y=2)
x=1, y=2
위치 전용 (positional-only) 인자
>>> def my_function2(x,y,/):
... print(f"{x=} {y=}")
...
>>> my_function2(1,2)
x=1 y=2
>>> my_function2(x=1, y=2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: my_function2() got some positional-only arguments passed as keyword arguments: 'x, y'
키워드 전용 (keyword-only) 인자
>>> def my_function3(x,y,*args, kw1, kw2=0) : # *args 이후 키워드 인자만 받겠다.
... print(f"{x=}, {y=}, {kw1=}, {kw2=}")
...
>>> my_function3(1,2,kw1=3, kw2=4)
x=1, y=2, kw1=3, kw2=4
>>> my_function3(1,2,kw1=3)
x=1, y=2, kw1=3, kw2=0
함수 인자의 개수
- 함수가 너무 많은 인자를 가진다면 -> 나쁜 디자인일 가능성이 높다.
- 1. 구체화 (reification)을 통해 전달하는 모든 인자를 포함하는 새로운 객체를 만든다.
- 2. 가변인자나 키워드 인자를 사용하여 동적 서명을 가진 함수를 만든다.
- 동적이므로 너무 남용하지 않도록 주의!
- 3. 한 가지 기능으로 하는 함수를 분리해라!
- 함수 인자와 결합력
- 함수 인자가 많을수록 호출하기 위한 정보 수집하기 어렵다.
- 함수의 추상화가 적으면 재사용성이 떨어지고 다른 함수에 의존적이게 된다.
- Code Smell이 아닌지 검토 필요!
- 많은 인자를 가진 함수의 서명 최소화
- 공통 객체에 파라미터 대부분이 포함되어 있는 경우 리팩토링 하기
7. 소프트웨어 디자인 우수 사례 결론
소프트웨어 독립성 (orthogonality)
- 모듈, 클래스 또는 함수를 변경하면 수정한 컴포넌트가 외부 세계에 영향을 미치지 않아야 한다.
- 어떤 객체의 메서드를 호출하는 것은 다른 관련 없는 개체의 내부 상태를 변경해서는 안 된다.
예시)
"""Clean Code in Python - Chapter 3: General traits of good code
> Orthogonality
"""
def calculate_price(base_price: float, tax: float, discount: float) -> float:
"""
>>> calculate_price(10, 0.2, 0.5)
6.0
>>> calculate_price(10, 0.2, 0)
12.0
"""
return (base_price * (1 + tax)) * (1 - discount)
def show_price(price: float) -> str:
"""
>>> show_price(1000)
'$ 1,000.00'
>>> show_price(1_250.75)
'$ 1,250.75'
"""
return "$ {0:,.2f}".format(price)
def str_final_price(
base_price: float, tax: float, discount: float, fmt_function=str
) -> str:
"""
>>> str_final_price(10, 0.2, 0.5)
'6.0'
>>> str_final_price(1000, 0.2, 0)
'1200.0'
>>> str_final_price(1000, 0.2, 0.1, fmt_function=show_price)
'$ 1,080.00'
"""
return fmt_function(calculate_price(base_price, tax, discount))
- calculate_price 함수와 show_price 함수는 독립이다.
- 두 함수에 대한 단위 테스트를 성공하면 전체 테스트가 필요하지 않다!
코드 구조
- __init__.py 파일을 가진 새 디렉토리를 만들면 파이썬 패키지가 만들어진다.
- __init__.py 에 다른 파일에 있던 모든 정읠ㄹ 가져옴으로써 호환성도 보장할 수 있다.
- 모듈의 __all__ 변수에 익스포트가 가능하도록 표시할 수도 있다.
- 모듈을 임포트할 때 구문을 분석하고 메모리에 로드할 객체가 줄어든다.
- 의존성이 줄었으므로 더 적은 모듈만 가져오면 된다.
# model structure
"""
model
|--- a.py
|--- b.py
┕--- c.py
"""
# model/__init__.py
from model.a import AModel
from model.b import BModel
from model.c import CModel
- 프로젝트에서 사용할 상수 값을 저장할 특정 파일을 만들고 임포트 하는 것도 좋다!
from src.constants import OUTPUT_PATH
Reference
https://github.com/PacktPublishing/Clean-Code-in-Python-Second-Edition/tree/main/book/src/ch03/src
https://github.com/dream2globe/CleanCodeInPython/blob/master/Ch3_FeaturesOfGoodCodes.md
'TIL > 클린코드' 카테고리의 다른 글
CH 6. 디스크립터로 더 멋진 객체 만들기 (0) | 2023.06.26 |
---|---|
CH 5. 데코레이터를 사용한 코드 개선 (0) | 2023.05.16 |
CH 4. SOLID 원칙 (0) | 2023.05.15 |
CH 2. 파이썬스러운 코드 (0) | 2023.05.02 |
CH1. 코드 포매팅과 도구 (0) | 2023.04.26 |