Протоколы в 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, независимо от его явного типа.
Типирование утки вдохновлено тестом на утку:
Если оно ходит как утка и крякает как утка, то это, должно быть, утка.
На практике, когда вы пишете функцию, принимающую входные данные, вас больше заботят поведение и свойства входных данных, а не их явный тип.