본문 바로가기

더 나은 엔지니어가 되기 위해/파이썬을 파이썬스럽게

슬기로운 파이썬 트릭 3 - 클래스와 객체 지향 프로그래밍

이 글은 슬기로운 파이썬 트릭을 읽고 핵심만 빠르게 정리한 글이다.
전반적으로 대부분 아는 내용이지만 remind up 한다는 느낌으로 정리해본다.


4.1. 객체 비교: is vs ==

예제

a = [1, 2, 3]
b = a
c = [1, 2, 3]
>>> a == b
True
>>> a is b
True
>>> a == c
True
>>> a is c
False

요점 정리

  • 두 변수가 동일한(identical) 객체를 가리키는 경우, is 표현식은 True 로 평가된다.
  • 두 변수가 동등한(equal: 내용이 같은) 객체를 가리키는 경우,== 표현식은 True 로 평가된다.

4.2. 문자열 변환(모든 클래스는 __repr__ 이 필요하다)

예제

class Car:
    def __init__(self, color, mileage):
        self.color = color
        self.mileage = mileage

    def __repr__(self):
        return f"{self.__class__.__name__}({self.color!r}, {self.mileage!r})"

    def __str__(self):
        return f"a {self.color} car"
>>> my_car = Car('red', 37281)
>>>
>>> repr(my_car)
"Car('red', 37281)"
>>> str(my_ar)
"a red car"

요점 정리

  • __str____repr__ 메서드를 사용하여 자신의 클래스에서 문자열 변환을 제어할 수 있다.
  • __str__ 의 결과는 읽을 수 있어야 한다.
  • __repr__ 의 결과는 모호하지 않아야 한다.
  • 항상 __repr__ 을 클래스에 추가하라.
    • __str__ 의 기본 구현은 __repr__ 을 호출하기만 한다.
    • 따라서 클래스에 최소한의 구현으로 거의 모든 경우에 유용한 문자열 변환 결과가 보장된다.

4.3. 자신만의 예외 클래스 정의하기

Bad Case

def validate(name):
    if len(name) < 10:
        raise ValueError

Good Case

class BaseValidationError(ValueError):
    pass

class NameTooShortError(BaseValidationError):
    pass

class NameTooLongError(BaseValidationError):
    pass


def validate(name):
    if len(name) < 10:
        raise NameTooShortError

요점 정리

  • 고유한 예외 타입을 정의하면 코드의 의도가 좀 더 명확하게 표시되고 디버깅하기 더 쉬워진다.
  • 파이썬 내장 Exception 클래스 또는 ValueErrorKeyError 와 같은 구체적인 예외 클래스에서 사용자 정의 예외를 파생시키자.
  • 상속을 사용하여 논리적으로 그룹화된 예외 계층을 정의할 수 있다.

4.4 재미있고 이득이 되는 객체 복제하기

얕은 복사

>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> ys = list(a)  # 얕은 복사본 만들기
>>
>>> xs.append([10, 11, 12])
>>> xs[1][0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9], [10, 11, 12]]
>>> ys
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]  # 얕은 복사본의 문제 : 두 단계 이상의 참조 객체는 복사되지 않는다.

깊은 복사

>>> import copy
>>>
>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> ys = copy.deepcopy(xs)  # 깊은 복사본 만들기
>>
>>> xs.append([10, 11, 12])
>>> xs[1][0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9], [10, 11, 12]]
>>> ys
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]  # 깊은 복사는 재귀적으로 모든 객체를 복사한다.

요점 정리

  • 객체의 얕은 복사본을 만들면 자식 객체가 복사되지 않는다. 따라서 사본은 원본과 완전히 독립적이지 않다.
  • 깊은 복사는 객체의 자식 객체를 재귀적으로 복사한다.
    • 이렇게 얻은 복사본은 원본과 완전히 독립적이지만, 그만큼 시간이 더 걸린다.
  • copy 모듈을 사용하여 임의의 객체를 복사할 수 있다.

4.5. 추상화 클래스는 상속을 확인한다.

예제

from abc import ABCMeta, abstractmethod

