Search code examples
pythongenericstypesinstantiationtype-hinting

How may I instantiate a class of type hint in python?


I am new to python and am coming from C# and java. I want to instantiate a class of the type provided as type hint R as following

from typing import (TypeVar, Generic)

class BaseParams(object):
     def __init__(self) -> None:
          self.name = 'set-in-base-class'

class ChildParams(BaseParams):
     def __init__(self) -> None:
          super().__init__()
          self.name = 'set-in-child-class'

R = TypeVar('R', bound= BaseParams)

class MyGeneric(Generic[R]):
     def __init__(self) -> None:
          super().__init__()
     def test(self):
          r = R() # how should I instantiate R here
          print(r.name)

c = MyGeneric[ChildParams]()
c.test()

something like the following C# code

class BaseParams
{
    public BaseParams()
    {
        Name = "set-in-base-class";
    }
    public string Name { get; set; }
}
class ChildParams : BaseParams
{
    public ChildParams()
    {
        Name = "set-in-child-class";
    }
}
class MyGenericClass<R> where R : BaseParams, new()
{
    public void test()
    {
        var r = new R();
        Console.WriteLine(r.Name);
    }
}

I've made quite a lot search on how to do that in python and all the sources refer to a situation where we provide the type in a method or something like that. I wonder if it is possible to do that at all.

would you please someone help me to have a workaround on this?


Solution

  • It's still type hints and not type declaration, but I like inspecting weird objects and looking at their insides, so I gave it a go:

    I tried to inspect Generics in interactive session using dir and also looked for clues in the typing's source.

    My testing consisted of having one MyGeneric without R specified and one with, then inspecting them both. Both of them had __orig_bases__ which included Generic[R], but then I noticed that the parametrized MyClass has additional __orig_class__ attribute - bingo!

    >>> a = MyGeneric()
    >>> b = MyGeneric[int]()
    >>> a.__orig_bases__
    (typing.Generic[~R],)
    >>> b.__orig_
    b.__orig_bases__   b.__orig_class__(  
    >>> b.__orig_bases__
    (typing.Generic[~R],)
    >>> a.__orig_class__
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'MyGeneric' object has no attribute '__orig_class__'. Did you mean: '__orig_bases__'?
    >>> b.__orig_class__
    __main__.MyGeneric[int]
    

    By looking at Generic's source earlier, I already knew that .__args__ parameter gets whatever is in [], as tuple (because it could be multiple things).

    So:

    >>> b.__orig_class__.__args__
    (<class 'int'>,)
    >>> b.__orig_class__.__args__[0]
    <class 'int'>
    

    Going back to your code I had in file, I put that attribute chain and run the file - and got set-in-child-class.

    from typing import (TypeVar, Generic)
    
    class BaseParams(object):
         def __init__(self) -> None:
              self.name = 'set-in-base-class'
    
    class ChildParams(BaseParams):
         def __init__(self) -> None:
              super().__init__()
              self.name = 'set-in-child-class'
    
    R = TypeVar('R', bound= BaseParams)
    
    class MyGeneric(Generic[R]):
         def __init__(self) -> None:
              super().__init__()
         def test(self):
              r = self.__orig_class__.__args__[0]()
              print(r.name)
    
    c = MyGeneric[ChildParams]()
    c.test()
    

    I'd advise to put that magical chain of attributes as some function with nice name. Like get_parametrized_base_arg or something. Or just put a good comment to explain the magic.