Первоначально опубликовано в моем блоге : https://sobolevn.me/2021/06/typeclasses-in-python
Сегодня я собираюсь представить новую концепцию для разработчиков Python: Typeclasses. Это концепция нашего нового сухой питон
Библиотека под названием классы
.
Я скажу вам заранее, что это будет выглядеть очень знакомо для того, что вы уже знаете, и, возможно, даже использовать. Более того, мы повторно используем много существующего кода из стандартной библиотеки Python. Итак, вы можете назвать этот подход «родным» и «Pythonic». И это все еще будет интересно: я показываю примеры на 4 разных языках!
Но, прежде чем обсудить сами типа, давайте обсудим, какую проблему они решат.
Некоторые функции должны вести себя по -разному
Хорошо, это знакомая проблема для всех разработчиков. Как мы можем написать функцию, которая будет вести себя по -разному для разных типов?
Давайте создадим пример. Мы хотим Приветствую
Разные типы по -разному (да, примеры «Привет, мир», вот и мы). Мы хотим приветствовать
:
str
экземпляры какПривет, {string_content}!
Myuser
экземпляры какПривет еще раз, {имя пользователя}
Обратите внимание, что Приветствую
В качестве простого примера на самом деле не имеет большого «бизнеса», но более сложные вещи, такие как to_json
, from_json
, to_sql
В from_sql
и to_binary
Иметь большой смысл и можно найти практически в любом проекте. Но, ради простоты реализации, я собираюсь придерживаться нашего Приветствую
пример.
Первый подход, который приходит в нашу уму, – это использовать iSinstance ()
Проверяет внутри самой функции. И это может работать в некоторых случаях! Единственным требованием является то, что мы должен Знайте все типы, с которыми мы будем работать заранее.
Вот как бы это выглядело:
@dataclass class MyUser(object): name: str def greet(instance: str | MyUser) -> str: if isinstance(instance, str): return 'Hello, "{0}"!'.format(instance) elif isinstance(instance, MyUser): return 'Hello again, {0}'.format(instance.name) raise NotImplementedError( 'Cannot greet "{0}" type'.format(type(instance)), )
Основное ограничение заключается в том, что мы не можем легко расширить эту функцию для другого типа (мы можем использовать функцию обертки, но я рассматриваю это переосмысление).
Но в некоторых случаях – Isinstance
Недостаточно, потому что нам нужна расширенность. Нам нужно поддерживать другие типы, которые неизвестны заранее. Нашим пользователям может потребоваться приветствовать
их пользовательские типы.
И это та часть, где все начинает становиться интересными.
Все языки программирования решают эту проблему по -разному. Давайте начнем с традиционного подхода Python к ООП.
ООП расширяемость и чрезмерные проблемы
Итак, как Python решает эту проблему?
Мы все знаем, что у Python есть магические методы для некоторых встроенных функций, таких как Len ()
и __len__
, это решает точно такую же проблему.
Допустим, мы хотим приветствовать пользователя:
@dataclass class MyUser(object): name: str def greet(self) -> str: return 'Hello again, {0}'.format(self.name)
Вы можете использовать этот метод напрямую, или вы можете создать помощника с набор Протокол
:
from typing_extensions import Protocol class CanGreet(Protocol): def greet(self) -> str: """ It will match any object that has the ``greet`` method. Mypy will also check that ``greet`` must return ``str``. """ def greet(instance: CanGreet) -> str: return instance.greet()
И тогда мы можем его использовать:
print(greet(MyUser(name='example'))) # Hello again, example
Итак, это работает? Не совсем Анкет
Есть несколько проблем.
Первый Некоторые классы не хотят знать некоторые подробности о себе, чтобы сохранить целостность абстракции. Например:
class Person(object): def become_friends(self, friend: 'Person') -> None: ... def is_friend_of(self, person: 'Person') -> bool: ... def get_pets(self) -> Sequence['Pet']: ...
Это делает это Человек
(каламбур) заслуживает того, чтобы знать, что некоторые to_json
Существует преобразование, которое может превратить эту бедную Человек
в текстовые данные? А как насчет бинарного маринования? Конечно, эти детали не должны быть добавлены в абстракцию на уровне бизнеса, это называется Утекая абстракция Когда вы делаете иначе.
Более того, я думаю, что смешивание структуры и поведения в единую абстракцию плохо. Почему? Потому что вы не можете заранее сказать, какое поведение вам понадобится из данной структуры.
Для абстракций на этом уровне гораздо легче иметь поведение вблизи структуры, а не внутри него. Смешивание этих двух имеет смысл только тогда, когда мы работаем на более высоком уровне, как Сервисы или процессы Анкет
Второй , это работает только для пользовательских типов. Существующие типы трудно расширить Анкет Например, как бы вы добавили Приветствую
Метод для str
тип?
Вы можете создать str
подтип с Приветствую
Метод в нем:
class MyStr(str): def greet(self) -> str: return 'Hello, {0}!'.format(self)
Но это потребует изменения в нашем использовании:
print(greet(MyStr('world'))) # Hello, world! print(greet('world')) # fails with TypeError
Обезьяна
Некоторые могут предположить, что мы можем просто вставить необходимые методы непосредственно в объект/тип. Некоторые динамически напечатанные языки пошли по этому пути: JavaScript
(В 2000 -х и начале 2010 -х годов, в основном популяризированной jQuery
плагины) и Ruby
( все еще происходит прямо сейчас ). Вот как это выглядит:
String.prototype.greet = function (string) { return `Hello, ${string}!` }
Это совершенно очевидно, что он не будет работать ни для чего сложного. Почему ?
- Различные части вашей программы могут использовать обезьяны методов с одинаковым именем, но с различной функциональностью. И Ничто не сработает
- Его трудно прочитать, потому что исходный источник не содержит исправленного метода, и место для исправления может быть глубоко скрыто в других файлах
- Например, сложно печатать,
mypy
не поддерживает это вообще - Сообщество Python не привыкли к этому стилю, было бы довольно трудно убедить их написать свой код так (и это хорошо!)
Я надеюсь, что это ясно: мы не попадем в эту ловушку. Давайте рассмотрим другую альтернативу.
Дополнительные абстракции
Люди знакомы с такими вещами, как django-rest-framework
может рекомендовать добавить Специальные абстракции к Приветствую
различные виды:
import abc from typing import Generic, TypeVar _Wrapped = TypeVar('_Wrapped') class BaseGreet(Generic[_Wrapped]): """Abstract class of all other """ def __init__(self, wrapped: _Wrapped) -> None: self._wrapped = wrapped @abc.abstractmethod def greet(self) -> str: raise NotImplementedError class StrGreet(BaseGreet[str]): """Wrapped instance of built-in type ``str``.""" def greet(self) -> str: return 'Hello, {0}!'.format(self._wrapped) # Our custom type: @dataclass class MyUser(object): name: str class MyUserGreet(BaseGreet[MyUser]): def greet(self) -> str: return 'Hello again, {0}'.format(self._wrapped.name)
И мы можем использовать это так:
print(greet(MyStrGreet('world'))) # Hello, world! print(greet(MyUserGreet(MyUser(name='example')))) # Hello again, example
Но теперь у нас другая проблема: у нас есть разрыв между реальными типами и их обертками. Нет простого способа обернуть тип в его обертку. Как мы можем их соответствовать? Мы должны сделать это либо вручную, либо использовать какой -то реестр, такой как Dict [type, type [basegreet]]
Анкет
И этого все еще недостаточно, будут ошибки времени выполнения! На практике это заканчивается как
Как многие из нас могли видеть это с DRF
Сериализаторы при попытке сериализовать пользовательский незарегистрированный тип.
Typeclasses и подобные концепции
Давайте посмотрим, как функциональные языки (и Rust
, люди все еще Будь то функционально или нет) обрабатывать эту проблему.
Некоторые общеизвестные знания:
- У всех этих языков нет
класс
концепция, как мы его знаем в Python и, конечно, нет подклассов - Все языки ниже не имеют
объект
S, как мы делаем в Python, они не смешивают поведение и структуру (однако,elixir
имеет Алан Кей Реальные объекты ) - Вместо этого эти языки используют Ак-хок-полиморфизм Для того, чтобы функции ведут себя по -разному для разных типов через перегрузку
- И, конечно, вам не нужно знать ни одного из языков ниже, чтобы понять, что происходит
Эликсир
Давайте начнем с одного из моих любимых. Эликсир
имеет Протокол
S Чтобы достичь того, что мы хотим:
@doc "Our custom protocol" defprotocol Greet do # This is an abstract function, # that will behave differently for each type. def greet(data) end @doc "Enhancing built-in type" defimpl Greet, for: BitString do def greet(string), do: "Hello, #{string}!" end @doc "Custom data type" defmodule MyUser do defstruct [:name] end @doc "Enhancing our own type" defimpl Greet, for: MyUser do def greet(user), do: "Hello again, #{user.name}" end
Я почти уверен, что мои читатели смогли читать и понять Эликсир
Даже если они не знакомы с этим языком. Это то, что я называю красотой!
Использование кода выше:
# Using our `Greet.greet` function with both our data types: IO.puts(Greet.greet("world")) # Hello, world! IO.puts(Greet.greet(%MyUser{name: "example"})) # Hello again, example
Вещь с Эликсир
S Протокол
S это то, что это в настоящее время невозможно Чтобы выразить, что какой -то тип поддерживает наш Greet.greet
для Эликсир
S Тип Проверка . Но это не имеет большого значения для Эликсир
, который на 100% динамически напечатан.
Протоколы очень широко используются, они поддерживают много функций языка. Вот несколько реальных примеров:
Перечисляется
Позволяет работать с коллекциями: подсчет элементов, Поиск участников, сокращение и нарезкуНить. Chars
что -то вроде__str__
В Python он преобразует структуры в читаемый по человеку формат
Ржавчина
Ржавчина
имеет Черта
S Анкет Концепция очень похожа на Протокол
s в Эликсир
:
// Our custom trait trait Greet { fn greet(&self) -> String; } // Enhancing built-in type impl Greet for String { fn greet(&self) -> String { return format!("Hello, {}!", &self); } } // Defining our own type struct MyUser { name: String, } // Enhancing it impl Greet for MyUser { fn greet(&self) -> String { return format!("Hello again, {}", self.name); } }
И, конечно, из -за Ржавчина
Статическое набор текста, мы можем выразить, что аргумент некоторой функции поддерживает только что определительную черту, которую мы только что определили:
// We can express that `greet` function only accepts types // that implement `Greet` trait: fn greet(instance: &dyn Greet) -> String { return instance.greet(); } pub fn main() { // Using our `greet` function with both our data types: println!("{}", greet(&"world".to_string())); // Hello, world! println!("{}", greet(&MyUser { name: "example".to_string() })); // Hello again, example }
Видеть? Идея настолько похожа, что использует почти тот же синтаксис, что и Эликсир
Анкет
Примечательные примеры реальной жизни того, как Ржавчина
использует его Черта
S:
Копия
иКлон
– дублирующие объектыОтладка
чтобы показать лучшеrepr
объекта снова как__str__
в Python
В основном, Черта
S являются ядром этого языка, он широко используется в тех случаях, когда вам нужно определить любое общее поведение.
Хаскелл
Хаскелл
имеет Typeclasses делать почти то же самое.
Итак, что такое Typeclass? TypeClass – это группа типов, все из которых удовлетворяют некоторому общему контракту. Это также форма специального полиморфизма, который в основном используется для перегрузки.
Мне немного жаль Хаскелл
Синтаксис ниже, может быть не очень приятно и ясно читать, особенно для людей, которые не знакомы с этим блестящим языком, но у нас есть то, что имеем:
{-# LANGUAGE FlexibleInstances #-} -- Our custom typeclass class Greet instance where greet :: instance -> String -- Enhancing built-in type with it instance Greet String where greet str = "Hello, " ++ str ++ "!" -- Defining our own type data MyUser = MyUser { name :: String } -- Enhancing it instance Greet MyUser where greet user = "Hello again, " ++ (name user)
По сути, мы делаем то же самое, что и уже для Ржавчина
и Эликсир
:
- Мы определяем
Приветствую
TypeClass, который имеет одну функцию для реализации:приветствовать
- Затем мы определяем реализацию экземпляра для
Строка
Тип, который является встроенным (псевдоним для[Char]
) - Тогда мы определяем пользователь
Myuser
Тип симя
ПолеСтрока
тип - Реализация
Приветствую
Typeclass дляMyuser
это последнее, что мы делаем
Тогда мы можем использовать наш новый приветствовать
Функция:
-- Here you can see that we can use `Greet` typeclass to annotate our types. -- I have made this alias entirely for this annotation demo, -- in real life we would just use `greet` directly: greetAlias :: Greet instance => instance -> String greetAlias = greet main = do print $ greetAlias "world" -- Hello, world! print $ greetAlias MyUser { name="example" } -- Hello again, example
Некоторые реальные примеры Typeclasses:
Показать
Чтобы преобразовать вещи в читаемые пользователи представленияФунктор
,Применить
иMonad
все типы
Я бы сказал, что среди наших трех примеров, Хаскелл
полагается на свои типы самых тяжелых.
Важно отметить, что Typeclases от Хаскелл
и черты от Ржавчина
немного разные , но мы не будем вдаваться в эти детали, чтобы сохранить эту статью довольно короткой.
Но как насчет Python?
сухой питотон/классы
В библиотеке Python Standard есть потрясающая функция под названием SingleDispatch
Анкет
Это делает именно то, что нам нужно. Вы все еще помните, что мы находим способ изменить поведение функции на основе ввода?
Давайте посмотрим!
from functools import singledispatch @singledispatch def greet(instance) -> str: """Default case.""" raise NotImplementedError @greet.register def _greet_str(instance: str) -> str: return 'Hello, {0}!'.format(instance) # Custom type @dataclass class MyUser(object): name: str @greet.register def _greet_myuser(instance: MyUser) -> str: return 'Hello again, {0}'.format(instance.name)
Выглядит круто, кроме того, это в стандартной LIB, вам даже не нужно ничего устанавливать!
И мы можем использовать его как обычная функция:
print(greet('world')) # Hello, world! print(greet(MyUser(name='example'))) # Hello again, example
Итак, какой смысл в написании совершенно другой библиотеки, как мы делали с сухой питотон/классы
?
Мы даже повторно используем некоторые части SingleDispatch
реализация, Но есть несколько ключевых различий.
Лучший набор
С SingleDispatch
Вы не можете быть уверены, что все будет работать, потому что оно не поддерживается mypy
Анкет
Например, вы можете передать неподдерживаемые типы:
greet(1) # mypy is ok with that :( # runtime will raise `NotImplementedError`
В сухой питотон/классы
Мы исправили это. Вы можете пройти только типы, которые поддерживаются:
from classes import typeclass @typeclass def greet(instance) -> str: ... @greet.instance(str) def _greet_str(instance: str) -> str: return 'Iterable!' greet(1) # Argument 1 to "greet" has incompatible type "int"; expected "str"
Или вы можете сломать @singledispatch
Контракт на подпись:
@greet.register def _greet_dict(instance: dict, key: str) -> int: return instance[key] # still no mypy error
Но не с сухой питотон/классы
:
@greet.instance(dict) def _greet_dict(instance: dict, key: str) -> int: ... # Instance callback is incompatible # "def (instance: builtins.dict[Any, Any], key: builtins.str) -> builtins.int"; # expected # "def (instance: builtins.dict[Any, Any]) -> builtins.str"
@singledispatch
также не позволяет определять общие функции:
@singledispatch def copy(instance: X) -> X: """Default case.""" raise NotImplementedError @copy.register def _copy_int(instance: int) -> int: return instance # Argument 1 to "register" of "_SingleDispatchCallable" # has incompatible type "Callable[[int], int]"; # expected "Callable[..., X]" reveal_type(copy(1)) # Revealed type is "X`-1" # Should be: `int`
Что, опять же, возможно с сухой питотон/классы
, мы полностью поддерживаем общие функции :
from typing import TypeVar from classes import typeclass X = TypeVar('X') @typeclass def copy(instance: X) -> X: ... @copy.instance(int) def _copy_int(instance: int) -> int: ... # ok reveal_type(copy(1)) # int
И ты не можешь ограничить @singledispatch
Работать только с подтипами определенных типов, даже если вы хотите.
Протоколы не поддерживаются
Протоколы являются важной частью Python. К сожалению, они не поддерживаются @singledispatch
:
@greet.register def _greet_iterable(instance: Iterable) -> str: return 'Iterable!' # TypeError: Invalid annotation for 'instance'. # typing.Iterable is not a class
Протоколы Поддержка также решается с сухой питотон/классы
:
from typing import Iterable from classes import typeclass @typeclass def greet(instance) -> str: ... @greet.instance(Iterable, is_protocol=True) def _greet_str(instance: Iterable) -> str: return 'Iterable!' print(greet([1, 2, 3])) # Iterable!
Нет возможности аннотировать типы
Допустим, вы хотите написать функцию и аннотировать один из ее аргументов, что она должна поддерживать приветствовать
функция Что-то вроде:
def greet_and_print(instance: '???') -> None: print(greet(instance))
Это невозможно с @singledispatch
Анкет Но вы можете сделать это с сухой питотон/классы
:
from classes import AssociatedType, Supports, typeclass class Greet(AssociatedType): """Special type to represent that some instance can `greet`.""" @typeclass(Greet) def greet(instance) -> str: """No implementation needed.""" @greet.instance(str) def _greet_str(instance: str) -> str: return 'Hello, {0}!'.format(instance) def greet_and_print(instance: Supports[Greet]) -> None: print(greet(instance)) greet_and_print('world') # ok greet_and_print(1) # type error with mypy, exception in runtime # Argument 1 to "greet_and_print" has incompatible type "int"; # expected "Supports[Greet]"
Вывод
Мы прошли долгий путь, от базового сложенного iSinstance ()
Условия – через ООП – в типовые клады.
Я показал, что эта родная и питоническая идея заслуживает более широкого признания и использования. И наши дополнительные функции в сухой питотон/классы
Можно спасти вас от множества ошибок и помочь написать более выразительную и безопасную бизнес -логику.
В результате использования TypeClasses вы растете свои структуры от поведения, что позволит вам избавиться от бесполезных и сложных абстракций и написать код типов в типах. У вас будет ваше поведение рядом с структурами, а не внутри них. Это также решит проблему расширения ООП.
Объединить его с другими сухой питон
Библиотеки для дополнительного эффекта!
Будущая работа
Что мы планируем на будущее?
Есть несколько ключевых аспектов для улучшения:
- Наш
Поддерживает
должен взять любое количество аргументов типа:Поддерживает [A, B, C]
Анкет Этот тип будет представлять собой тип, который поддерживает все три типа TypeclassA
,B
иВ
в то же время - Мы не Поддержка бетона дженерики только пока. Так, например, невозможно определить различные случаи для
Список [int]
иСписок [str]
. Это может потребовать добавления времени выполнения Typecheker ксухой питотон/классы
- Я планирую Чтобы сделать тесты частью этого приложения также! Мы отправим Гипотеза плагин Чтобы протестировать Typeclasses пользователей в одной строке кода
Быть в курсе!
Если вам нравится эта статья, вы можете:
- Пожертвовать в будущее
сухой питон
Развитие на GitHub - Светь наш
классы
репо - Подписаться в мой блог для большего контента!
Оригинал: “https://dev.to/wemake-services/typeclasses-in-python-3ma6”