Skip to content

Latest commit

 

History

History
467 lines (380 loc) · 17.5 KB

220327.md

File metadata and controls

467 lines (380 loc) · 17.5 KB

함수와 메소드의 인자

파이썬은 여러 가지 방법으로 인자를 받도록 함수를 정의할 수 있으며 사용자도 여러 가지 방법으로 인자를 제공할 수 있다.

파이썬의 함수 인자 동작방식

인자는 함수에 어떻게 복사되는가

  • 모든 인자가 값에 의해 전달 된다.
  • 변형 가능한 객체를 전달하고 함수에서 값을 변경하면 함수 반환 시 실제 값이 변경되는 부작용이 생길 수 있다.
def function(arg):
    arg += " in function"
    print(arg)

immutable = "hello"
function(immutable)

immutable

'hello in function'
'hello'

mutable = list("hello")
function(mutable)

mutable

['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n']
['h', 'e', 'l', 'l', 'o', ' ', 'i', 'n', ' ', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n']

결과 설명

  • string 객체는 불변형 타입이므로 function 내부에서 새로운 객체를 만들어서 지역 변수인 arg에 다시 할당하여 문제가 되지 않는다.
  • list 객체 같은 변형 객체를 전달하면 연산자는 원래 리스트에 대한 참조를 보유하고 있는 변수를 통해 값을 수정하므로 실제 값을 변경할 수 있다.

이렇게 변경하는 방식은 가급적 피하는 것이 좋다.

가변인자

가변 인자를 사용하려면 해당 인자를 패킹할 변수의 이름 앞에 *를 사용한다.

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]
print(a, b, c)

1 2 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]

*rest, last = range(6)
show(last, rest)

요소: 5 - 나머지: [0, 1, 2, 3, 4]

first, *middle, last = range(6)
print(first, middle, last, sep=' - ')

0 - [1, 2, 3, 4] - 5

first, last, *empty = (1, 2)
print(first, last, empty, sep=' - ')

1 - 2 - []

변수 언패킹의 가장 좋은 사용 예는 반복

  • 일련의 요소를 반복해야 하고 각 요소가 차례로 있다면 각 요소를 반복할 때 언패킹하는 것이 좋다.
USER = [(i, f"first_name_{i} ", "last_name_{i} ") for i in range(1_000)]

class User:
    def __init__(self, user_id, first_name, last_name):
        self.user_id = user_id
        self.first_name = first_name
        self.last_name = last_name

    def bad_users_from_rows(dbrows) -> list:
        """DB 레코드에서 사용자를 생성하는 파이썬스럽지 않은 잘못된 사용 예"""
        return [User(row[0], row[1], row[2]) for row in dbrows]

    def users_from_rows(dbrows) -> list:
        """DB 레코드에서 사용자 생성"""
        return [
            User(user_id, first_name, last_name)
            for (user_id, first_name, last_name) in dbrows
        ]

**를 키워드 인자에 사용할 수 있다.

딕셔너리에 **를 사용하여 함수에 전달하면 파라미터의 이름으로 키를 사용하고, 파라미터의 값으로 딕셔너리의 값을 사용한다.

function(**{"key": "value"})
function(key="value")

둘은 서로 동일한 의미를 갖는다.

반대로 **로 시작하는 파라미터를 함수에 사용하면 키워드 제공 인자들이 사전으로 패킹된다.

def function(**kwargs):
    print(kwargs)

function(key="value")

{'key': 'value'}

함수 인자의 개수

너무 많은 인자를 사용하는 함수나 메소드의 해결할 방법

  • 구체화
    • 전달하는 모든 인자를 포함하는 새로운 객체 생성
  • 가변 인자나 키워드 인자 같은 특정 기능을 사용하여 동적 서명을 가진 함수를 만든다.
    • 파이썬스럽지만 남용은 금물
    • 매우 동적이어서 유지보수가 어려움

함수 인자와 결합력

  • 함수 서명의 인수가 많을수록 호출자 함수와 밀접하게 결합될 가능성이 커진다.
  • 함수가 보다 일반적인 인터페이스를 제공하고 더 높은 수준의 추상화로 작업할 수 있다면 코드 재사용성이 높아진다.
  • 클래스의 __init__ 메소드를 포함한 모든 종류의 함수와 객체 메소드에 적용된다.

많은 인자를 취하는 작은 함수의 서명

