В Python я столкнулся с проблемой: тип списка является инвариантным — это означает, что он может содержать только объекты определенного типа, иначе вы получите ошибку типа (например, при запуске mypy) — но иногда вам нужно использовать коллекция в более общей функции, которая может принимать все типы базового класса (назовем его A), а также все производные классы. Яркий пример это
class A:
pass
class B(A):
pass
def print_list_a(my_list: list[A]) -> None:
print(my_list)
l1 = [A()]
l2 = [B()]
print_list_a(l1)
print_list_a(l2)
Когда я запускаю вышеописанное с помощью mypy в строгом режиме, я получаю следующее
main.py:41: error: Argument 1 to "print_list_a" has incompatible type "list[B]"; expected "list[A]" [arg-type]
main.py:41: note: "List" is invariant
main.py:41: note: Consider using "Sequence" instead, which is covariant
Можете ли вы объяснить, что означает примечание «инвариантные и ковариантные типы» и как лучше всего его решить?
Я перефразировал это на QA, чтобы лучше соответствовать формату stackoverflow.






Прежде всего, для ясности, я бы хотел изменить определение print_list_a следующим образом:
def print_list_a(my_list: list[A]) -> None:
a: A
for a in my_list:
print(a)
В противном случае может возникнуть вопрос, а почему бы просто не сделать параметр типа Any?:
def print_list_a(a: Any) -> None:
print(a)
Теперь, чтобы ответить на ваш вопрос об ошибках MyPy, есть две вещи:
l2 подразумевается как list[B], поскольку это самый узкий тип, удовлетворяющий MyPy.list в Python инвариантны, что означает, что B является (подклассом) A не имеет никакого отношения к отношениям между list[B] и list[A]list[B] является (подклассом) list[A], поскольку для этого потребуется, чтобы list были ковариантными.list[A] является (подклассом) list[B]), что требует, чтобы list были контравариантными.Таким образом, объединяя два приведенных выше пункта, вы можете передавать только list[A] в print_list_a(...). В частности, вы не можете передать list[B], так как list[B] не являются list[A].
Таким образом, вы можете исправить код, изменив одно из приведенных выше:
l2 было list[A], явно аннотировав его:class A:
pass
class B(A):
pass
def print_list_a(my_list: list[A]) -> None:
a: A
for a in my_list:
print(a)
l1: list[A] = [A()]
l2: list[A] = [B()]
print_list_a(l1)
print_list_a(l2)
print_list_a взять ковариантный параметр такой, что some collection[B] является (подклассом) some collection[A]:Это вариант только в том случае, если у вас есть доступ к исходному коду print_list_a и он print_list_a не добавляет элементы в параметр my_list.
Но пока это правда, вы можете использовать ковариантный тип коллекции (например, доступный только для чтения Sequence), и по сути это выглядит как предоставленный вами ответ:
from typing import Sequence
class A:
pass
class B(A):
pass
def print_list_a(my_list: Sequence[A]) -> None:
a: A
for a in my_list:
print(a)
l1 = [A()]
l2 = [B()]
print_list_a(l1)
print_list_a(l2)
Чтобы понять, почему ковариация не работает при добавлении функции в список, рассмотрим этот пример:
def append_and_print_list_a(my_list: list[A]) -> None:
my_list.append(A()) # Note we are adding an A to the list, which is legal
a: A
for a in my_list:
print(a)
l2: list[B] = [B()]
append_and_print_list_a(l2) # Does not compile
Мы говорим:
l2 — это list, который содержит только B (не A)append_and_print_list_a мы добавляем A() к my_list.Код, если бы ему разрешили проверку типа, привел бы к конфликту двух приведенных выше пунктов.
Если вы запустите это в mypy в строгом режиме, вы получите сообщение об ошибке. На самом деле это была моя первая попытка и причина этого поста. Я остановился на использовании связанного типа, как описано в моем ответе ниже, но кто-то посоветовал мне использовать последовательность вместо списка, поскольку это ковариантный тип. Мне действительно интересно, каковы последствия этого
Из любопытства, что такое ошибка MyPy?
Ах, извините, я прочитал это в спешке. Я не заметил, что вы набрали l2 для списка [A]. Повторно запустил таким образом, сообщив mypy, что это список A, и все прошло нормально! Возможно, это самое простое, что можно сделать здесь?
Есть ли случай, когда вы бы использовали последовательность, а не делали это? Вы упомянули в своем комментарии о разных эффектах чтения и письма из списка?
Я должен признать, что мои знания (и комментарии здесь) в основном основаны на ковариации/контравариантности/инвариантности, не специфичной для Python (они применимы и к дженерикам в других языках). Я не особо знаком с иерархией типов коллекций Python, но считаю, что Sequence являются более общими и доступны только для чтения (в отличие от MutableSequence), и поэтому позволяют вашей функции принимать не только list, но и str. также. Поэтому, если вам нужна более общая функция, имеет смысл использовать Sequence.
Зависит от того, что вам нужно: обязательно ли это должен быть список? Т.е. вам нужны методы, которые есть только у
list? Тогда ваша функция должна принять список. Или вы хотите, чтобы ваш интерфейс был более универсальным? Должен ли он также принимать кортежи или другие «последовательности»? Тогда ваша функция должна приниматьSequence— этот совет справедлив для всех типов, реализующих какой-либо интерфейс.