Patrick's 데이터 세상

Python Clean Code 정리 본문

Programming/Python

Python Clean Code 정리

patrick610 2023. 5. 25. 00:01
반응형
SMALL

 

 

 

 

 

 

 

마리아노 아나야의 파이썬 클린코드를 읽으며 정리해 두는 포스팅이다.

이미 알고 있는 내용이 많지만 유지보수, 애자일 개발을 좀 더 official 하게 skillup 하기 위해 정리해두려고 한다.

 

 

 

 

👉🏻 코드 포매팅

클린 코드라 함은 PEP-8 가이드라인을 지킨 띄어쓰기, 네이밍 컨벤션, 줄 길이 제한 등의 코딩 표준, 포매팅, 린팅 도구 등 레이아웃 설정과 같은 것 이상의 의미함.

클린 코드는 품질 좋은 소프트웨어를 개발하고, 견고하고 유지보수가 쉬운 시스템, 기술 부채를 피하는 것을 말한다.

 

 

프로젝트 코딩 스타일 가이드 준수

 

PEP-8 style

∙ 검색 효율성 : 코드에서 원하는 부분을 빠르게 검색.

                        ex) keyword argument에 값을 할당할 때는 띄어쓰기 x, 변수에 값을 할당할 때는 띄어쓰기 o

∙ 일관성 : 코드 레이아웃, 문서화, 이름 작명 규칙 등을 일정한 포맷을 갖게함.

∙ 더 나은 오류 처리 : try/except 블록 내부의 코드를 최소화하자. 실수로 예외를 숨기는 것을 방지.

∙ 코드 품질 : 정적 분석 도구를 사용하여 한 줄당 버그 수를 줄임.

 

자동 코딩 검증 도구를 사용하자!

 

문서화 Documentation

documenting code 하는 것과 adding comments 하는 것은 차이점이 있다.

docstringannotation을 사용한다.

 

코드 주석 code comments

좋은 코드는 코드 자체가 문서화되기 때문에 가능한 적은 주석을 갖아야 한다.

주석 작성 전에 새로운 함수를 추가하거나 보다 나은 변수명을 사용하는 것으로 개선할 수 있음.

주석 처리된 코드는 바로 삭제 - 지식의 오염과 혼란을 가져오기 때문.

 

문서화 Docstring

소스 코드에 포함된 문서.

모듈, 클래스, 메서드, 함수에 대해 문서를 제공하기 위함이다.

동작 방식과 입출력 정보 등을 다른 엔지니어에게 공유하기 위해서 작성.

 

IPython 대화형 인터프리터에서 dict.update??로 실행

일반 파이썬 쉘에서 my_function.__doc__, help(function)으로 사용

필자는 vscode에서 audoDocstring extension으로 사용.

"""_summary_

    Args:
        messages (_type_): _description_

    Returns:
        _type_: _description_
"""

 

어노테이션 Annotation

코드 사용자에게 함수 인자로 어떤 값이 와야 하는지 힌트를 주는 것.

타입 힌팅을 활성화.

from dataclasses import dataclass

@dataclass
class Point:
  lat: float
  long: float

def locate(latitude: float, longitude: float) -> Point:
	"""맵에서 좌표에 해당하는 객체를 검색"""
locate.__annotations__

{'latitude': float, 'longitude': float, 'return': __main__.Point}

어노테이션을 사용하면 __annotations__라는 이름과 값을 매핑한 사전 타입 값이 생긴다.

이 정보로 문서 생성, 유효성 검증, 타입 체크를 할 수 있다.

point = Point(0, 1, 1.1)
print(point)

Point(lat=0, long=1, sss=1.1)

별도 __ini__ 변수 선언, 할당하지 않아도 바로 인스턴스 속성으로 인식.

 

어노테이션으로 의존성을 주입시켜 주는 자동화 도구를 사용할 수 있다.

mypy : 데이터 타입 일관성 검사

pylint : 일반적 코드 검증

 

 

자동 포매팅

Pull Request 시 불필요한 논쟁을 줄이고 코드의 핵심에 집중할 수 있도록 설계된 코딩 컨벤션을 활용해야 한다.

 

flake8 : 자동 PEP-8 표준 준수. 

