Рубрики
Без рубрики

Ковариация и контравариация в общих типах

С Python и Corgis. Помечено Python, Generics, Corgis.

Статическая набравка потрясающая. Это помогает обнаружить ошибки, действует как документация в коде и делает разработку более приятными. Недавно я начал пользоваться Python’s Набрав Модуль для добавления статического ввода на все мои проекты Python. Система печати Python может не быть такой же мощной, как можно надеяться, но я думаю, что, как только вы напечатаете, вы не возвращаетесь.

Это, однако, легко запускаться в неинтекобривные ошибки при определении типов для общих типов, таких как списки и словари. Предположим, например, что A DogContainer берет Список Собака S в конструкторе:

from typing import List

class Dog:
    ...

class DogContainer:

    def __init__(self, dogs: List[Dog]):
        self.dogs = dogs

Теперь давайте добавим немного корги в контейнер:

class Corgi(Dog):
    ...

corgis: List[Corgi] = [Corgi(), Corgi()]
container = DogContainer(dogs=corgis)  # ERROR!

Тип-проверка поднимает следующую ошибку:

Аргумент списка типов [CORGI] ‘не может быть назначен параметрам «Собаки» типа «Собака»: «Corgi» несовместимо с «собакой».

Почему эта работа не работает? Конечно, можно заменить список Corgis для списка собак?

Ковариация

Давайте вернемся и размышляем, почему мы думаем, что мы можем заменить Список [Corgi] для Список [Собака] Отказ Потому что Corgi это подтип Собака (мы пишем Corgi <: Собака , что означает, что мы можем использовать экземпляр Corgi Везде, где экземпляр Собака ожидается), мы думаем, что следует следовать этому Список [Corgi] <: список [собака] Отказ Это свойство называется ковариация Отказ

Общий класс C [T] является ковариантным в переменной типа T Если следующее владеет: A <: B ⇒ C [A] <: C [B]

Мы видели выше, что Список [Corgi] не подтип Список [Собака] . Поэтому Список это не ковариант в своем типе переменной.

Почему Список Не ковариант? Проблема в том, что можно добавлять новые объекты в Список , то есть, это смешно. Или положить его другой путь, Список не ковариант, потому что он действует как раковина из вариабельной переменной. Подумайте о следующем коде:

class StBernard(Dog):
    ...

def do_stuff(dogs: List[Dog]) -> None:
    """Function that secretly adds Beethoven to the input list.
    """
    dogs.append(StBernard(name="Beethoven"))

corgis: List[Corgi] = ...
do_stuff(corgis)
last_corgi = corgis.pop()  # BOOM, type-safety broken! You think you have a corgi but you have Beethoven

Проблема здесь – Добавить метод. Что делать, если мы удалили какие-либо методы из Список Это делают его смешным? Это, по сути, даст нам Последовательность , неизменный список.

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

A <: BПоследовательность [A] <: последовательность [B]

Поэтому Последовательность [T] это ковариант в T Отказ Еще один пример неизменного контейнера Сопоставление , версия только для чтения Дикт Отказ

Чистые источники являются ковариантными

Давайте обобщем предыдущее обсуждение следующему правилу:

Если общий класс C [T] Это чистый источник экземпляров типа T это ковариант в T Отказ

Почему источники ковариантны? Предположим, у вас есть A <: B Отказ Теперь представьте, что у вас есть код, который зависит от Источник [B] Отказ Вы можете заменить Источник [a] ? Да, потому что Источник [a] производит экземпляры типа А Это действительные случаи B Отказ

Например, подумайте о классе Заводчик [T] который производит собак типа T . Ковариация означает, что Заводчик [Corgi] <: Заводчик [Собака] Отказ Другими словами, любой код ожидает Заводчик [Собака] будет рад получить Заводчик [Corgi] Отказ Это имеет смысл интуитивно.

Следующий кусок кода показывает, как мы могли определить Заводчик Класс и отметьте его ковариант в своем типе переменной с ковариант = Правда :

import abc
from typing import TypeVar

DogType_co = TypeVar('T', bound=Dog, covariant=True)


class Breeder(abc.ABC, Generic[DogType_co]):
    @abc.abstractmethod
    def get(self) -> DogType_co:
        ...


class CorgiBreeder(Breeder[Corgi]):
    def get(self) -> Corgi:
        print("Breeding a corgi")
        return Corgi()


breeder: Breeder[Dog] = CorgiBreeder()  # cast allowed because of covariance

