Menu
Coddy logo textTech

Mixins

Coddy Python 여정의 Object Oriented Programming 섹션에 포함된 레슨 — 64개 중 38번째.

믹스인(Mixins)은 클래스에 추가 기능을 "혼합"하기 위해 사용되는 특별한 종류의 다중 상속입니다. 믹스인은 그 자체로 완전한 클래스는 아니면서 특정 메서드를 제공합니다.

다음은 간단한 믹스인(mixin)의 예입니다:

class JSONSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

이 믹스인(mixin)이 무엇을 하는지 분석해 보겠습니다:

  • self.__dict__ - 이 특수 속성은 객체의 모든 속성과 해당 값의 딕셔너리를 포함합니다
  • json.dumps() - 이 함수는 Python 딕셔너리를 JSON 형식의 문자열로 변환합니다
  • 이 믹스인은 자신을 상속하는 모든 클래스에 이 JSON 직렬화 기능을 "믹스인(mixes in)"합니다

이제 실제로 실행해 보겠습니다:

class User(JSONSerializableMixin):
    def __init__(self, name, email):
        self.name = name
        self.email = email

믹스인은 이를 상속받는 모든 클래스에 JSON 기능을 추가합니다:

user = User("Alice", "alice@example.com")
print(user.to_json())

출력:

{"name": "Alice", "email": "alice@example.com"}

핵심 통찰: User 클래스는 이제 직접 정의하지 않고도 to_json() 메서드를 갖게 되었습니다. 믹스인이 그 기능을 "믹스인"했습니다!

다양한 기능을 위해 여러 개의 믹스인(mixin)을 생성합니다:

class PrintableMixin:
    def pretty_print(self):
        for key, value in self.__dict__.items():
            print(f"{key}: {value}")

class ComparableMixin:
    def __eq__(self, other):
        return self.__dict__ == other.__dict__

각 믹스인은 어떤 클래스가 믹스인을 사용하는지에 관계없이 객체의 속성을 작업하기 위해 self.__dict__에 접근합니다. 이것이 믹스인의 힘입니다. 믹스인은 모든 클래스의 속성과 함께 작동하는 재사용 가능한 기능을 제공합니다.

하나의 클래스에 여러 믹스인(mixin)을 결합합니다:

class Product(JSONSerializableMixin, PrintableMixin, ComparableMixin):
    def __init__(self, name, price):
        self.name = name
        self.price = price

product1 = Product("Laptop", 999)
product2 = Product("Laptop", 999)

모든 mixin 기능을 사용합니다:

print(product1.to_json())         # JSONSerializableMixin에서 제공
product1.pretty_print()          # PrintableMixin에서 제공
print(product1 == product2)      # ComparableMixin에서 제공

출력:

{"name": "Laptop", "price": 999}
name: Laptop
price: 999
True

믹스인(mixin)의 주요 특징:

  • 단독으로 인스턴스화되도록 설계되지 않음
  • 특정하고 재사용 가능한 기능을 제공함
  • 보통 __init__ 메서드를 가지지 않음
  • 이름이 주로 "Mixin" 또는 "able"로 끝남
  • 다중 상속과 결합될 수 있음
  • 유연성을 위해 self.__dict__ 또는 다른 공통 객체 기능과 함께 작동함

핵심 포인트: 믹스인(Mixins)은 복잡한 상속 트리를 만들지 않고도 서로 다른 클래스 계층 구조 간에 기능을 공유할 수 있는 방법을 제공합니다. 이를 통해 직렬화(serialization), 비교(comparison) 또는 출력(printing)과 같은 특정 기능을 필요한 모든 클래스에 "믹스인(mix in)"할 수 있습니다. 이는 코드 재사용을 촉진하고 클래스가 주요 책임에 집중할 수 있도록 유지합니다.

challenge icon

챌린지

중급

이 챌린지에서는 믹스인(mixins)과 상속을 사용하여 간단한 이커머스 시스템을 구현합니다.