black : PEP-8보다 엄격한 하위 집합을 관리함으로써 항상 결정적인 형태의 포맷을 갖게 한다.

             ex) 각각의 요소가 별도 라인으로 정의된 경우 끝에 후행 쉼표가 없으면 쉼표를 추가한다.

 

 

 

 

👉🏻 Pythonic Code

파이썬 언어로 작업을 처리하는 고유한 관용구를 따르는 코드.

1. 코드가 더 적어지곡 이해하기 쉬워 더 나은 성능을 냄.

2. 동일한 패턴과 구조로 작성하여 실수를 줄이고 문제의 본질에 보다 집중이 가능.

 

 

인덱스 Index

다른 언어와 마찬가지로 인덱스로 접근 가능.

파이썬에서는 음수 인덱스로 끝에서부터 접근이 가능하다.

my_numbers[-3]

 

슬라이스 Slice

my_numbers[2:5]

처음이나 끝에서부터 동작

my_numbers[:3]
my_numbers[3:]

복사본 만들기

my_numbers[::]

범위 내에서 n개 씩 건너뛰기

my_numbers[1:7:2]

⭐️ 대괄호 슬라이스 방식은 실제로는 slice 함수에 파라미터를 전달하는 것과 같다.

interval = slice(None, 3)
my_numbers[interval] == my_numbers[:3]

True

 

자체 시퀀스 생성

myobject[key]와 같은 형태를 사용할 때 __getitem__이라는 매직 메서드가 호출되고 대괄호 안에 key값을 파라미터로 전달한다.

시퀀스는 __getitem_, __len__을 모두 구현한 객체이므로 반복이 가능.

리스트, 튜플과 문자열은 표준 라이브러리에 있는 시퀀스 객체의 예.

 

시퀀스나 이터러블 객체를 만들지 않고 키로  객체의 특정 요소를 가져오려면 from collections.abc import Sequence를 활용하여 클래스가 시퀀스임을 선언하기 위해 Sequence 인터페이스를 구현해야 한다.

 

컨텍스트 관리자 Context Manager

어떤 중요한 작업 전후에 실행하는 패턴에 잘 대응되는 기능.

리소스 관리와 관련하여 사용한다.

 

fd = open(filename)

try:
    process_file(fd)
finally:
    fd.close()

위와 같이 finally로 할당된 모든 리소스를 해제하는 방식으로 사용할 수 있다.

with open(filename) as fd:
    process_file(fd)

똑같은 기능을 with문(PEP-343)을 사용하여 컨텍스트 관리자로 진입할 수 있다.

예외가 발생한 경우에도 블록이 완료되면 파일이 자동으로 닫힌다.

컨텍스트 관리자는 __enter__와 __exit__ 두 개의 매직 메서드로 구성된다.

with문이 __enter__ 메서드를 호출하고 as 이후에 지정된 변수에 할당.

해당 블록에 대한 마지막 문장이 끝나면 컨텍스트가 종료되고  파이썬이 처음 호출한 원래 컨텍스틑 관리자 객체의 __exit__ 메서드를 호출함을 의미한다.

 

컨텍스트 관리자는 파일이나 커넥션 등의 리소스 관리에서 매우 유용하지만 관심사를 분리하고 독립적으로 유지되어야하는 코드를 분리하는 좋은 방법이다.

def stop_database():
    run("systemctl stop postgresql.service")

def start_database():
    run("systemctl start postgresql.service")

class DBHandler:
    def __enter__(self):
        stop_database()
        return self

    def __exit__(self, exc_type, ex_value, ex_traceback):
        start_database()

def db_backup():
    run("pg_dump database")

def main():
    with DBHandler():
        db_backup()

유지보수 작업과 상관 없이 백업을 실행하고 백업 후 with가 종료하면 __exit__로 다시 database를 실행한다.

 

contextlib.contextmanager 데코레이터를 적용하면 해당 함수의 코드를 컨텍스트 관리자로 변환하는 방법도 있다.

import contextlib

@contextlib.contextmanager
def db_handler():
    try:
        stop_database()
        yield
    finally:
        start_database()

with db_handler():
    db_backup()

제너레이터 함수를 정의하고 @contextlib, contextmanager 데코레이터를 적용했다. 이 함수는 yield 문을 사용했으므로 제너레이터 함수가 된다.

