What if Python was a typed language? Well other than breaking all existing code. But what if it could act like a typed language when you want it to but retain the utility of a duck typed language otherwise?
PEP 484 introduced nice type annotations for Python 3.5+, and packages such as MyPy allow you to leverage these to detect errors prior to runtime.
What MyDiPy does is extend this functionality so that it actually works at runtime in a way you would expect. That means overloading functions, type checking on arguments, multiple dispatch, inheriting from parent classes, and even a unified casting framework.
This is a proof-of-concept project from a couple of weekends of experimenting. I have not used this in any other projects (yet). While it seems to work remarkably well, there are known issues that need resolving.
The first (extremely contrived) example highlights method overloading, multiple dispatch, and inheritance. It's basically a modification of one of the unit tests:
>>> from mydipy import OverloadObject, overload, inherit
>>>
>>> # Create an overloadable parent class
>>> class A(OverloadObject):
... def test(self, val: int) -> int:
... return -1*val
...
... def test(self, val: int) -> str:
... return "VALUE: -"+str(val)
...
>>> # In this class we set `auto_overload=False` so you need the @overload decorator
>>> class B(OverloadObject, auto_overload=False):
... @overload
... def test(self, val: str) -> str:
... return "B="+val
...
... @overload
... def test(self, val: int) -> str:
... return "B="+str(int)
...
... @overload
... def test(self, val):
... raise ValueError()
...
>>> # Now let's inherit from A and B and see what happens
>>> class C(A,B,auto_overload=True):
...
... def test(self, val: int) -> int:
... return -val*5
...
... # Here we want to go to Class A for any calls where val is an int and we want a str returned
... @inherit(A)
... def test(self, val: int) -> str: ...
...
... # Similarly we want to go to Class B for anything where val is a str
... @inherit(B)
... def test(self, val: str): ...
...
>>> ex = C()
>>> print(ex.test(1))
-5
>>> print(ex.test(1,_returns=str))
VALUE: -1
>>> print(ex.test('test'))
B=testAnd here's an example that shows how casting can work in practice:
>>> from mydipy import OverloadObject, cast, to, inherit
>>>
>>> class Currency(OverloadObject):
... """Generic parent Currency Class"""
... exchange_ratio = 0.0
... prefix = ''
...
... def __init__(self, value : float):
... # Value of our currency
... self.value = value
...
... def __str__(self):
... # Pretty form
... return self.prefix + str(self.value)
...
>>> class Dollar(Currency):
... """Dollar currency. Use this as the basis for all exchanges"""
... exchange_ratio = 1.0
... prefix = '$'
...
... # Let's convert this currency back into dollars so we can do exchanges
... def __cast__(self) -> Currency:
... return Dollar(self.value * self.exchange_ratio)
... # This will mean we will automatically use __str__, __int__, and __nonzero__ to convert to str, int, and bool respectively
... @inherit
... def __cast__(self): ...
...
... def __mul__(self, oth : Currency) -> Currency:
... # Convert both to Dollars to do addition
... if type(oth) != Dollar:
... oth = cast(Dollar,oth)
... if type(self) != Dollar:
... me = cast(Dollar,self)
... else:
... me = self
... return Dollar(me.value * oth.value)
...
>>> class Euro(Dollar):
... exchange_ratio = 1.21
... prefix = '€'
...
>>> # Define some amount of dollars and euros
>>> a = Dollar(5)
>>> b = Euro(3)
>>> # The next two are equivalent!
>>> print(cast(Dollar,b))
$3.63
>>> print(b -to>> Dollar)
$3.63
>>> # Automatically unit convert for addition
>>> print(a*b)
$18.15Seem interesting to you? Read on for more details.
See the module documentation for details and examples.
At a high level, MyDiPy includes:
OverloadObjectClass: This is what the rest of the module is built around. Any class which inherits from this may define method multiple times and watch the correct version be called@overloadDecorator: Used to specify which methods to overload whenauto_overload=FalseinOverloadObjectchild classes@inheritDecorator: Allows you to inherit method overloads from specific classes. Really only useful inOverloadObjectclasses
@type_checkDecorator: Used to enforce type checking based on function annotations before execution. Automatically applied to overload functions@no_type_checkDecorator: Explicitly flag a function as not being type checkedTypeCheckErrorError: Thrown whenever a type check fails on function/method invocation
castandtoFunctions: Cast anOverloadObject-based class with__cast__(self) -> <Class>methods defined to the target class.castis identical in principle to MyPy's functiontois the reverse version that also has an infix for-to>>meaningcast(str, a) == to(a, str) == (a -to>> str)
@OverloadFunctionDecorator: This is a decorator/class for overloading functions outside of class methodsTypedMetaMetaClass: This is a MetaClass which allows overloading, but does not have methods built in for casting. Supports theauto_overloadoption. Generally recommend usingOverloadObjectClass unless you have a specific reason not to.