Возможно ли типизированное неявное преобразование (принуждение) в Python 3.x?
Возможно ли реализовать пользовательское автоматическое / неявное преобразование (также называемое принуждением) в Python 3.6+, которое не сделает mypy
а другие статические анализаторы печальны? Примером может быть def(foo: A)
и дано def b_to_a(b: B) -> A
Есть ли способ, которым я мог бы написать foo(some_b)
(где some_b: B
) вместо foo(b_to_a(some_b))
?
Я думаю, что определенно есть несколько хороших способов сделать это в динамике Python (например, привязка членов к классам, которые включают конвертеры), или даже привязка таких преобразователей к самому объекту функции, чтобы он мог обрабатывать преобразование для выбранных типов., но мое текущее понимание типов Python заставляет меня думать, что это не удовлетворит mypy
и тому подобное.
Для сравнения посмотрите неявные преобразования Scala.
3 ответа
Вот реализация этой функции, которую я придумал. Мы храним словарь конвертеров с одной отправкой для типов, для которых мы знаем "неявные" преобразования. Мы добавляем конвертеры к этому, используя @implicit
декоратор.
Затем у нас есть @coerce
Декоратор, который может проверять аннотации функций во время выполнения, получать соответствующие конвертеры и применять преобразования. Ниже представлена структура:
from functools import wraps, singledispatch
from inspect import signature
from collections import OrderedDict
converters = {}
def implicit(func):
ret = func.__annotations__.get('return', None)
if not ret or len(func.__annotations__) != 2:
raise ValueError("Function not annotated properly or too many params")
if ret not in converters:
@singledispatch
def default(arg):
raise ValueError("No such converter {} -> {}".format(type(arg).__name__, ret.__name__))
converters[ret] = default
else:
default = converters[ret]
t = next(v for k, v in func.__annotations__.items() if k != 'return')
default.register(t)(func)
return wraps(func)(default)
def convert(val, t):
if isinstance(val, t):
return t
else:
return converters[t](val)
def coerce(func):
@wraps(func)
def wrapper(*args, **kwargs):
sig = signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
bound.arguments = OrderedDict(
(param, convert(val, sig.parameters[param].annotation))
for param, val in bound.arguments.items())
return func(*bound.args, **bound.kwargs)
return wrapper
И пример:
from typing import Tuple, Type
@implicit
def str_to_int(a: str) -> int:
return int(a)
@implicit
def float_to_int(a: float) -> int:
return int(a)
@coerce
def make_ints(a: int, b: int) -> Tuple[Type, Type]:
return (type(a), type(b))
print(make_ints("20", 5.0))
# (<class 'int'>, <class 'int'>)
Похоже, вы ищете что-то вроде типов протоколов, предложенных в PEP 544. Этот PEP еще не утвержден (и, возможно, еще не имеет полной реализации), поэтому может пройти некоторое время, прежде чем вы получите нужную вам функцию (Python 3.8 в ближайшее время).
В любом случае, согласно PEP, протоколы позволят вам описать своего рода абстрактный тип, основанный на том, какие методы и атрибуты у него есть, без конкретных типов, которые должны знать о протоколе или делать что-то конкретное (ему не нужно наследовать от абстрактный базовый класс, просто есть необходимые методы). Это похоже на то, как вы можете настроить, как isinstance
а также issubclass
работать с использованием метаклассов, но он работает со статической проверкой типов, не только во время выполнения.
Например, итераторы в Python - это существующий протокол, который реализуют многие несвязанные классы. Если PEP утвержден и реализован, вам не нужно объявлять пользовательский тип итератора как наследующий от typing.Iterator
больше, он бы выяснил это автоматически, просто потому, что класс __iter__
а также __next__
методы.
В вашем примере вы могли бы сделать A_Like
протокол, который требует to_A
метод:
class A_Like(typing.Protocol):
def to_A(self) -> A:
...
Тогда вы бы реализовать A.to_A
с простым return self
, в то время как B.to_A
делает соответствующее преобразование. Оба класса будут рассматриваться как соответствующие A_Like
тип протокола, так def foo(a: A_Like)
будет удовлетворять проверки типа (с телом класса нужно сделать a = a.to_A()
прежде чем звонить A
конкретные методы).
Вы можете сделать это сейчас с наследованием от общего абстрактного базового класса (который может быть простым миксином), но это определенно не так элегантно, как это будет с протоколами. Другой вариант, если вам не нужно преобразовывать много классов, это просто использовать Union
типы объявлений: def foo(a: Union[A, B])
Я не думаю, что это проблема конверсии. Но это похоже на проблему аннотации.
Во-первых, если foo
может справиться только A
как это можно было принять B
? И если foo
может справиться B
тоже, почему это должно только принять A
?
Во-вторых, если вы хотите аннотировать это foo
принимать A
или же B
, ты можешь использовать def(foo: Union[A, B])
,
Наконец, если вы имеете в виду B
должно иметь несколько методов, которые могут быть обработаны функцией, которая может обрабатывать только A
, Это все еще пример B
, Без правильной аннотации ваши статические анализаторы будут предупреждать вас.