데코레이터를 적용하면 yield문 앞의 모든 명령문은 __ente__ 메서드의 일부처럼 취급된다.

또 다른 예로 contextlib.ContextDecorator를 사용할 수도 있다.

 

컨텍스트 관리자는 파이썬을 차별화하는 기능이고 가급적 사용하는 것이 이상적인 방법이다.

 

컴프리헨션 Comprehension

컴프리헨션을 사용하면 코드를 보다 간결하게 작성하고 가독성이 높아진다.

데이터에 대해 변환이 필요할 때는 for 루프를 사용하는 것이 낫지만 할당 표현식을 활용할 수도 있다.

 

# List append
numbers = []
for i in range(10):
    numbers.append(run_calculation(i))

# Comprehension
numbers = [run_calculation(i) for i in range(10)]

단일 파이썬 명령어를 호출하므로 일반적으로 더 나은 성능을 보인다.

 

할당 표현식 assignment expression

def collect_account_ids_from_arns(arns: Iterablelstr)) -> Set[str]:
    return {
        matched,groupdict()["account_id"]
        for arn in arns:
            if (matched := re.match(ARN_REGEX, arn)) is not None
    }

if (matched := re.match(ARN_REGEX, arn)) is not None 문장을 보면 문자열에 정규식을 적용한 결과가 None이 아닌 것들만 matched 변수로 저장되고, matched 변수를 다른 부분에서 사용할 수 있다.

간접 참조가 적고, 동일 스코프 내에서 값이 효율적으로 수집된다.

 

 

프로퍼티, 속성(Attribute)과 객체 메서드의 다른 타입들

public, private, protected와 같은 접근 제어자를 가지는 다른 언어들과 달리 파이썬 객체의 모든 속성과 함수는 public이다.

따라서 호출자가 객체의 속성을 호출하지 못하도록 할 방법이 없다.

밑줄로 시작하는 속성은 private 속성을 의미하고 외부에서 호출되지 않기를 기대한다는 의미지만 이것이 호출을 금지시켜 주는 것은 아니다.

 

밑줄

class Connector:
    def __init__(self, source):
        self.source = source
        self._timeout = 60

conn = Connector("postgresql://localhost")
conn.source

'postgresql://localhost'

conn._timeout

60

conn.__dict__

{'source' : 'postgresql://localhost', '_timeout' : 60}

위 코드에서 source는 public이고 _timeout은 private이다.

두 속성 모두 접근이 가능하지만 _timeout은 connector 자체 내부에서만 사용하고 바깥에서는 호출하지 않을 것으로 고려된다.

따라서 외부 인터페이스를 고려하지 않고 안전하게 리팩토링할 수 있다.

밑줄로 시작하는 속성은 private처럼 취급되어야 하고 외부에서 호출하면 안 된다.

 

이름 맹글링 name mangling

변수의 이름을 "_<class_name>__<attribute-name>" 형태로 변경하는 것.

ex) _Connector__timeout

따라서 해당 속성을 외부에서 호출하면 존재하지 않는다.

 

프로퍼티

파이썬에서는 프로퍼티를 사용하여 유효성 검사 메서드 setter, getter 메서드를 더 간결하게 캡슐화할 수 있다.

class Coordinate:
    def __init__(self, lat: float, long: float) -> None:
        self._latitude = self_longitude = None
        self.latitude = lat
        self.longitude = long
	
    @property
    def latitude(self) -> float:
        return self._latitude
    
    @latitude.setter
    def latitude(self, lat_value: float) -> None:
        if lat_value not in range(-90, 90 + 1):
            raise ValueError(f"유효하지 않은 위도 값: {lat_value}")
        self._latitude = lat_value

    @property
    def longitude(self) -> float:
        return self._longitude

    @longitude.setter
    def longitude(self, long_value: float) -> None:
        if long_value not in range(-180, 180 + 1):
            raise ValueError(f"유효하지 않은 경도 값: {long_value}")
        self._longitude = long_value

위 코드에서 프로퍼티는 latitude와 longitude를 정의하기 위해 사용했다.

위와 같이 사용함으로써 private 변수에  저장된 값을 반환하는 별도의 속성을 만든다.

대부분의 경우 일반 속성을 사용하는 것만으로 충분하지만 속성 값을 가져오거나 수정할 때 특별한 로직이 필요한 경우에 프로퍼티를 사용한다.

 