많은 파라미터를 사용하는 함수의 리팩토링 방법

  • 공통 객체에 파라미터 대부분이 포함된 경우
    • 가장 쉬움
    • 아래의 경우 그냥 request를 파라미터로 전달하면 해결
    • 함수는 전달받은 객체를 변경해서는 안 된다.
track_request(request.headers, request.ip_addr, request.request_id)
  • 파라미터 그룹핑
    • 컨테이너처럼 하나의 객체에 파라미터를 담는다.
    • 추상화
  • 함수의 서명을 변경하여 다양한 인자를 허용
    • 최후의 수단
    • 인자가 많은데 *args 또느 **kwargs를 사용하면 더 이해하기 어려울 수도 있다.

*args 또느 **kwargs 사용 시

  • 장점
    • 함수가 융통성 있고 적응력이 좋다.
  • 단점
    • 서명을 잃어버린다.
    • 가독성을 거의 상실한다.
    • 매우 좋은 docstring을 만들어야 정확한 동작을 알 수 있다.

소프트웨어 디자인 우수 사례 결론

좋은 소프트웨어 디자인

  • 소프트웨어 엔지니어링의 우수 사례를 따름
  • 언어의 기능이 제공하는 대부분의 장점 활용

소프트웨어의 독립성

  • 모듈, 클래스 또는 함수를 변경하면 수정한 컴포넌트가 외부 세계에 영향을 미치지 않아야 한다.
  • 불가능하다고 해도 가능한 한 영향을 최소화하려고 시도해야 한다.
  • 소프트웨어의 런타임 구조 측면에서 직교성은 변경을 내부 문제로 만드는 것이라고 할 수 있다.

예시

def calculate_price(base_price: float, tax: float, discount: float) -> float:
    return (base_price * (1 + tax)) * (1 - discount)

def show_price(price: float) -> str:
    return "$ {0:,.2f}".format(price)

def str_final_price(base_price: float, tax: float, discount: float, fmt_function=str) -> str:
    return fmt_function(calculate_price(base_price, tax, discount))
  • 위쪽의 두 함수는 독립성을 가져 하나를 변경해도 다른 하나는 변경되지 않는다.
  • 마지막 함수는 아무것도 전달하지 않으면 문자열 변환을 기본 표현 함수로 사용하고 사용자 정의 함수를 전달하면 해당 함수를 사용해 문자열을 포맷한다.
    • 그러나 show_price의 변경 사항은 calculate_price에 영향을 미치지 않는다.
  • 어느 함수를 변경해도 나머지 함수가 그대로라서 편하게 변경할 수 있다.
str_final_price(10, 0.2, 0.5)

'6.0'

str_final_price(1000, 0.2, 0.1, fmt_function=show_price)

'$ 1,080.00'

코드의 두 부분이 독립적이라는 것은 다른 부분에 영향을 주지 않고 변경할 수 있다는 뜻

  • 이는 변경된 부분의 단위 테스트가 나머지 단위 테스트와도 독립적이라는 뜻

코드 구조

  • 코드를 구조화하는 방법은 팀의 작업 효율성과 유지 보수성에 영향을 미친다.
  • 여러 정의가 들어있는 큰 파일을 만드는 것은 좋지 않다.
  • 유사한 컴포넌트끼리 정리하여 구조화해야 한다.

대용량 파일을 작은 파일로 나누기

  • 코드의 여러 부분이 해당 파일의 정의에 종속되어 있어도 전체적인 호환성을 유지하면서 패키지로 나눌 수 있다.
  • __init__.py 파일을 가진 새 디렉토리를 만드는 것으로 해결 가능하다.
  • 이러면 파이썬 패키지가 만들어지고, 이 파일과 함께 특정 정의를 포함하는 여러 파일을 생성한다.
  • 이때는 각각의 기준에 맞춰 보다 적은 클래스와 함수를 갖게 된다.
  • 그 다음 __init__.py 파일에 다른 파일에 있던 모든 정의를 가져옴으로써 호환성도 보장할 수 있다.
  • 뿐만 아니라 이러한 정의는 모듈의 __all__ 변수에 익스포트 가능하도록 표시할 수도 있다.

위 방법의 장점

  • 각 파일을 탐색하고 검색이 쉽다.
  • 모듈을 임포트할 때 구문을 분석하고 메모리에 로드할 객체가 줄어든다.
  • 의존성이 줄었기 때문에 더 적은 모듈만 가져오면 된다.
  • 프로젝트를 위한 컨벤션을 갖는데 도움이 된다.
