Протоколы в Python — использование на примерах

В этом руководстве вы узнаете о протоколах в Python и их использовании для определения неявных интерфейсов.

Что такое протокол Python?

Предположим, у вас есть функция, которая вычисляет общую стоимость списка продуктов, где каждый продукт имеет атрибуты названия, количества и цены:

from typing import List


class Product:
    def __init__(self, name: str, quantity: float, price: float):
        self.name = name
        self.quantity = quantity
        self.price = price


def calculate_total(items: List[Product]) -> float:
    return sum([item.quantity * item.price for item in items])

В этом примере функция calculate_total() принимает список объектов Product и возвращает общее значение.

При написании данной функции вам может потребоваться вычислить общую сумму списка продуктов. Но вы, вероятно, захотите использовать его для других списков, таких как инвентарные списки, в будущем.

Если вы внимательно посмотрите на функцию calculate_total(), то увидите, что она использует только атрибуты количества и цены. Чтобы сделать calculate_total() более динамичным и использовать подсказки типов, вы можете использовать Protocol из модуля typing. Класс Protocol доступен начиная с версии Python 3.8, описанной в PEP 544.

Ниже описано, как использовать класс протокола.

Сначала определите класс Item, который наследуется от Protocol с двумя атрибутами: количеством и ценой:

class Item(Protocol):
    quantity: float
    price: float

Во-вторых, измените функцию calculate_total(), которая принимает список объектов Item вместо списка объектов Product:

def calculate_total(items: List[Item]) -> float:
    return sum([item.quantity * item.price for item in items])

Сделав это, вы можете передать любой список объектов Item в функцию calculation_total() с условием, что каждый элемент имеет два атрибута: количество и цена.

Ниже показана полная программа:

from typing import List, Protocol


class Item(Protocol):
    quantity: float
    price: float


class Product:
    def __init__(self, name: str, quantity: float, price: float):
        self.name = name
        self.quantity = quantity
        self.price = price


def calculate_total(items: List[Item]) -> float:
    return sum([item.quantity * item.price for item in items])


# calculate total a product list
total = calculate_total([
    Product('A', 10, 150),
    Product('B', 5, 250)
])

print(total)

Например, вы можете определить список запасов в инвентаре и передать его в функцию calculate_total():

# ...

class Stock:
    def __init__(self, product_name, quantity, price):
        self.product_name = product_name
        self.quantity = quantity
        self.price = price


# calculate total an inventory list
total = calculate_total([
    Stock('Tablet', 5, 950),
    Stock('Laptop', 10, 850)
])

print(total)

В этом примере классу Product и Stock не требуется создавать подклассы класса Item, но их все равно можно использовать в функции calculation_total().

В Python это называется утиной типизацией. При утиной типизации поведение и свойства объекта определяют тип объекта, а не явный тип объекта. Например, объект с количеством и ценой будет следовать протоколу Item, независимо от его явного типа.

Типирование утки вдохновлено тестом на утку:

Если оно ходит как утка и крякает как утка, то это, должно быть, утка.

На практике, когда вы пишете функцию, принимающую входные данные, вас больше заботят поведение и свойства входных данных, а не их явный тип.

Похожие посты
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *