How do you build custom data types in Python (similar to how you define interface
in TypeScript)?
Let's say for example I have a class which accepts two input arguments. Both are optional, but one of them is required to initialize the class.
I can do something like:
@dataclass
class Interface:
a: Optional[str] = None
b: Optional[int] = None
def __post_init(self):
if self.a is None and self.b is None:
raise ValueError('one of a or b is required')
Now I want to define a function which accepts an argument of the type Interface
:
def func(i: Interface) -> Interface:
return i
Of course, this works if I invoke:
func(Interface('abc')) # works
func(Interface(2)) # this does not work!
Is it possible to call the function similar to how you would initialize the Interface
class? E.g.:
func('abc') # should work
func(2) # should also work
func([1, 2]) # should report a type issue
The reason for doing this is that I have to make extensive use of the Interface
"type", and would like to avoid writing the types everywhere.
CodePudding user response:
Your current definition still requires the arguments to match properly with the generated __init__
, which has a form like:
def __init__(self, a: Optional[str] = None, b: Optional[int] = None):
...
When you do func('abc')
it works because it's like invoking __init__
with 'abc'
for a
and None
for b
, so the types match. When you do func(2)
, it's still trying to pass 2
to a
, not b
, so the types don't match and you get a (correct) error.
As currently, you must replace func(2)
with func(b=2)
or func(None, 2)
.
If you want to do it with a single positional argument that can be either, you either:
Have to store both in the same field, e.g.:
@dataclass class Interface: a: Optional[Union[str, int]] = None # On modern Python, a: str | int | None = None would work # No postinit needed
with the downside being that now you don't know which is stored there, and have to check each time, or...
Manually write
__init__
instead of relying ondataclasses
to generate it, so it accepts a single value and stores it in the correct field:@dataclass class Interface: a: Optional[str] = None b: Optional[int] = None def __init__(self, ab: Optional[Union[str, int]] = None): # Or cleaner 3.10 syntax: def __init__(self, ab: str | int | None = None): self.a = self.b = None if isinstance(ab, str): self.a = ab elif isinstance(ab, int): self.b = ab else: raise TypeError(f"Only str or int accepted, received {type(ab).__name__}") # No postinit needed
CodePudding user response:
from dataclasses import dataclass
from typing import Optional
@dataclass
class Interface:
a: Optional[str] = None
b: Optional[int] = None
def __post_init__(self):
if self.a is None and self.b is None:
raise ValueError('one of a or b is required')
First issue, is you have your __post_init__
written as __post_init
. If it is not written correctly, then it won't run after the class is instantiated.
Secondly, I am not sure the purpose of the def func
, so I have ignored it for my examples.
Thirdly, the way the dataclass works is when instantiated, the first parameter is mapped to the first property in the class, unless you override it with explicit parameter names. Therefore in order to instantiate a class with just an int, you should do Interface(None, 3)
or Interface(b=3)
.
Therefore this is what will happen:
Interface("abc")
# works with just a string
Interface("abc", 3)
# works with both
Interface(b=3)
# works with just an int
Interface()
# raises an error as expected
Interface(None, None)
# raises an error as expected
Interface(a=None)
# raises an error as expected
Interface(b=None)
# raises an error as expected
Its worth mentioning that you will not have a warning that either of these parameters are required
However, in case you need this code to work, I have made a basic version of what you were trying to use:
class Interface:
def __init__(self, *args):
if len(args) == 0:
raise ValueError("a string or integer is required")
self.a = None
self.b = None
for value in args:
if type(value) == str:
self.a = value
elif type(value) == int:
self.b = value
This code takes an infinite amount of arguments, and creates a list of all of them. It then loops through the list, assigning the data to the corresponding variable. Although if multiple values of the same data type are used as arguments, the last argument of that type will be used.
e.g. If we use x = Interface("a", "b")
, x.a
will equal "b"