from myproject.constants import CONNECTION_TIMEOUT

이렇게 프로젝트에서 사용할 상수 값을 저장할 특정한 파일을 만들고 임포트하면 끝이다.

정보를 중앙화하는 것의 장점

  • 코드 재사용 쉬움
  • 실수로 인한 중복 회피

Chapter 4

SOLID 원칙

  • S: 단일 책임 원칙
  • O: 개방/폐쇄의 원칙
  • L: 리스코프 치환 원칙
  • I: 인터페이스 분리 원칙
  • D: 의존성 역전 원칙

단일 책임 원칙 (SRP)

  • 소프트웨어 컴포넌트가 단 하나의 책임을 져야한다는 원칙
  • 클래스가 유일한 책임이 있다는 것은 하나의 구체적인 일을 담당한다는 것을 의미

너무 많은 책임을 가진 클래스

SRP를 준수하지 않은 디자인

class SystmemMonitor:
    def load_activity(self):
        """소스에서 처리할 이벤트를 가져오기"""
    
    def identify_events(self):
        """가져온 데이터를 파싱하여 도메인 객체 이벤트로 변환"""

    def stream_events(self):
        """파싱한 이벤트를 외부 에이전트로 전송"""

문제점

  • 독립적인 동작을 하는 메소드를 하나의 인터페이스에 정의했다는 것

이 예제에서 각 메소드는 클래스의 책임을 대표하고, 각각의 책임마다 수정 사유가 발생한다.

외부 요소에 의한 영향을 최소화 하고 싶을 때는 보다 작고 응집력 있는 추상화를 하면 된다.

책임 분산

모든 메소드를 다른 클래스로 분리하여 각 클래스마다 단일 책임을 갖게한다.

솔루션 수정

  • 각자의 책임을 가진 여러 객체로 분할
  • 객체들과 협력하여 동일한 기능 수행
  • 각각의 객체들은 특정한 기능을 캡슐화

결과
AlertSystem - run()

  • ActivityReader - load()
  • SystemMonitor - identify_event()
  • Output - stream()

각 클래스가 딱 하나의 메소드를 가져야 한다는 것은 아니다.
처리해야 할 로직이 같은 경우 여러 메소드를 가질 수 있다.

개방/폐쇄 원칙

  • 모듈이 개방되어 있으면서도 폐쇄되어야 한다는 원칙
  • 클래스를 디자인할 때는 유지보수가 쉽도록 로직을 캡슐화하여 확장에는 개방, 수정에는 폐쇄되도록 한다.

개방/폐쇄 원칙을 따르지 않을 경우 유지보수의 어려움

예제

  • 다른 시스템에서 발생하는 이벤트를 분류하는 기능을 가지고 있음
  • 각 컴포넌트는 수집한 데이터를 기반으로 어떤 타입의 이벤트인지 정확히 분류 해야 함
  • 단순함을 위해 데이터는 딕셔너리 형태로 저장되어 있고 로그나 쿼리 등의 방법으로 이미 데이터를 수집했다고 가정
  • 이 데이터를 기반으로 고유한 계층구조를 가진 다른 이벤트로 분류
class Event:
    def __init__(self, raw_data):
        self.raw_data = raw_data

class UnknownEvent(Event):
    """데이터만으로 식별할 수 없는 이벤트"""

class LoginEvent(Event):
    """로그인 사용자에 의한 이벤트"""

class LogoutEvent(Event):
    """로그아웃 사용자에 의한 이벤트"""

class SystemMonitor:
    """시스템에서 발생한 이벤트 분류"""

    def __init__(self, event_data):
        self.event_data = event_data

    def identify_event(self):
        if (
            self.event_data["before"]["session"] == 0
            and self.event_data["after"]["session"] == 1
        ):
            return LoginEvent(self.event_data)
        elif (
            self.event_data["before"]["session"] == 0
            and self.event_data["after"]["session"] == 1
        ):
            return LogoutEvent(self.event_data)

        return UnknownEvent(self.event_data)

다음과 같이 동작

l1 = SystemMonitor({"before": {"session": 0}, "after": {"session": 1}})
print(l1.identify_event().__class__.__name__)

LoginEvent

