5

I know I can do this:

import typing

T = typing.TypeVar("T")

class MyGenericClass(Generic[T]):
    def a_method(self):
        print(self.__orig_class__)

MyOtherGeneric[SomeBaseClass]().a_method()

to print SomeBaseClass. Probably, I will just stick with that ability to achieve what I am ultimately trying to do (modify functionality based on T), but I am now stuck wondering how all of this even works.

Originally, I wanted to access the base type information (the value of T) from inside the class at the time the object is being instantiated, or soon thereafter, rather than later in its lifecycle.

As a concrete example, in the code below, I wanted something to replace any of ?n? so I could get the value SomeOtherBaseClass early in the object's lifecycle. Maybe there's some code that needs to go above one of those lines, as well.

import typing

T = typing.TypeVar("T")

class MyOtherGenericClass(Generic[T]):
    def __init__(self, *args, **kwargs):
        print(?1?)
    
    def __new__(klass, *args, **kwargs):
        print(?2?)

MyOtherGenericClass[SomeOtherBaseClass]()

I was trying to set some instance variables at the time of instantiation (or, somehow, soon after it) based on the value of T. I'm rethinking my approach given that the typing module and, specifically, this stuff with generics, still seems to be in an unstable period of development.

So… Is that possible? A user pointed out that, at least in 3.8, __orig_class__ gets set during typing._GenericAlias.__call__, but how does that __call__ method get invoked? When does that happen?

Related reading:

Carter Pape
  • 1,009
  • 1
  • 17
  • 40

2 Answers2

1

I dunno, it looks like if you wanna have a Self type, which is a thing in PEP 673 in the upcomming Python 3.11.

The current workaround for the not yet implemented Self-Type is:

from typing import TypeVar

TShape = TypeVar("TShape", bound="Shape")

class Shape:
    def set_scale(self: TShape, scale: float) -> TShape:
        self.scale = scale
        return self


class Circle(Shape):
    def set_radius(self, radius: float) -> Circle:
        self.radius = radius
        return self

Circle().set_scale(0.5).set_radius(2.7)  # => Circle

Whereby the the self parameter and the return type are both hinted with the upper-bounded Type-Variable TShape instead of the Self Type.

Cobalius
  • 21
  • 2
1

You have two/three options, from dirtiest to dirty:

Hack typing.Generic mechanisms:

and inspect calling frames

import inspect
import types
from typing import Generic
from typing import Type
from typing import TypeVar

T = TypeVar('T')

class A(Generic[T]):
    _t: Type[T]
    def __class_getitem__(cls, key_t) -> type:
        g = typing._GenericAlias(cls, key_t)  # <-- undocumented
        return g
    
    def __new__(cls):
        prevfr = inspect.currentframe().f_back
        t = inspect.getargvalues(prevfr).locals['self'].__args__[0]
        o = super().__new__(cls)
        o._t = t
        return o

    def __init__(self) -> None:
        print(f"{self._t=}")

print(A[str]) # >>> "<types.A[str] at 0x11b52c550>"
print(A[str]()) # >>> "self._t=<class 'str'>"

Reify your generic class

T = TypeVar('T')

class A(Generic[T]):
    __concrete__ = {}
    _t: Type[T]
    def __class_getitem__(cls, key_t: type):
        cache = cls.__concrete__
        if (c := cache.get(key_t, None)): return c
        cache[key_t] = c = types.new_class(
            f"{cls.__name__}[{key_t.__name__}]", (cls,), {}, lambda ns: ns.update(_t=key_t))
        return c
    def __init__(self) -> None:
        print(f"{self._t=}")

A[str]()

Hide your shameful privates with metaclasses

T = TypeVar('T')

class AMeta(type):
    __concrete__ = {}
    def __getitem__(cls, key_t: type):
        cache = cls.__concrete__
        if (c := cache.get(key_t, None)): return c
        cache[key_t] = c = types.new_class(
            f"{cls.__name__}[{key_t.__name__}]", (cls,), {}, lambda ns: ns.update(_t=key_t))
        return c

class A(Generic[T], metaclass=AMeta):
    _t: Type[T]
    def __init__(self) -> None:
        print(f"{self._t=}")

A[str]()

Note that this add substantial instantiation overhead. Neither solution is elegant. Python sages wisely (IMHO, Typescript looking at ya') decided to implement typing support using existing Python constructs or creating as few new ones as possible. They are adding runtime support slowly, the focus being static typing. Thus

Using class_getitem() on any class for purposes other than type hinting is discouraged.

But the inherently dynamic nature of Python will triumph at the end and we'll got full runtime introspection in time.

Tested in Python 3.10.8

Fallible
  • 247
  • 1
  • 6