class Base(metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass


class Concrete(Base):
    def foo(self):
        pass

    # bar() 선언하는 걸 잊어버렸다!
>>> c = Concrete()
TypeError: "Can't instantiate abstract class Concrete with abstract methods bar"

요점 정리

  • 추상화 클래스는 파생 클래스가 인스턴스화될 때, 기반 클래스의 추상 메서드를 모두 구현했는지 확인한다.
  • 추상화 클래스를 사용하면 버그를 방지하고, 클래스 계층을 쉽게 유지 관리할 수 있다.

4.6. 네임드튜플은 어디에 적합한가

네임드 튜플

from collections import namedtuple

# Car 라는 이름의 불변 클래스를 생성하고 color 와 mileage 를 속성으로 가진다.
Car = namedtuple("Car", ["color", "mileage"])
>>> my_car = Car("red", 3812.4)
>>> my_car.color
"red"
>>> my_car.mileage
3812.4
>>> color, mileage = my_car  # 일반 튜플처럼 unpacking 도 가능하다.

네임드튜플 상속하기

Car = namedtuple("Car", ["color", "mileage"])

# namedtupled 의 _fields 속성으로 기존 속성목록을 가져온다.
ElectricCar = namedtuple("ElectricCar", Car._fields + ("charge", ))
>>> ElectricCar("red", 1234, 45.0)
ElectricCar(color="red", mileage=1234, charge=45.0)

내장 도우미 메서드

# _asdict() 사용 예
>>> my_car = Car("red", 3812.4)
>>> my_car._asdict()
OrderedDict([('color', 'red'), ('mileage', 3812.4)])
>>>
>>> import json
>>> json.dumps(my_car._asdict())
"{'color': 'red', 'mileage': 3812.4}"

# _replace() 사용 예
>>> my_car._replace(color='blue')
Car(color='blue', mileage=3812.4)

# _make() 사용 예
>>> Car._make(['red', 999])
Car(color='red', mileage=999)

요점 정리

  • collections.namedtuple불변 클래스를 수동으로 정의하는 메모리 효율적인 지름길이다.
  • 네임드튜플은 데이터를 이해하기 쉬운 구조로 만들어 주어, 코드를 정리하는 데 도움이 될 수 있다.

4.7 클래스 변수 vs 인스턴스 변수

일반적으로 다 알고 있는 내용(클래스 변수, 인스턴스 변수가 무언인지에 대한 설명)이라 스킵. 별 내용 없다.


4.8. 인스턴스 메서드, 클래스 메서드, 정적 메서드

예제

class MyClass:
    def method(self):
        """인스턴스 메서드: self 를 첫 번째 인자로 받음"""
        return "instance method called", self

    @classmethod
    def classmethod(cls):
        """클래스 메서드: cls 를 첫 번째 인자로 받음"""
        return "class method called", cls

    @staticmethod
    def staticmethod():
        """정적 메서드: self, cls 둘 다 안받음"""
        return "staticmethod called"
>>> my_class = MyClass()
>>> my_class.method()
('instance method called', <__main__.MyClass object at 0x107d03e50>)  # 인스턴스 접근
>>> MyClass.classmethod()
('class method called', <class '__main__.MyClass'>)  # 클래스 자체 접근
>>> my_class.staticmethod()
staticmethod called

@classmethod 로 팩토리 메서드 만들기

class Pizza:
    """피자를 생성하는 팩토리"""

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

    def __repr__(self):
        return f"Pizza({self.ingredients!r})"

    @classmethod
    def margherita(cls):
        return cls(["mozzarella", "tomatoes"])

    @classmethod
    def prosciutto(cls):
        return cls(["mozzarella", "tomatoes", "ham"])
>>> Pizza.margherita()
>>> Pizza(['mozzarella', 'tomatoes'])
>>> Pizza.prosciutto()
>>> Pizza(['mozzarella', 'tomatoes', 'ham'])

요점 정리

  • 인스턴스 메서드는 인스턴스가 필요하며, self 를 통해 인스턴스에 접근할 수 있다.
  • 클래스 메서드는 인스턴스가 필요하지 않다. 인스턴스(self) 에는 접근할 수 없지만 클래스 자체(cls) 에 접근할 수 있다.
  • 정적 메서드는 cls 또는 self 에 접근할 수 없다.
    일반함수처럼 작동하지만 자신을 정의한 클래스의 네임스페이스 속한다.
  • 정적 및 클래스 메서드는 클래스 설계에 대한 개발자의 의도를 전달하고 강제한다.
    이러한 점 덕분에, 유지 보수하는데 확실히 도움이 된다.