보다 간결한 구문으로 클래스 만들기

파이썬에는 객체의 값을 초기화하는 일반적인 모든 프로젝트에서 공통적으로 반복해서 사용되는 보일러플레이트boilerplate 코드가 있다.

일반적으로 __init__ 메서드에 객체에서 필요ㅛ한 모든 속성을 파라미터로 받은 다음 내부 변수에 할당하는데 dataclass 모듈을 사용하면 코드를 단순화할 수 있다.

@dataclass 데코레이터를 제공하여 클래스에 적용하면 모든 클래스의 속성에 대해 마치 __init__ 메서드에서 정의의한 것처럼 인스턴스 속성으로 처리한다.

__init__ 메서드를 자동으로 생성하므로 __init__ 메서드를 구현할 필요가 없다.

 

field

dataclass 모듈은 field 객체를 제공한다.

field 객체는 해당 속성에 특별한 특징이 있음을 표시한다.

mylist = [] 처럼 __init__ 메서드에서는 비어있는 리스트를 할당할 수 없는데 filed 객체를 사용하면 default_factory 파라미터에 list 객체를 전달하여 초기값을 지정할 수 있다.

 

R-Trie 구조 구현

from typing import List
from dataclasses import dataclass, field


R = 26

@dataclass
class RTrieNode:
    size = R
    value: int
    next_: List["RTrieNode"] = field(default_factory=lambda: [None] * R)

    def __post_init__(self):
        if len(self.next_) != self.size:
            raise ValueError(f"리스트(next_)의 길이가 유효하지 않음")

__post_init__으로 __init__ 메서드가 없을 때 초기화 직후 유효성 검사하거나 값을 초기화하기 전에 어떤 계산을 하거나 로직을 추가할 수 있다.

default_factory 파라미터에 list 객체를 전달하여 초기값을 지정한다.

 

⭐️ 이터러블 객체

파이썬에는 기본적으로 반복 가능한 객체가 있다. 리스트, 튜플, 세트, 사전

이러한 내장 반복형 객체만 for 루프에서 사용 가능한 것이 아니고 나만의 반복 로직을 가진 이터러블을 만들 수 있다.

이터러블은 __iter__ 매직 메서드를 구현한 객체, 이터레이터는 __next__ 매직 메서드를 구현한 객체를 말한다.

파이썬 반복은 이터러블 프로토콜이라는 자체 프로토콜을 사용해 동작한다.

for e in myobject: 형태로 객체를 반복할 수 있는지 확인하기 위해 파이썬은 고수준에서 아래 항목을 차례로 검사한다.

👉🏻 객체가 __next__나 __iter__ 이터레이터 메서드 중 하나를 포함하는지 여부

👉🏻 객체가 시퀀스이고 __len__과 __getitem__를 모두 가졌는지 여부

 

이터러블 객체 만들기

객체를 반복하려고 하면 파이썬은 해당 객체의 iter() 함수를 호출한다.

다음은 일정 기간의 날짜를 하루 간격으로 반복하는 객체의 코드이다.

from datetime import timedelta

class DateRangeIterable:
    """ 자체 이터레이터 메서드를 가지고 있는 이터러블"""

    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._present_day = start_date

    def __iter__(self):
        return self

    def __next__(self):
        if self._present_day >= self.end_date:
            raise StopIteration()
        today = self._present_day
        self._present_day += timedelta(days=1)
        return today
from datetime import date

for day in DateRangeIterable(date(2022, 1, 1), date(2022, 1, 5)):
    print(day)

...
...

2022-01-01
2022-01-02
2022-01-03
2022-01-04

이 객체는 한 쌍의 날짜를 통해 생성되며 위와 같이 해당 기간의 날짜를 반복하면서 하루 간격으로 날짜를 표시한다.

이  함수는 __iter__ 매직 메서드를 호출할 것이다.

__iter__ 메서드는 self를 반환하고 있으므로 객체 자신이 이터러블임으르 나타내고 있다. 따라서 루프의 각 단계에서마다 자신의 next() 함수를 호출한다. next() 함수는 다시 __next__ 메서드에게 위임한다. 이 메서드는 요소를 어떻게 생산하고 하나씩 반환할 것인지 결정한다.

