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

Простые зависимые типы в Python

Python Typing только что получил новую блестящую особенность: Буквальные типы. Давайте погрузимся в знакомое с ним. Теги с Python, учебником, начинающим, обучением.

Первоначально опубликовано в моем блоге : https://sobolevn.me/2019/01/simple-deded-types-in-python.

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

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

Кто-то сказал мне, что каждое уравнение, которое я включен в книгу, будет вдвое, что продажи. Поэтому я решил не иметь никаких уравнений вообще. В конце концов, однако, я поставил в одном уравнении, знаменитое уравнение Эйнштейна, ^ 2. Я надеюсь, что это не отпугивает половину моих потенциальных читателей

Что такое зависимый ввод? Это концепция, когда вы полагаетесь на значения некоторых типов, а не просто сырье.

Рассмотрим этот пример:

from typing import Union

def return_int_or_str(flag: bool) -> Union[str, int]:
    if flag:
        return 'I am a string!'
    return 0

Мы можем ясно видеть, что в зависимости от стоимости Флаг мы можем получить ул или int ценности. Тип результата будет Союз [ул, int] Отказ И каждый раз, когда мы называем эту функцию с помощью смешанного типа возврата, мы должны проверить, какой тип мы на самом деле получили и что делать с этим. Это неудобно и делает ваш код более сложным.

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

Рассмотрим открыть Функция из стандартной библиотеки Отказ Как часто вы получили ошибки времени выполнения, потому что вы перепутали ул и Байты ? Это случилось тысячу раз для меня. И я не хочу, чтобы это повторилось! Итак, мы напишем Type-Safe Code для этого времени.

def open_file(filename: str, mode: str):
    return open(filename, mode)

Какой тип возврата мы ожидаем здесь? ул ? Подождите секунду! Мы можем назвать это так: open_file ('ue.txt', 'rb') И это вернется Байты Действительно Итак, тип возврата – Союз [IO [STR], IO [BYTES]] Отказ И это действительно трудно работать с этим. Мы в конечном итоге окажемся с большим количеством условий, ненужные отказы и охранники.

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

Буквальный и @overload.

Если у вас нет Marpy и Typing_extensions Установлен, вам нужно установить последнюю версию этих пакетов.

» pip install mypy typing_extensions

И теперь мы готовы переписать наш код с помощью Буквальный и @overload. :

from typing import overload
from typing_extension import Literal

Быстрая сторона Примечание : Набрав это встроенный Python Модуль, где определены все возможные типы. И скорость разработки этого модуля ограничена новым Python версию релизы. И Typing_extensions является официальным пакетом для новых типов, которые будут доступны в будущих выпусках Python Отказ Итак, это решает все проблемы с скоростью выпуска и частотой регулярных Набрав модуль.

Литерал

Буквальный Тип представляет определенное значение определенного типа.

from typing_extensions import Literal

def function(x: Literal[1]) -> Literal[1]:
     return x

function(1)
# => OK!

function(2)
# => Argument has incompatible type "Literal[2]"; expected "Literal[1]"

Для запуска этого кода используйте: mypy.6 --strict test.py. . Это останется прежними для всех примеров в этой статье.

Это потрясающе! Но, в чем разница между Буквальный [0] и int тип?

from typing_extensions import Literal

def function(x: int = 0, y: Literal[0] = 0) -> int:
    reveal_type(x)
    # => Revealed type is 'builtins.int'
    reveal_type(y)
    # => Revealed type is 'Literal[0]'
    return x

Выявленные типы отличаются. Единственный способ получить Буквальный Тип – аннотировать как Литерал . Это сделано для сохранения обратной совместимости с более старыми версиями Marpy И не обнаруживать Х: как Буквальный тип. Потому что ценность х позже может быть изменен.

Вы можете использовать Буквальный [0] Везде, где регулярно int можно использовать, но не наоборот.

from typing_extensions import Literal

def function(x: int, y: Literal[0]) -> int:
    return x

x1: int = 0
y1: Literal[0] = 0

function(y1, y1)
function(x1, x1)
# => Argument 2 has incompatible type "int"; expected "Literal[0]"

Видеть? С Х1 является переменной – ее нельзя использовать, где мы ожидаем Буквальный s. В первой части этой серии я написал статью о Использование реальных констант в Python Отказ Прочитайте его, если вы не знаете разницу между переменными и константами в Python Отказ

Поможет ли константы в этом случае? Да, они будут!

from typing_extensions import Literal, Final

