Возможно ли типизированное неявное преобразование (приведение) в 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.


person bbarker    schedule 19.06.2018    source источник
comment
Какие классы A и B? Как работает конвертация? Вы могли бы сделать это, написав декоратор, который отправляет все входные данные правильному конвертеру, возможно, через какой-то общая функция одиночной отправки   -  person Patrick Haugh    schedule 19.06.2018
comment
Любой тип класса. Идея состоит в том, что пользователь предоставленной функции, которая принимает значение типа A, может написать неявный преобразователь для любого типа T, который будет автоматически вызываться по мере необходимости, пока преобразователь находится в области видимости (по крайней мере, так это работает в Scala). )   -  person bbarker    schedule 19.06.2018
comment
Нет, python не предлагает ничего подобного. Вы можете написать что-нибудь для достижения аналогичного эффекта, но это создаст гораздо больше кода, чем несколько вызовов b_to_a. В зависимости от того, какую операцию вы хотите выполнить с объектом, вы можете использовать утиную типизацию, чтобы оснастить определенные классы для выполнения этой операции, а затем использовать Абстрактный базовый класс для описания этой группы классов.   -  person Patrick Haugh    schedule 19.06.2018


Ответы (3)


Вот реализация этой функции, которую я придумал. У нас есть словарь конвертеров single-dispatch для известных нам типов. «неявные» преобразования для. Мы добавляем к этому преобразователи с помощью декоратора @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'>)
person Patrick Haugh    schedule 19.06.2018
comment
Это удивительно (напоминает мне размышления в стране Java), нужно будет изучить это и попробовать. (Но если протоколы будут реализованы, в конце концов я могу предпочесть их для моего конкретного случая использования). - person bbarker; 19.06.2018

Я не думаю, что это проблема конвертации. Но это похоже на проблему с аннотацией.

Во-первых, если foo может обрабатывать только A, как он может принять B? И если foo может обрабатывать и B, то почему он должен принимать только A?

Во-вторых, если вы хотите отметить, что foo принимает A или B, вы можете использовать def(foo: Union[A, B]).

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

person Sraw    schedule 19.06.2018
comment
По сути, я хотел бы использовать понятие классов типов из Haskell, которые более гибки, чем наследование. - person bbarker; 19.06.2018

Похоже, вы ищете что-то вроде типов протоколов, предложенных в 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])

person Blckknght    schedule 19.06.2018
comment
статическая утиная типизация на самом деле является своего рода тем, как я думаю о классах типов, хотя я еще не уверен, являются ли они точным совпадением. +1, приятно видеть это в пути (надеюсь). На самом деле, это даже лучше, чем то, на что я надеялся, так как я просто искал неявное принуждение как способ сделать статическое утиное типирование/классы типов! - person bbarker; 19.06.2018
comment
Похоже, я могу пойти дальше и без проблем использовать пакет typing_extensions из pypi (или просто добавить файл в свой проект из репозитория github) в Python 3.6.5 без проблем (пока). Конечно, честное предупреждение, стандарт может измениться или даже не быть принят, как вы заметили. - person bbarker; 20.06.2018