for가 작동하는 원리는 StopIteration 예외가 발생할 때까지 next()를 호출하는 것과 같다.

더 이상 생산할 것이 없을 경우 파이썬에게 StopIteration 예외를 발생시켜 알려줘야 한다.

 

class DateRangeContainerIterable:
    def _init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date

    def __iter__(self):
        current_day = self.start_date
        while current_day < self.end_date:
            yield current_day
            current_day += timedelta(days=1)
>>> r1 = DateRangeContainerIterable(date(2022,1, 1), date(2022, 1, 5))
>>> ", ".join(map(str, r1))
'2022-01-01, 2022-01-02, 2022-01-03, 2022-01-04'
>>> max(r1)
datetime.date(2022, 1, 4)

StopIteration을 수정하는 방법 중 위 코드와 같이 매번 새로운 DataRangeIterable 인스턴스를 생성하는 방법이 있다.

각각의 for 루프는 __iter__를 호출하고, __iter__는 다시 제너레이터를 생성한다.

이러한 형태의 객체를 컨테이너 이터러블 container iterable이라 한다.

 

시퀀스 만들기

객체에 __iter__() 메서드를 정의하지 않았지만 반복이 필요한 경우가 있다.

__iter_가 정의됭어 있지 않으면 __getiterm__을 찾고 없으면 TypeError를 발생시킨다.

시퀀스는 __len__과 __getiterm__을 구현하고 첫 번쨎 인덱스 0부터 시작하여 포함된 요소를 한 번에 하나씩 차례로 가져올 수 있어야 한다.

이전까지의 예제는 메모리를 적게 사용한다는 장점이 있지만 n번째 요소를 얻고 싶으면 도달할 때까지 n번 반복한다는 단점이 있다.

이 문제는 컴퓨터 과학에서 발생하는 전형적인 메모리와ㅘ CPU 사이의 트레이드오프이다.

이터러블을 사용하면 메모리를 적게 사용하지만 n번째 요소를 얻기 위한 시간복잡도는 O(n)이다.

하지만 시퀀스로 구현하면 모든 데이터를 한 번에 보관해야 하므로 더 많은 메모리가 사용되지만 특정 요소를 가져오기 위한 인덱싱의 시간복잡도는 O(1)로 상수에 가능하다.

class DateRangeSequence:
    def __init__(self, start_date, end_date):
        self.start_date = start_date
        self.end_date = end_date
        self._range = self._create_range()
    
    def _create_range(self):
        days = []
        current_day = self.start_date
        while current_day < self.end_date:
            days.append(current_day)
            current_day += timedelta(days=1)
        return days

    def ___getitem__(self, day_no):
        return self._range[day_no]

    def __len__(self):
        return len(self._range)
>>> s1 = DateRangeSequence(date(2022, 1, 1), date(2022, 1, 5))
>>> for day in st:
print(day)

2022-01-01
2022-01-02
2022-01-03
2022-01-04

>>> s1[0]
datetime.date(2022, 1, 1)
>>> s1[3]
datetime.date(2022, 1, 4)
>>> s1[-1]
datetime.date(2022, 1, 4)

 

⭐️ 컨테이너 객체

__contains__ 메서드를 구현한ㄴ 객체로 일반적으로 Boolean 값을 반환한다.

파이썬에서는 element in container처럼 in 키워드가 발견될 때 호출된다.

위 코드를 파이썬은 container.__contains__(element)로 해석한다.

def mark_coordinate(grid, coord):
    if 0 <= coord.x < grid.width and 0 <= coord.y < grid.height:
        grid[coord] = MARKED

위 if문처럼 코드의 의도가 무엇인지 이해하기 어렵고 난해한 상황에서 Grid 객체 스스로 특정 좌표가 자신 영역 안에 포함되어있는지 판단하게 하여 객체 지향 설계를 한다.

 

컴포지션을 사용하여 다른 클래스에 책임을 분배하고, 컨테이너 매직 메서드 __contains__를 사용하는 모습

class Boundaries:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    def __contains__(self, coord):
        x, y = coord
        return 0 <= x < self.width and 0 <= y < self, height
        
class Grid:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.limits = Boundaries(width, height)

    def __contains__(self, coord):
        return coord in self.limits