def function(x: int = 0, y: Literal[0] = 0) -> int:
     return x

x: Final = 0
y: Literal[0] = 0

function(y, y)
function(x, x)

Как вы можете видеть, при объявлении некоторого значения Финал – Мы создаем постоянную. Это не может быть изменено. И это соответствует тому, что Буквальный является. Исходный код Реализация этих двух типов также очень похожа.

Почему я постоянно называю зависимыми типами Python просто? Потому что он в настоящее время ограничен простыми значениями: int. , ул ...| , Bool , плавать , Нет Отказ В настоящее время он не может работать с кортежами, спискими, дикторами, пользовательскими типами и классами и т. Д. Но вы можете отслеживать прогресс развития В этой теме Отказ

Не забывайте о Официальные документы .

@overload.

Следующее, что нам нужно, это @overload декоратор. Требуется определение нескольких деклараций функций с различными типами ввода и результатами.

Представьте, что у нас есть ситуация, когда нам нужно написать функцию, которая уменьшает значение. Это должно работать с обоими ул и int входы. Когда дано ул ...| Он должен вернуть все входные символы, кроме последнего, но при данных int. Это должно вернуть предыдущее число.

from typing import Union

def decrease(first: Union[str, int]) -> Union[str, int]:
    if isinstance(first, int):
        return first - 1
    return first[:-1]

reveal_type(decrease(1))
# => Revealed type is 'Union[builtins.str, builtins.int]'
reveal_type(decrease('abc'))
# => Revealed type is 'Union[builtins.str, builtins.int]'

Не слишком практично, не так ли? Marpy Еще не знает, какой конкретный тип был возвращен. Мы можем улучшить печатать с @overload декоратор.

from typing import Union, overload

@overload
def decrease(first: str) -> str:
    """Decreases a string."""

@overload
def decrease(first: int) -> int:
    """Decreases a number."""

def decrease(first: Union[str, int]) -> Union[str, int]:
    if isinstance(first, int):
        return first - 1
    return first[:-1]

reveal_type(decrease(1))
# => Revealed type is 'builtins.int'
reveal_type(decrease('abc'))
# => Revealed type is 'builtins.str'

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

Идея такова: всякий раз, когда Marpy Находит функцию с несколькими @overload. Главы он пытается соответствовать входным значениям к этим объявлениям. Когда он находит первый матч – возвращает тип результата.

Официальная документация Может также помочь вам понять, как использовать его в ваших проектах.

Зависимые типы

Теперь мы собираемся объединить наши новые знания о Буквальный и @overload вместе, чтобы решить нашу проблему с открыть . Наконец!

Помните, нам нужно вернуть Байты для 'rb' Режим и ул ...| для ‘R’ режим. И нам нужно знать точный тип возврата.

Алгоритм будет:

  1. Напишите несколько @overload декораторы, чтобы соответствовать всем возможным случаям
  2. Напишите Буквальный [] Типы, когда мы ожидаем получить 'R' или «РБ»
  3. Написать функцию логики в общем случае
from typing import IO, Any, Union, overload
from typing_extensions import Literal

@overload
def open_file(filename: str, mode: Literal['r']) -> IO[str]:
    """When 'r' is supplied we return 'str'."""

@overload
def open_file(filename: str, mode: Literal['rb']) -> IO[bytes]:
    """When 'rb' is supplied we return 'bytes' instead of a 'str'."""

@overload
def open_file(filename: str, mode: str) -> IO[Any]:
    """Any other options might return Any-thing!."""

def open_file(filename: str, mode: str) -> IO[Any]:
    return open(filename, mode)

reveal_type(open_file('some.txt', 'r'))
# => Revealed type is 'typing.IO[builtins.str]'
reveal_type(open_file('some.txt', 'rb'))
# => Revealed type is 'typing.IO[builtins.bytes]'
reveal_type(open_file('some.txt', 'other'))
# => Revealed type is 'typing.IO[AnyStr]'

Что мы имеем здесь? Три @overload Декораторы и функциональное тело с логикой. Первый @overload Декоратор объявляет, чтобы вернуться ул ...| для ‘R’ Буквальный Параметр, второй говорит об возврате Байты Когда мы используем ‘rb’ параметр. И третий отстаил. Всякий раз, когда мы предоставляем другой другой режим - мы можем получить оба ул и Байты Отказ

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

Спасибо, как работают зависимые типы в Python Действительно

Вывод

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

Оригинал: “https://dev.to/wemake-services/simple-dependent-types-in-python-4e14”