Контравариация

Если вы посмотрите на код сверху, где была сломана безопасность типа, вы можете заметить, что другой способ добиться безопасности типа безопасности будет предотвратить чтение от Corgis Отказ Другими словами, Список должно быть только для записи или раковина .

Как конкретный пример раковины, рассмотрим Фотограф Класс, где генерический тип должен быть Собака :

from typing import Generic,TypeVar

DogType = TypeVar('DogType', bound=Dog)

class Photographer(Generic[DogType]):

    def photograph(self, dog: DogType_co):
        print("Photographing dog")


class CorgiPhotographer(Photographer[Corgi]):
    def photograph(self, corgi: Corgi):
        self.feed_sausage_to(corgi)
        super().photograph(corgi)

    def feed_sausage_to(self, corgi: Corgi):
        print("Feeding sausage to corgi")


class DogPhotographer(Photographer[Dog]):
    def photograph(self, dog: Dog):
        super().photograph(dog)

Можем ли мы предположить Фотограф [CORGI] <: фотограф [собака] ? Это будет означать, что куда-нибудь нам нужен фотограф для собак, мы можем использовать фотограф Corgi. Это не так: фотограф Corgi не может сфотографировать Святого Бернарда.

Тем не менее, инверсию делает: если нужен фотограф Corgis, фотограф универсальных собак определенно может справиться с работой. Формально, Фотограф [собака] <: фотограф [CORGI] Отказ

Фотограф является примером класса, который является контравариант в его переменме типа. Официальное определение:

Общий класс C [T] контравариация в типе переменной T Если следующее владеет: A <: B => C [A]:> C [B]

Способ отметить Class Contravaranarant в вариабе его типа – это настроить контравариант = Правда :

DogType_contra = TypeVar('DogType_contra', bound=Dog, contravariant=True)

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

dog_photographer: Photographer[Corgi] = DogPhotographer()  # Photographer[Dog] <: Photographer[Corgi]

Чистые мойки противоречат

Фотограф противоречит в своем типе переменной, потому что это чистый раковина для собак. Это не производит новых собак, но только их потребляет (eww). Мы приходим к следующему правилу:

Если общий класс C [T] это чистая мойка экземпляров типа T это контравариант в T Отказ

Почему раковины контравариация? Предположим, у вас есть A <: B и представьте, что у вас есть какой-либо код, который зависит от Раковина [A] Отказ Можете ли вы заменить Мойка [B] ? Да, потому что Мойка [B] готов потреблять любой B И, потому что А это подтип B он также может потреблять экземпляры типа A .

Инвариантные типы

Общие типы, которые не являются ни ковариантными, ни контравариантными, являются Инвариант в их типах переменных. Мы видели выше, что Список является примером инвариантного родового типа.

Как еще один пример инвариантного родового типа, рассмотрим Rescueshelter [D] класс. Экземпляр Rescueshelter [D] принимает новые плохо обработанные собаки типа D и доставляет их в свои новые дома.

Rescueshelter Источник и мойка собак, так что это ни контравариант, ни ковариант. Нельзя заменить Rescueshelter [Corgi] для Rescueshelter [Собака] потому что Rescueshelter [Corgi] не принимает собак, кроме корги. Точно так же нельзя заменить Rescueshelter [Собака] для Rescueshelter [Corgi] Потому что человек, который ищет корги от приюта спасения, не будет рад получить собаку Святого Бернарда.

Этот пример иллюстрирует опасность полагаться слишком много на интуиции. Интуитивно, возможно, может показаться ясно, что спасательный укрытие Corgis «является« спасением собак, и можно заменить бывшее для последнего. Мы видели выше, что это только верно, если укрытие спасения действует только как источник собак.

Заключение

В заключение, в целом проще работать с общими типами, которые являются ковариантными или контравариантными. В следующий раз вы определяете функцию или структуру данных в зависимости от Список или Обдумывать , внимательно подумайте, если вам нужно мутировать объект. Если нет, вы, вероятно, должны использовать Последовательность или Сопоставление вместо. Точно так же, если вам не нужно читать с вашего универсального типа, вы можете определить его как только для записи и пометить переменную тип как Contravariant.

Спасибо за прочтение! Если здесь есть что-то, что кажется неверным или неточным, пожалуйста, оставьте комментарий.

Не забудьте насладиться Corgis ответственно Отказ

Оригинал: “https://dev.to/meeshkan/covariance-and-contravariance-in-generic-types-3k63”