>>> grid = Grid(5, 10)
>>> (4, 2) in grid
True
>>> (3, 20) in grid
False

위 디자인을 사용하면 Grid에게 어떤 좌표가 포함되어 있는지 직접 물어볼 수 있으며, Grid는 내부적으로 협력 객체(Boudaries)에게 질의를 전달하여 도움을 받을 수 있다.

두 객체는 매우 응집력이 있고, 최소한 로직을 가지고 있어 메서드가 간결하고 그 자체로 자명하다.

 

👉🏻 매직 메서드 요약

사용 예 매직 메서드 비고
obj[key]
obj[i:j]
obj[i:j:k]
__getitem__(key) 첨자형(subscriptable) 객체
with obj: ... __enter__/__exit__ 컨텍스트 관리자
for i in obj: ... __iter__/__next__
__len__/__getitem__
이터러블 객체
시퀀스
obj.<attribute> __getattr__ 동적 속성 조회
obj(*args, **kwargs) __call__(*args, **kwargs) 호출형(callable) 객체

위와 같은 매직 메서드를 올바르게 구현하는 가장 좋은 방법은 collection.abc 모듈에서 정의된 추상 클래스를 상속하는 것이다.

 

⭐️ 파이썬에서 유의할 점

 

변경 가능한 파라미터의 기본 값 : 변경 가능한 객체를 함수의 기본 인자로 사용하면 안된다.

내장 타입 확장 : 리스트, 문자열, 사전과 같은 내장 타입을 확장하는 올바른 방법은 collections 모듈을 사용하는 것.

Pythonic하게 코드를 작성하는 가장 좋은 방법은 관용구를 따르는 것뿐만이 아니라 파이썬이 제공하는 모든 기능을 최대한 활용하는 것.

 

 


 

👉🏻 SOLID 원칙

S : 단일 책임 원칙(Single responsibility principle)

O : 개방/폐쇄의 원칙(Open/closed principle)