다음 파일들에 필요한 클래스들을 구현하세요 (각 파일의 TODO 주석을 따르세요):

  • printablemixin.py - 포맷된 출력 기능을 가진 PrintableMixin을 생성합니다.
  • discountmixin.py - 가격 할인 계산을 위한 DiscountMixin을 구현합니다.
  • shippablemixin.py - 무게 및 배송비 기능을 위한 ShippableMixin을 구축합니다.
  • product.py - 적절한 믹스인 상속을 포함한 기본 Product 클래스를 개발합니다.
  • physicalproduct.py - Product를 확장하는 PhysicalProduct 클래스를 생성합니다.
  • digitalproduct.py - 특별한 할인 동작을 가진 DigitalProduct 클래스를 구현합니다.

치트 시트

믹스인(Mixins)은 그 자체로 완전한 클래스는 아니면서 클래스에 추가적인 기능을 "혼합(mix in)"하기 위해 사용되는 특별한 종류의 다중 상속입니다.

기본적인 믹스인 예시:

class JSONSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

class User(JSONSerializableMixin):
    def __init__(self, name, email):
        self.name = name
        self.email = email

여러 개의 믹스인을 결합할 수 있습니다:

class PrintableMixin:
    def pretty_print(self):
        for key, value in self.__dict__.items():
            print(f"{key}: {value}")

class ComparableMixin:
    def __eq__(self, other):
        return self.__dict__ == other.__dict__

class Product(JSONSerializableMixin, PrintableMixin, ComparableMixin):
    def __init__(self, name, price):
        self.name = name
        self.price = price

믹스인의 주요 특징:

  • 그 자체로 인스턴스화되도록 설계되지 않음
  • 구체적이고 재사용 가능한 기능을 제공함
  • 보통 __init__ 메서드를 가지지 않음
  • 이름이 종종 "Mixin" 또는 "able"로 끝남
  • 다중 상속과 결합될 수 있음

직접 해보기

from product import Product
from physicalproduct import PhysicalProduct
from digitalproduct import DigitalProduct
from printablemixin import PrintableMixin
from discountmixin import DiscountMixin
from shippablemixin import ShippableMixin

def test_basic_functionality():
    # 모든 클래스의 기본 기능 테스트
    p = Product("Laptop", 1000)
    assert p.print_details() == "Product: Laptop, Price: $1000", f"Print details failed: {p.print_details()}"
    assert p.apply_discount(10) == 900, f"Discount calculation failed: {p.apply_discount(10)}"
    
    physical = PhysicalProduct("Desk", 500)
    physical.set_weight(30)
    assert physical.calculate_shipping() == 15, f"Shipping calculation failed: {physical.calculate_shipping()}"
    
    digital = DigitalProduct("Software", 200)
    assert digital.apply_discount(10) == 180, f"Digital discount failed: {digital.apply_discount(10)}"
    print("Basic functionality test passed!")

def test_edge_cases():
    # 0 및 음수 값과 같은 경계 사례 테스트
    p = Product("Free Item", 0)
    assert p.apply_discount(10) == 0, f"Zero price discount failed: {p.apply_discount(10)}"
    
    p_neg = Product("Negative Item", -100)
    assert p_neg.apply_discount(10) == -90, f"Negative price discount failed: {p_neg.apply_discount(10)}"
    
    physical = PhysicalProduct("Empty Box", 10)
    physical.set_weight(0)
    assert physical.calculate_shipping() == 0, f"Zero weight shipping failed: {physical.calculate_shipping()}"
    
    physical_neg = PhysicalProduct("Anti-Gravity Item", 10)
    physical_neg.set_weight(-5)
    assert physical_neg.calculate_shipping() == -2.5, f"Negative weight shipping failed: {physical_neg.calculate_shipping()}"
    print("Edge cases test passed!")

def test_large_values():
    # 매우 큰 값으로 테스트
    p = Product("Expensive Item", 1000000)
    assert p.apply_discount(10) == 900000, f"Large value discount failed: {p.apply_discount(10)}"
    
    physical = PhysicalProduct("Heavy Item", 500)
    physical.set_weight(1000)
    assert physical.calculate_shipping() == 500, f"Large weight shipping failed: {physical.calculate_shipping()}"
    print("Large values test passed!")

