Полиморфизм и сопоставление типов шаблонов в Python

Полиморфизм и сопоставление типов шаблонов в Python

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

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

Python использует динамический выбор конечной реализации оператора на основе используемых типов данных. В Python мы всегда пишем общие определения. Код не привязан к конкретному типу данных. В рабочей среде Python будут найдены соответствующие операции, основанные на типах используемых объектов. Это означает, что компилятор не подтверждает, что наши функции ожидают и создают соответствующие типы данных. Мы обычно полагаемся на модульное тестирование и инструмент mypy для такого рода проверки типов.

В редких случаях нам может потребоваться другое поведение, основанное на типах элементов данных. У нас есть два способа решить эту проблему:

  • Мы можем использовать функцию isinstance(), чтобы различать разные случаи;
  • Мы можем создать собственный подкласс чисел. Number или NamedTuple и реализовать соответствующие полиморфные имена специальных методов.

В некоторых случаях нам действительно нужно сделать так, чтобы мы могли включать соответствующие преобразования типов данных для каждой операции. Кроме того, нам нужно будет использовать функцию cast(), чтобы сделать типы явными для инструмента mypy.

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

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

from typing import NamedTuple, Tuple, Any
class Rank_Data(NamedTuple):
rank_seq: Tuple[float]
raw: Any

Типичное использование такого рода определения класса показано в этом примере:

 >>> data = {'key1': 1, 'key2': 2}
>>> r = Rank_Data((2, 7), data)
>>> r.rank_seq[0]
2
>>> r.raw
{'key1': 1, 'key2': 2}

Строка необработанных данных в этом примере — это словарь. В этом списке есть два рейтинга для этого конкретного элемента. Приложение может получить последовательность ранжирования, а также исходный исходный элемент данных. Во многих предыдущих примерах нам нужна либо итеративная, либо конкретная коллекция. Цикл for является изящным методом в данной работе. Однако мы не всегда используем оператор for, и для некоторых функций нам пришлось явно использовать iter(). Мы можем справиться с этой ситуацией с помощью простой проверки isinstance(), как показано в следующем фрагменте кода:

def some_function(seq_or_iter: Union[Sequence, Iterator]):
if isinstance(seq_or_iter, Sequence):
yield from some_function(iter(seq_or_iter), key)
return
# Do the real work of the function using the Iterator

Этот пример включает проверку типа для обработки небольшой разницы между объектом Sequence и Iterator. В частности, функция использует iter() для создания Iterator из последовательности и называет себя рекурсивно с производным значением.

Для упорядочения рангов будет поддерживаться союз [Sequence, Iterator]. Поскольку исходные данные должны быть отсортированы для ранжирования, проще использовать list() для преобразования заданного итератора в конкретную последовательность. Будет использоваться основная проверка isinstance(), но вместо создания итератора из последовательности (как показано выше) следующие примеры создадут объект последовательности из итератора.

В контексте нашей функции упорядочения рангов мы можем сделать функцию несколько более общей. Следующие два выражения определяют входные данные:

Source = Union[Rank_Data, Any]
Union[Sequence[Source], Iterator[Source]]

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

  • Sequence[Rank_Data]
  • Sequence[Any]
  • Iterator[Rank_Data]
  • Iterator[Any]

Обработка четырех комбинированных типов данных

Вот функция rank_data() с тремя случаями для обработки четырех комбинаций типов данных:

from typing import (
Callable, Sequence, Iterator, Union, Iterable, 
TypeVar, cast, Union
)
K_ = TypeVar("K_") # Some comparable key type used for ranking.
Source = Union[Rank_Data, Any] 
def rank_data(
seq_or_iter: Union[Sequence[Source], Iterator[Source]],
key: Callable[[Rank_Data], K_] = lambda obj: cast(K_, obj)
) -> Iterable[Rank_Data]:
if isinstance(seq_or_iter, Iterator):
# Iterator? Materialize a sequence object
yield from rank_data(list(seq_or_iter), key)
return
data: Sequence[Rank_Data]
if isinstance(seq_or_iter[0], Rank_Data):
# Collection of Rank_Data is what we prefer.
data = seq_or_iter
else:
# Convert to Rank_Data and process.
empty_ranks: Tuple[float] = cast(Tuple[float], ())
data = list(
Rank_Data(empty_ranks, raw_data)
for raw_data in cast(Sequence[Source], seq_or_iter)
)
for r, rd in rerank(data, key):
new_ranks = cast(
Tuple[float], 
rd.rank_seq + cast(Tuple[float], (r,)))
yield Rank_Data(new_ranks, rd.raw)

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

  • Учитывая Iterator (объект без использования метода __getitem __ ()), мы реализуем объект списка для работы. Это будет работать для Rank_Data, а также для любого другого типа необработанных данных. Этот случай охватывает объекты, которые являются Iterator [Rank_Data], а также Iterator [Any].
  • Учитывая последовательность [Any], мы будем обертывать неизвестные объекты в корни Rank_Data пустым набором ранжировок для создания последовательности [Rank_Data].
  • Наконец, с учетом последовательности [Rank_Data] добавим еще ранжирование в кортеж рангов внутри каждого контейнера Rank_Data.