L : 리스코프(Liskov) 치환 원칙(Liskov's substitution Principle)

I : 인터페이스 분리 원칙(Interface segregation principle)

D : 의존성 역전 원칙(Dependency inversion principle)

 

 

단일 책임 원칙 Single responsibility principle

소프트웨어 컴포넌트(클래스)가 단 하나의 책임을 져야한다는 원칙.

하나의 구체적인 일을 담당하는 것을 의미하며 따라서 변경이 필요한 이유도 단 하나만 있어야 한다.

필요한 일 이상의 것을 하거나 너무 많은 것을 아는 객체를 신(god)객체라 부르는데, 서로 다른 행동을 그룹화한 것으로 유지보수가 어렵다.

따라서 클래스는 작을수록 좋다.

너무 많은 책임을 지닌 클래스를 책임을 분산하여 솔루션을 관리하기 쉽도록 모든 메서드를 다른 클래스로 분리하여 각 클래스마다 단일 책임을 갖게 한다.

 

책임 분산 예시

 

개방/폐쇄 원칙 OCP

모듈이 개방되어 있으면서도 폐쇄되어야 한다는 원칙.

클래스를 디자인할 때는 유지보수가 쉽도록 로직을 캡슐화하여 확장에는 개방되고 수정에는 폐쇄되도록 해야 한다.

다형성을 따르는 형태의 계약을 만들고 모델을쉽게 확장할 수 있는 일반적인 구조로 디자인하는 것.

OCP를 다르지 않으면 파급 효과가 생기거나 작은 변경이 코드 전체에 영향을 미치거나 다른 부분을 손상시키게 된다.

 

# OCP가 적용된 예제
class Message:
    """Message 추상 클래스"""
    def __init__(self, data):
        self.data = data
        
    @staticmethod
    def is_collect_grade_message(data: dict):
        return False
 
class FirstGradeMessage(Message):
    """FirstGrade에 대한 메세지 처리 클래스"""
    @staticmethod
    def is_collect_grade_message(data: dict):
        return data['grade'] == 1
    
class SecondGradeMessage(Message):
    """SecondGrade에 대한 메세지 처리 클래스"""
    @staticmethod
    def is_collect_grade_message(data: dict):
        return data['grade'] == 2
    
class ThirdGradeMessage(Message):
    """ThirdGrade에 대한 메세지 처리 클래스"""
    @staticmethod
    def is_collect_grade_message(data: dict):
        return data['grade'] == 3
    
class DefaultGradeMessage(Message):
    """DefaultGrade에 대한 메세지 처리 클래스"""
    
class GradeMessageClassification():
    """Grade에 따른 메세지 분류 클래스"""
    def __init__(self, data):
        self.data = data
        
    def classification(self):
        for grade_message_cls in Message.__subclasses__():
            try:
                if grade_message_cls.is_collect_grade_message(self.data):
                    return grade_message_cls(self.data)
            except KeyError:
                continue
                
            return DefaultGradeMessage(self.data)

새로운 요구사항에 대해서 기존의 로직을 수정해야 하는일이 없게 되므로 시스템 안정성 뿐만 아니라 유지보수 측면에서도 좋은 이점을 가져올 수 있다.

 

리스코프 치환 원칙 LSP

설계의 안정성을 높이기 위해 객체가 가져야하는 일련의 특성.

클라이언트는 부모 타입 대신에 어떠한 하위 타입을 사용해도 정상적으로 동작해야 한다.

 

클래스 게층 구조

좋은 클래스는 명확하고 간결한 인터페이스를 가지고 있으며, 하위 클래스가 해당 인터페이스를 따르는 한 프로그램은 정상적으로 동작한다.

 

도구를 사용해 LSP 문제 검사하기

LSP 문제를 mypy, pylint 등 도구를 사용해 쉽게 검출할 수 있다.

class Event:
    ...
    def meets_condition(self, event_data: dict) -> bool:
        return false

class LoginEvent(Event):
    def meets_condition(self, event_data: list) -> bool:
        return bool(event_data)

이 파일에 대해 mypy를 실행하면 오류가 발생한다.

위 코드는 LSP를 위반했다. 파생 클래스가 부모 클래스에서 정의한 파라미터와 다른 타입을 사용했기 때문이다.

LSP 원칙을 따랐다면 호출자는 아무런 차이를 느끼지 않고 투명하게 Event, LoginEvent를 사용해야 한다.

 

반환 값을 부울 값이 아닌 다른 값으로 변경해도 동일한 오류가 발생한다.

이 코드의 클라이언트가 부울 값을 사용할 것으로 기대한다는 것.

파생 클래스 중 하나가 이 반환 타입을 변경하면 계약을 위반하게 되며 프로그램이 정상적으로 동작할 것이라고 기대할 수 없다.

 

LSP는 객제지향 소프트웨어 설계의 핵심이 되는 다형성을 강조하기 때문에 좋은 디자인의 기초가 된다.

인터페이스의 메서드가 올바른 계층구조를 갖도록 하여 상속된 클래스가 부모 클래스와 다형성을 유지하도록 하는 것이다.

 

 

인터페이스 분리 원칙 ISP

이미 반복적으로 재검토했던 "작은 인터페이스"에 대한 가이드라인을 제공한다.

객체 지향적 용어로 인터페이스는 객체가 노출하는 메서드의 집합이다.

즉, 객체가 수신하거나 해석할 수 있는 모든 메시지가 인터페이스를 구성하며, 클라이언트는 이것들을 호출할 수 있다.

인터페이스는 클래스의 정의와 구현을 분리한다.

 

파이썬에서는 덕 타이핑 원칙을 따르기 때문에 인터페이스는 메서드의 형태를 보고 암시적으로 정의된다.

덕 타이핑은 모든 객체가 자신이 가지고 있는 메서드와 자신이 할 수 있는 일에 의해서 표현된다는 점에서 출발한다.

 

추상 기본 클래스(abstract base class)는 파생 클래스가 반드시 구현해야 하는 것을 명시적으로 가리키기 위한 유용하고 강력한 도구이다.

@abtractmethod 데코레이터를 사용하고 기본 클래스에서 @abstractmethod로 마킹한 메서드는 반드시 파생 클래스에서 모두 구현을 해야만 인스턴스화가 가능하다.

abc 모듈에 가상 서브클래스(virtual subclass) 

 

 

 

 

 

 


👉🏻 참고

파이썬 클린코드 2nd Edition - 마리아노 아나야 지음, 김창수 옮김

 

 

 

 

 

 

 

 

반응형
LIST
Comments