Menu
Coddy logo textTech

Миксины

Часть раздела Object Oriented Programming путешествия по Python на Coddy — урок 38 из 64.

Миксины (примеси) — это особый вид множественного наследования, используемый для «подмешивания» дополнительной функциональности в классы. Они предоставляют определенные методы, не являясь при этом полноценными классами сами по себе.

Вот пример простой примеси (mixin):

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

Давайте разберем, что делает этот примесь (mixin):

  • self.__dict__ — этот специальный атрибут содержит словарь всех атрибутов объекта и их значений
  • json.dumps() — эта функция преобразует словарь Python в строку в формате JSON
  • Примесь «подмешивает» эту функциональность сериализации JSON в любой класс, который наследуется от нее

Теперь посмотрим на это в действии:

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(), не определяя его напрямую. Примесь «подмешала» эту функциональность!

Создайте несколько миксинов для различной функциональности:

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__ для работы с атрибутами объекта, независимо от того, какой класс использует этот миксин. В этом и заключается сила миксинов — они предоставляют переиспользуемую функциональность, которая работает с атрибутами любого класса.

Объедините несколько примесей в одном классе:

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

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

Используйте все возможности миксинов:

print(product1.to_json())         # Из JSONSerializableMixin
product1.pretty_print()          # Из PrintableMixin
print(product1 == product2)      # Из ComparableMixin

Вывод:

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

Основные характеристики примесей (mixins):

  • Не предназначены для создания экземпляров сами по себе
  • Предоставляют специфическую, повторно используемую функциональность
  • Обычно не имеют методов __init__
  • Имена часто заканчиваются на «Mixin» или «able»
  • Могут сочетаться с множественным наследованием
  • Работают с self.__dict__ или другими общими функциями объектов для обеспечения гибкости

Ключевой момент: Миксины (Mixins) предоставляют способ совместного использования функциональности в различных иерархиях классов без создания сложных деревьев наследования. Они позволяют «подмешивать» определенные возможности, такие как сериализация, сравнение или печать, в любой класс, которому они необходимы. Это способствует повторному использованию кода и позволяет классам сосредоточиться на своих основных обязанностях.

challenge icon

Задание

Средне

В этом испытании вы реализуете простую систему электронной коммерции, используя примеси (mixins) и наследование.

Реализуйте необходимые классы в этих файлах (следуйте комментариям TODO в каждом файле):

  • printablemixin.py — Создайте PrintableMixin с функциональностью форматированного вывода
  • discountmixin.py — Реализуйте DiscountMixin для расчета снижения цены
  • shippablemixin.py — Создайте ShippableMixin для функций веса и стоимости доставки
  • product.py — Разработайте базовый класс Product с соответствующим наследованием примесей
  • physicalproduct.py — Создайте класс PhysicalProduct, расширяющий Product
  • digitalproduct.py — Реализуйте класс DigitalProduct со специальным поведением скидок

Шпаргалка

Примеси (Mixins) — это особый вид множественного наследования, используемый для «подмешивания» дополнительной функциональности в классы, при этом сами они не являются полноценными классами.

Базовый пример примеси:

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():
    # Проверка граничных случаев, таких как нулевые и отрицательные значения
    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"
    
    # Атрибут weight не должен существовать до вызова set_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