Первый случай вызывает rank_data() рекурсивно. Остальные два случая полагаются на функцию rerank(), которая строит новый кортеж Rank_Data с дополнительными значениями ранжирования. Это содержит несколько ранжировок для сложной записи значений необработанных данных.

Обратите внимание на то, что для сложного использования выражения родовых наборов требуется относительно сложное выражение cast(). Инструмент mypy предлагает функцию show_type(), которая может быть включена для отладки выводимых типов.

Функция rerank() следует немного другой конструкции на примере функции rank(), показанной ранее. Он дает два кортежа с рангом и исходным объектом данных:

def rerank(
rank_data_iter: Iterable[Rank_Data],
key: Callable[[Rank_Data], K_]
) -> Iterator[Tuple[float, Rank_Data]]:
sorted_iter = iter(
sorted(
rank_data_iter, key=lambda obj: key(obj.raw)
)
)
# Apply ranker to head, *tail = sorted(rank_data_iter)
head = next(sorted_iter)
yield from ranker(sorted_iter, 0, [head], key)

Суть rerank() заключается в сортировке коллекции объектов Rank_Data. Первый элемент, head, используется для предоставления начального значения функции ranker(). Функция ranker() может проверять оставшиеся элементы в iterable, чтобы увидеть, соответствуют ли они этому начальному значению, что позволяет вычислить правильный ранг для партии совпадающих элементов.

Функция ranker() принимает отсортированную итерабельность данных, номер базового ранга и начальный набор элементов минимального ранга. Результатом является итерируемая последовательность двух кортежей с номером ранга и ассоциированным объектом Rank_Data:

def ranker(
sorted_iter: Iterator[Rank_Data],
base: float,
same_rank_seq: List[Rank_Data],
key: Callable[[Rank_Data], K_]
) -> Iterator[Tuple[float, Rank_Data]]:
try:
value = next(sorted_iter)
except StopIteration:
dups = len(same_rank_seq)
yield from yield_sequence(
(base+1+base+dups)/2, iter(same_rank_seq))
return
if key(value.raw) == key(same_rank_seq[0].raw):
yield from ranker(
sorted_iter, base, same_rank_seq+[value], key)
else:
dups = len(same_rank_seq)
yield from yield_sequence(
(base+1+base+dups)/2, iter(same_rank_seq))
yield from ranker(
sorted_iter, base+dups, [value], key)

Результат функции ranker () зависит от функции yield_sequence (), которая выглядит следующим образом:

def yield_sequence(
rank: float,
same_rank_iter: Iterator[Rank_Data]
) -> Iterator[Tuple[float, Rank_Data]]:
head = next(same_rank_iter)
yield rank, head
yield from yield_sequence(rank, same_rank_iter)

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

>>> scalars= [0.8, 1.2, 1.2, 2.3, 18]
>>> list(rank_data(scalars))
[Rank_Data(rank_seq=(1.0,), raw=0.8), 
Rank_Data(rank_seq=(2.5,), raw=1.2),
Rank_Data(rank_seq=(2.5,), raw=1.2), 
Rank_Data(rank_seq=(4.0,), raw=2.3),
Rank_Data(rank_seq=(5.0,), raw=18)]

Когда мы работаем с немного более сложным объектом, мы также можем иметь несколько ранжировок. Ниже приведена последовательность из двух кортежей:

 >>> pairs = ((2, 0.8), (3, 1.2), (5, 1.2), (7, 2.3), (11, 18))
>>> rank_x = list(rank_data(pairs, key=lambda x:x[0]))
>>> rank_x
[Rank_Data(rank_seq=(1.0,), raw=(2, 0.8)),
Rank_Data(rank_seq=(2.0,), raw=(3, 1.2)),
Rank_Data(rank_seq=(3.0,), raw=(5, 1.2)),
Rank_Data(rank_seq=(4.0,), raw=(7, 2.3)),
Rank_Data(rank_seq=(5.0,), raw=(11, 18))]

Здесь мы определили набор пар. Затем мы оценили два кортежа, назначив последовательность объектов Rank_Data переменной rank_x. Затем мы оценили этот набор объектов Rank_Data, создав значение второго ранга и присвоив результат переменной rank_xy. Полученная последовательность может использоваться для слегка измененной функции rank_corr() для вычисления ранговых корреляций любого из доступных значений в атрибуте rank_seq объектов Rank_Data. Мы оставим эту модификацию в качестве упражнения для вас.

 

Добавить комментарий

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