Возможно ли типизированное неявное преобразование (принуждение) в 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, Без правильной аннотации ваши статические анализаторы будут предупреждать вас.

Другие вопросы по тегам