mypy: создание типа, который принимает список экземпляров подклассов

Предположим, у меня есть класс Child, который является подклассом класса Parent, и функция, которая принимает список экземпляров подклассов Parent:

from typing import List


class Parent:
    pass


class Child(Parent):
    pass


def func(objects: List[Parent]) -> None:
    print(objects)


children = [Child()]
func(children)

при запуске mypy возникает ошибка:

 error: Argument 1 to "func" has incompatible type "List[Child]"; expected "List[Parent]"

Как мне создать для этого тип?

P.S. Есть способ исправить эту конкретную ошибку с помощью типа Sequence:

def func(objects: Sequence[Parent]) -> None:
    print(objects)

но это не помогает в других подобных случаях. Мне нужен List, а не Sequence.


person kurtgn    schedule 13.11.2018    source источник


Ответы (1)


Передача списка здесь принципиально небезопасна для типов. Например, что если вы это сделаете?

def func(objects: List[Parent]) -> None:
    print(objects)
    objects.append(Parent())

children: List[Child] = [Child(), Child(), Child()]
func(children)
# Uh-oh! 'children' contains a Parent()!

Если бы это было разрешено для проверки типа, ваш код в конечном итоге содержал бы ошибку.

Чтобы использовать жаргон типов, List намеренно разработан как инвариантный тип. То есть, хотя Child является подклассом Parent, это не тот случай, когда List[Child] является подтипом List[Parent], или наоборот. Дополнительную информацию об инвариантности можно найти здесь и здесь.

Самая распространенная альтернатива - использовать вместо этого Sequence, который является интерфейсом / протоколом только для чтения / чем угодно. А поскольку Sequence доступен только для чтения, он может быть ковариантным безопасно: то есть Sequence[Child] считается допустимым подтипом Sequence[Parent].

В зависимости от того, что именно вы делаете, вы можете использовать введите переменные. Например. вместо того, чтобы сказать «эта функция принимает список Parent», вы говорите «эта функция принимает список любого класса, который является Parent, или подклассом Parent»:

TParent = TypeVar('TParent', bound=Parent)

def func(objects: List[TParent]) -> List[TParent]:
    print(objects)

    # Would not typecheck: we can't assume 'objects' will be a List[Parent]
    objects.append(Parent())  

    return objects

В зависимости от того, что именно вы делаете, вы можете создать собственный протокол, который определяет коллекцию, подобную списку только для записи (или настраиваемую структуру данных). А поскольку ваша структура данных будет доступна только для записи, вы можете сделать ее контравариантной, то есть WriteOnlyThing[Parent] будет подтипом WriteOnlyThing[Child]. Затем вы заставляете func принимать WriteOnlyThing[Child] и можете безопасно передавать как WriteOnlyThing[Child], так и WriteOnlyThing[Parent].

Если ни один из подходов не работает в вашем случае, ваш единственный выход - либо использовать # type: ignore, чтобы заглушить ошибку (не рекомендуется), отказаться от проверки типов содержимого списка и сделать аргумент типа List[Any] (также не рекомендуется), либо выяснить, как реструктурировать код, чтобы он был безопасным по типам.

person Michael0x2a    schedule 13.11.2018
comment
Чтобы сделать его типобезопасным, вы также можете использовать Tuple вместо List. Вы не можете добавить в кортеж, поэтому проблема, описанная @ Michael0x2a, исчезла. - person pawelswiecki; 13.11.2018
comment
@pawelswiecki - вы можете использовать кортеж, конечно, или любую ковариантную структуру данных, но я не думаю, что использование Tuple в этом случае действительно выгодно вам по сравнению с использованием Sequence в этом случае. Tuple является подклассом Sequence и добавляет только несколько дополнительных методов, специфичных для кортежей, таких как __lt__, ни один из которых, как мне кажется, также не реализован с помощью списков. Если исходный код OP использовал списки, они для начала использовали любой из этих методов, зависящих от кортежа. Так почему бы не перейти к более общему типу и просто использовать последовательность? - person Michael0x2a; 14.11.2018
comment
Если кортеж в порядке (сложно сказать без дополнительного контекста), я бы использовал его, поскольку это намного проще, чем использование TypeVar. (Список действительно реализует __lt__, по крайней мере, вы можете сравнивать списки.) - person pawelswiecki; 14.11.2018
comment
Typevar - это именно то, что я искал. Я видел документы, просто не мог осмыслить концепцию верхней границы. Спасибо! - person kurtgn; 15.11.2018
comment
@ Michael0x2a, пожалуйста, взгляните на этот вопрос stackoverflow.com/questions/53316262/, это расширенная полная версия моей проблемы со всем контекстом. Большое спасибо! - person kurtgn; 15.11.2018