Mehran
Mehran

Reputation: 16831

Can I extend a generic type in Python?

This is what I want to do:

from typing import TypeVar, Generic  

T = TypeVar('T')

class Parent:
    def __init__(self) -> None:
        print("Parent's constructor called")
        self.value = 2

    def get(self):
        return self.value

class Child(T):
    def __init__(self):
        print("Child's constructor called")
        super().__init__()


ch = Child[Parent]()
print(ch.get())

But it gives me an error:

Traceback (most recent call last):
  File "/tmp/inherit.py", line 13, in 
    class Child(T):
  File "/home/.../.pyenv/versions/3.9.13/lib/python3.9/typing.py", line 640, in __init__
    self.__constraints__ = tuple(_type_check(t, msg) for t in constraints)
  File "/home/.../.pyenv/versions/3.9.13/lib/python3.9/typing.py", line 640, in 
    self.__constraints__ = tuple(_type_check(t, msg) for t in constraints)
  File "/home/.../.pyenv/versions/3.9.13/lib/python3.9/typing.py", line 166, in _type_check
    raise TypeError(f"{msg} Got {arg!r:.100}.")
TypeError: TypeVar(name, constraint, ...): constraints must be types. Got (~T,).

Apparently, this is the right way of writing that code:

from typing import TypeVar, Generic  

T = TypeVar('T')

class Parent:
    def __init__(self) -> None:
        print("Parent's constructor called")
        self.value = 2

    def get(self):
        return self.value

class Child(Generic[T]): # <-- "Generic" was added here
    def __init__(self):
        print("Child's constructor called")
        super().__init__()


ch = Child[Parent]()
print(ch.get())

But this one also errors out:

Child's constructor called
Traceback (most recent call last):
  File "/tmp/inherit.py", line 20, in 
    print(ch.get())
AttributeError: 'Child' object has no attribute 'get'

Which is totally expected. Now my question is whether it is possible to extend a generic type in Python at all or not? If it is, how?

Upvotes: 3

Views: 3826

Answers (1)

Ahmed AEK
Ahmed AEK

Reputation: 17526

Python has multiple inheritance for this purpose.

class Child(Parent, Generic[T]):

The Generic is only a type-hint, it doesn't affect the interpreter behaviour, you still have to inherit Parent.

Edit: there's also composition

class Child(Generic[T]):
    def __init__(self, parent: T):
        print("Child's constructor called")
        self.parent: T = parent

    def get(self):
        return self.parent.get()

ch = Child[Parent](Parent())
print(ch.get())

Edit2: you can override the __getattr__ method to enable child to reuse parent functions in composition, but you cannot type-hint this easily, and it won't be pythonic.

class Child(Generic[T]):
    def __init__(self, parent: T):
        print("Child's constructor called")
        self.parent: T = parent

    def __getattr__(self, item: str):
        try:
            return super().__getattr__(item)
        except AttributeError:
            return getattr(self.parent, item)

ch = Child[Parent](Parent())
print(ch.get())

Edit3: as mentioned by @deceze you can also create new classes on the fly using type but the type-hints will need some work.

ch = type('Child', (ChildClass, ParentClass), {})()

usage as docs:

from typing import TypeVar, Generic

T = TypeVar('T')

class Parent:
    def __init__(self) -> None:
        print("Parent's constructor called")
        self.value = 2

    def get(self):
        print("Parent's get called")
        return self.value

class Child:
    def __init__(self):
        print("Child's constructor called")
        super().__init__()

    def set(self, v):
        print("Child's set called: ", v)
        self.value = v

ch = type('Child', (Child, Parent), {})()
print(ch.get())
ch.set(3)
print(ch.get())

which outputs (as expected):

Child's constructor called
Parent's constructor called
Parent's get called
2
Child's set called:  3
Parent's get called
3

Upvotes: 2

Related Questions