def test_inheritance():
    # 상속 관계 테스트
    p = Product("Test", 100)
    physical = PhysicalProduct("Test", 100)
    digital = DigitalProduct("Test", 100)
    
    assert isinstance(p, PrintableMixin), "Product should inherit from PrintableMixin"
    assert isinstance(p, DiscountMixin), "Product should inherit from DiscountMixin"
    
    assert isinstance(physical, Product), "PhysicalProduct should inherit from Product"
    assert isinstance(physical, ShippableMixin), "PhysicalProduct should inherit from ShippableMixin"
    assert isinstance(physical, PrintableMixin), "PhysicalProduct should inherit from PrintableMixin through Product"
    assert isinstance(physical, DiscountMixin), "PhysicalProduct should inherit from DiscountMixin through Product"
    
    assert isinstance(digital, Product), "DigitalProduct should inherit from Product"
    assert isinstance(digital, PrintableMixin), "DigitalProduct should inherit from PrintableMixin through Product"
    assert isinstance(digital, DiscountMixin), "DigitalProduct should inherit from DiscountMixin through Product"
    print("Inheritance test passed!")

def test_method_overriding():
    # 메서드 오버라이딩 동작 테스트
    p = Product("Regular Product", 100)
    digital = DigitalProduct("Digital Product", 100)
    
    # 동일한 가격, 동일한 할인율, 다른 결과
    assert p.apply_discount(20) == 80, f"Regular discount calculation failed: {p.apply_discount(20)}"
    assert digital.apply_discount(20) == 90, f"Digital fixed discount failed: {digital.apply_discount(20)}"
    
    # 디지털 제품은 매개변수에 관계없이 항상 10% 할인을 적용해야 함
    assert digital.apply_discount(0) == 90, "Digital product should apply 10% discount even with 0%"
    assert digital.apply_discount(50) == 90, "Digital product should apply 10% discount even with 50%"
    print("Method overriding test passed!")

def test_polymorphism():
    # 다양한 제품 유형 리스트를 통한 다형성 동작 테스트
    products = [
        Product("Regular", 100),
        PhysicalProduct("Physical", 100),
        DigitalProduct("Digital", 100)
    ]
    
    # 물리적 제품의 무게 설정
    products[1].set_weight(10)
    
    # apply_discount(20)에 대한 예상 결과
    expected_discounts = [80, 80, 90]
    
    for i, product in enumerate(products):
        # 모든 제품은 print_details 메서드를 가져야 함
        assert "Product:" in product.print_details(), f"Polymorphic print_details failed for {type(product)}"
        
        # 모든 제품은 apply_discount 메서드를 가져야 하지만 구현은 서로 다름
        assert product.apply_discount(20) == expected_discounts[i], f"Polymorphic apply_discount failed for {type(product)}"
    print("Polymorphism test passed!")

def test_attribute_access():
    # 속성 접근 패턴 테스트
    p = Product("Test Product", 100)
    assert p.name == "Test Product", "Name attribute not properly set in Product"
    assert p.price == 100, "Price attribute not properly set in Product"
    
    physical = PhysicalProduct("Physical Product", 200)
    assert physical.name == "Physical Product", "Name attribute not properly set in PhysicalProduct"
    assert physical.price == 200, "Price attribute not properly set in PhysicalProduct"
    
    # set_weight가 호출되기 전에는 weight 속성이 존재하지 않아야 함
    try:
        weight = physical.weight
        assert False, "Weight attribute should not exist before set_weight is called"
    except AttributeError:
        pass
    
    physical.set_weight(15)
    assert physical.weight == 15, "Weight attribute not properly set in PhysicalProduct"
    
    digital = DigitalProduct("Digital Product", 300)
    assert digital.name == "Digital Product", "Name attribute not properly set in DigitalProduct"
    assert digital.price == 300, "Price attribute not properly set in DigitalProduct"
    print("Attribute access test passed!")

# 메인 테스트 실행기
test_case = input()

if test_case == "basic_test":
    test_basic_functionality()
elif test_case == "edge_cases":
    test_edge_cases()
elif test_case == "large_values":
    test_large_values()
elif test_case == "inheritance":
    test_inheritance()
elif test_case == "method_overriding":
    test_method_overriding()
elif test_case == "polymorphism":
    test_polymorphism()
elif test_case == "attribute_access":
    test_attribute_access()
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

Object Oriented Programming의 모든 레슨