Is it possible to pass a constructor as a parameter? Yes, it is. There are multiple ways to instantiate a different class but passing a constructor from outside is one of the ways.
A function returns a differnt class
In some cases, two classes have the same function but return a different data type.
from typing import Iterator, Generic, Union, Type, TypeVar
class FooReturnType:
def __init__(self, value):
self.value = value
self.name = "FOO"
self.foo_val = 123
class BarReturnType:
def __init__(self, value):
self.value = value
self.name = "BAR"
self.bar_val = 998
class Foo:
def __init__(self, value: int):
self.value = value
def execute(self) -> FooReturnType:
return FooReturnType(self.value)
class Bar:
def __init__(self, value: int):
self.value = value
def execute(self) -> BarReturnType:
return BarReturnType(self.value)
def run1():
foo = Foo(10)
result = foo.execute()
print(f"{result.name}, {result.foo_val}, {result.value}")
# FOO, 123, 10
bar = Bar(20)
result = bar.execute()
print(f"{result.name}, {result.bar_val}, {result.value}")
# BAR, 998, 20
run1()
Of course, they can be used in this way.
Use Union type to restrict the class type
Let’s define a function that returns one of the classes. We can write it in the following way.
def create_instance(create: Union[Type[Foo], Type[Bar]]) -> Union[Foo, Bar]:
return create(30)
def run2():
foo = create_instance(Foo)
if isinstance(foo, Foo):
result = foo.execute()
print(f"{result.name}, {result.foo_val}, {result.value}")
# FOO, 123, 30
result = foo.execute()
# Cannot access member "foo_val" for type "BarReturnType"
# Member "foo_val" is unknown PylancereportGeneralTypeIssues
print(f"{result.name}, {result.foo_val}, {result.value}")
# FOO, 123, 30
run2()
First, pass the type of class. Then, restrict the possible data type by using Union
. create_instance
requires either Foo
or Bar
type and returns one of the instances.
To let IntelliSense that the instance has foo_val
, we have to check if it’s the instance of Foo
. Otherwise, Pylance shows an error.
Use Generic to use the same type for return type
To improve the previous version, we can use Generic instead. In this way, we can use the same data type both for the return type and the argument.
T = TypeVar("T", Foo, Bar)
def create_instance2(create: Type[T]) -> T:
return create(30)
def run3():
foo = create_instance2(Foo)
if isinstance(foo, Foo):
result = foo.execute()
print(f"{result.name}, {result.foo_val}, {result.value}")
# FOO, 123, 30
result = foo.execute()
print(f"{result.name}, {result.foo_val}, {result.value}")
# FOO, 123, 30
run3()
We don’t have to use isinstance
to read foo_val
because IntelliSense knows that create_instance2(Foo)
returns Foo
.
Why is it necessary in the first place?
I needed to implement the same logic for a different type when I used gRPC. The function was streaming function; thus the function needs to return the value as Iterator
. I put the common logic into a class. Let’s say FooBar
is the class that has the common logic. Service class has two functions that need respectively Foo
and Bar
.
class FooBar(Generic[T]):
def __init__(self, create: Type[T]) -> None:
self._create = create
def execute(self) -> Iterator[T]:
print("---Hello from FooBar")
for i in range(1, 3):
yield self._create(i + i)
class Service:
def execute_foofoo(self):
foobar = FooBar(Foo)
for foo in foobar.execute():
result = foo.execute()
# Use the specific property of the data
print(f"{result.name}, {result.foo_val}, {result.value}")
def execute_barbar(self):
foobar = FooBar(Bar)
for bar in foobar.execute():
result = bar.execute()
# Use the specific property of the data
print(f"{result.name}, {result.bar_val}, {result.value}")
Each function needs to read a different property but the same code can be written in the same place in this way.
Comments