LogicDaemon
LogicDaemon

Reputation: 570

Creating a generic typed Dict derivative

I'm trying to create a dict-like type with setdefault() in place of get(), with pre-defined default type. But it also should not be a pointer to same object all the time.

Problem is, I don't understand how to instantiate a var given a corresponding TypeVar.

Here is pseudo code:

F = TypeVar('F')
T = TypeVar('T')


class AutoExpandingDict(Dict[F, T]):

    def __getitem__(self, key: F) -> T:
        if key not in self:
            self[key] = new T()
        return super(AutoExpandingDict, self).__getitem__(key)

how do I make new T() work?

Expected use is:

stats = AutoExpandingDict[str, AutoExpandingDict[str, RequestTimer]]()

where RequestTimer is a class which records some statistics, then

def _api_request(method = 'GET', endpoint = None):
    ...
    with stats[method][endpoint]:
        ...
        
        response = self.session.request(method, f'{self.base_url}{api_path}{url_params_encoded}', ...)
        ...
    ...

There will be other uses aside from RequestTimer, and I don't want to copy-paste a lot of classes with just different names or repeat setdefault with magic parameters every time (if it will be a plain dict).

Upvotes: 0

Views: 574

Answers (1)

decorator-factory
decorator-factory

Reputation: 3083

Generic type parameters in Python are "erased" at runtime, so you can't access the value of T. Consider this:

A = TypeVar("A", covariant=True)

class Foo(Generic[A]):
    def __init__(self, something: A) -> None:
        self._something = something

    def do_something(self):
        [a_type] = get_params_somehow(self)
        print(a_type)

class X:
    pass

class Y(X):
    pass

foo1: Foo[X] = Foo(Y())
foo2: Foo[Y] = Foo(Y())

x = some().com().pu().ta() + tion()
foo3 = Foo(x)

foo1 an foo2 were created in the same way, and they can't access the information about their type (without resorting to hacks with inspect).

With foo3, the type of x will be inferred by a type checker, so foo3 can't really know what it is at runtime, and the type might be different with mypy, pyright, pyre etc. Again, the types (not runtime type, but the type hint stuff) are just "in the type checker's head".

If you only intend to construct the dict in the AutoExpandingDict[str, AutoExpandingDict[str, RequestTimer]]() manner, though, you could override __class_getitem__ or make a custom metaclass. But that is not very intuitive and probably overkill


What you might want to do is provide a factory, similar to collections.defaultdict:

class AutoExpandingDict(Dict[F, T]):
    def __init__(self, factory: Callable[[], T]):
        self._factory = factory

    def __getitem__(self, key: F) -> T:
        if key not in self:
            self[key] = self._factory()
        return super().__getitem__(key)

For example:

stats: AutoExpandingDict[str, AutoExpandingDict[str, RequestTimer]] = 
AutoExpandingDict(lambda: AutoExpandingDict(RequestTimer))

Maybe you just want a defaultdict?

Upvotes: 1

Related Questions