이 디자인의 문제점

  • 이벤트 유형을 결정하는 논리가 일체형으로 중앙 집중화 됨
    • 지원하려는 이벤트가 늘어날수록 메소드도 커져 결국 매우 큰 메소드가 될 수 있다.
  • 수정을 위해 닫히지 않음
    • 새로운 유형의 이벤트를 시스템에 추가할 때마다 메소드를 수정해야 함

확장성을 가진 이벤트 시스템으로 리팩토링

위 예제의 문제점

  • SystemMonitor 클래스가 분류하려는 구체 클래스와 직접 상호 작용한다는 점
  • 추상화 필요

대안

  • SystemMonitor 클래스를 추상적인 이벤트와 협력하도록 변경
  • 이벤트에 대응하는 개별 로직은 각 이벤트 클래스에 위임
  • 각각의 이벤트에 다형성을 가진 새로운 메소드 추가
    • 전달되는 데이터가 해당 클래스의 타입과 일치하는지 판단하는 역할
  • 기존 분류 로직을 수정하여 이 메소드를 사용해 전체 이벤트를 돌면서 검사하도록 한다.
class Event:
    def __init__(self, raw_data):
        self.raw_data = raw_data

    @staticmethod
    def meets_condition(event_data: dict):
        return False

class UnknownEvent(Event):
    """데이터만으로 식별할 수 없는 이벤트"""

class LoginEvent(Event):
    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 0
            and event_data["after"]["session"] == 1
        )

class LogoutEvent(Event):
    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 1
            and event_data["after"]["session"] == 0
        )

class SystemMonitor:
    """시스템에서 발생한 이벤트 분류"""

    def __init__(self, event_data):
        self.event_data = event_data

    def identify_event(self):
        for event_cls in Event.__subclasses__():
            try:
                if event_cls.meets_condition(self.event_data):
                    return event_cls(self.event_data)
            except KeyError:
                continue

        return UnknownEvent(self.event_data)

상호 작용이 추상화를 통해 이뤄지고 있음

분류 메소드는 이제 특정 이벤트 타입 대신에 일반적인 인터페이스를 따르는 제네릭 이벤트와 동작한다.
이 인터페이스를 따르는 제네릭들은 모두 meets_condition 메소드를 구현하여 다형성을 보장한다.

__subclasses__() 메소드를 사용해 이벤트 유형을 찾는 것에 주목

  • 새로운 유형의 이벤트를 지원하려면 단지 Event 클래스를 상속 받고, meets_condition() 메소드를 구현하기만 하면 된다.

이벤트 시스템 확장

확장 가능함을 증명

  • 사용자 트랜잭션에 대응하는 이벤트 추가 지원
class Event:
    def __init__(self, raw_data):
        self.raw_data = raw_data

    @staticmethod
    def meets_condition(event_data: dict):
        return False

class UnknownEvent(Event):
    """데이터만으로 식별할 수 없는 이벤트"""

class LoginEvent(Event):
    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 0
            and event_data["after"]["session"] == 1
        )

class LogoutEvent(Event):
    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 1
            and event_data["after"]["session"] == 0
        )

class TransactionEvent(Event):
    """시스템에서 발생한 트랜잭션 이벤트"""

    @staticmethod
    def meets_condition(event_data: dict):
        return event_data["after"].get("transaction") is not None

class SystemMonitor:
    """시스템에서 발생한 이벤트 분류"""

    def __init__(self, event_data):
        self.event_data = event_data

    def identify_event(self):
        for event_cls in Event.__subclasses__():
            try:
                if event_cls.meets_condition(self.event_data):
                    return event_cls(self.event_data)
            except KeyError:
                continue

        return UnknownEvent(self.event_data)

TransactionEvent라는 새로운 클래스를 추가하는 것만으로 기존 코드가 잘 동작한다.

identify_event() 메소드는 전혀 수정하지 않은 것으로 이 메소드가 새로운 유형의 이벤트에 대해서 폐쇄되어 있다고 할 수 있다.
반대로 Event 클래스는 필요할 때마다 새로운 유형의 이벤트를 추가할 수 있는 것으로 이벤트는 새로운 타입의 확장에 대해 개방되어 있다고 할 수 있다.

OCP 최종 정리

  • 이 원칙은 다형성의 효과적인 사용과 밀접하게 관련되어 있다.
  • 다형성을 따르는 형태의 계약을 만들고 모델을 쉽게 확장할 수 있는 일반적인 구조로 디자인 하는 것
  • 유지보수성에 대한 문제를 해결
  • 추상화에 대해서 적절한 폐쇄가 필요