Is it possible to implement custom automatic/implicit conversion (aka coercion) in Python 3.6+ that won't make mypy
and other static analyzers sad? An example would be a def(foo: A)
, and given def b_to_a(b: B) -> A
, is there a way I could potentially write foo(some_b)
(where some_b: B
) instead of foo(b_to_a(some_b))
?
I think there are definitely some good ways to do this in the dynamics of Python (tacking on members to classes that include converters, for instance), or even tacking on such converters to the function object itself so that it can handle conversion for selected types, but my current understanding of Python types makes me thing it would not satisfy mypy
and the like.
For comparison, see Scala's implicit conversions.
Many programming languages have something called type coercion; it's where the language will implicitly convert one object to another type of object in certain circumstances. Python does not have type coercion.
Implicit Type Conversion is automatically performed by the Python interpreter. Python avoids the loss of data in Implicit Type Conversion. Explicit Type Conversion is also called Type Casting, the data types of objects are converted using predefined functions by the user.
Type conversion is the process of converting a data type into another data type. Implicit type conversion is performed by a Python interpreter only. Explicit type conversion is performed by the user by explicitly using type conversion functions in the program code. Explicit type conversion is also known as typecasting.
For example, in 3+4.5, each argument is of a different type (one int, one float), and both must be converted to the same type before they can be added or it will raise a TypeError. Coercion between two operands can be performed with the coerce built-in function; thus, 3+4.5 is equivalent to calling operator.
Here's an implementation of this feature I came up with. We keep a dictionary of single-dispatch converters for types we know the "implicit" conversions for. We add converters to this using the @implicit
decorator.
We then have a @coerce
decorator that can inspect the function annotations at runtime, get the appropriate converters and apply the conversions. Below is the framework:
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
And an example:
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'>)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With