Python How to pass a class constructor as an argument

eye-catch Python

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.

Sponsored links

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.

Sponsored links

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

Copied title and URL