I am porting a project from Java to Python and there is a class with multiple constructors. I'm trying to port that same idea to python, in a pythonic way. I've recently been informed of the typing.overload decorator, but I can't seem to coerce the code to behave the way I wish. For example, if a Java class had the following constructor signatures:
public Foo(){}
public Foo(int number) {}
publid Foo(String b, float number) {}
public Foo(float number) {}
In Python, I tried to replicate that behavior with the following class definition:
class Foo():
@typing.overload
def __init__(self) -> None:
...
@typing.overload
def __init__(self, number: int) -> None:
...
@typing.overload
def __init__(self, string: str, number: float) -> None:
...
@typing.overload
def __init__(self, number: float) -> None:
...
def __init__(self, string: str = None, number: typing.Union[int,float] = None) -> None:
if isinstance(string, str):
print(f'String string: {string}')
elif isinstance(string, int):
print(f'String int: {string}')
elif isinstance(string, float):
print(f'String float: {string}')
elif isinstance(string, bool):
print(f'String bool: {string}')
else:
print(f'String None')
if isinstance(number, str):
print(f'Number string: {number}')
elif isinstance(number, int):
print(f'Number int: {number}')
elif isinstance(number, float):
print(f'Number float: {number}')
elif isinstance(number, bool):
print(f'Number bool: {number}')
else:
print(f'Number None')
if __name__ == '__main__':
test1 = TestClass(1.0)
print(f'\n')
test2 = TestClass(6)
print(f'\n')
test3 = TestClass('Test 3', 3.0)
print(f'\n')
test4 = TestClass('Test 4', True)
With the four instantiations, what I was expecting to see was:
String None
Number float: 1.0
String None
Number int: 6
String string: Test 3
Number float: 3.0
String string: Test 4
Number bool: True
What I got was:
String float: 1.0
Number None
String int: 6
Number None
String string: Test 3
Number float: 3.0
String string: Test 4
Number int: True
I realize that I won't necessarily get a 1:1 ability with Java's strict typing and Python's overloads only being there for type-checkers and no runtime enforcement. I just seem to be fundamentally missing something with how to actually implement an overloaded init the way I was envisioning.
Firdt thing to have in mind: typing in Python is an optional step,which runs prior to the code being actually compiled, and have no (direct) influence on it (although one's CI'pipeline or commit hooks might force typing to work - I'd advise otherwise).
So, despite typing, Python is essentialy a dynamic language. It can find out about the argument you passed if you expliced pass it as a named argument. Otherwise it will pick the arguments in order.
Otherwise, you put the guard/converting code as code to do a runtime check and assignment of your argument, before running the function body.
There are ways to create decorators for disatching a call to different functions as well - and that might keep your code 1:1 more like the java code (with several redundant implementations of the same function). The standarlibrary contains the functools.singledispatchmethod decorator which can do that for a single parameter. AFAIK you have to use a 3rdy party package, or roll your own decorator if you need to that for more than one argument.
And finally, reiterating, and most important: unnamed arguments are served to a function in the order they are passed, regardless of type. In your example, the first argument will always end up in the parameter named string. However, order can be enforced by simply naming the arguments when calling a method, for example TestClass(number=1.0) will leave string with the default value of None and the 1.0 will be used for the number parameter.
All in all, and typing.overload apart, the "Pythonic" way of getting either a string, a float or an int as the first argument for a method is to check and cast if needed the argument at runtime:
class Foo:
def __init__(self, argument: str|int|float|None = None) -> None:
match argument:
case str:
string = argument
number = None
case int|float:
number = argument
string = None
case None:
number = string = None
case _:
raise TypeError()
# the body with your ifs and prints can then
# be kept as you were experimenting:
if isinstance(string, str):
print(f'String string: {string}')
...
Even the use of typing.overload is just to make more complex kinds of parameter acceptable types more visible: it has no effect at runtime, as said, but also, no effect and no advantage at type-checking time, over a simple type-union as in this example. If it can makes things more readable, its nice to use it. If it just adds unneeded boilerplate, as in your example, you are just using Python as if it where Java, and it is not.
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