Make a Python function subscriptable

Viewed 112

for the purposes of a completely contrived programming exercise, I want to be able to access all objects that I have created of a specific class, using decorators, and by subscripting the class itself.

In the below example, I have created an instance of my A class, using the argument 1, and stored the result in a dictionary, using the key '1'.

So, by attempting to index the A class directly, it should return the instance of the object that was created, however, it only returns the parameter passed to __get__item, and raises a TypeError if I attempt to index it directly with A['1'].

import types


def remember(cl):
    seen = dict()

    def _remember(*args, **kwargs):
        key = ','.join(map(str, args))
        if key not in seen:
            seen[key] = cl(*args, **kwargs)
        return seen[key]

    _remember.__getitem__ = types.MethodType(seen.get, _remember)
    return _remember


@remember
class A(object):
    def __init__(self, x):
        pass


a = A(1)
b = A.__getitem__('1')
print(a == b) #should print true, but instead, A.__get__item returns '1'. Ideally, A['1'] would work.
1 Answers

Here's what I came up with.

We can't have A[<something>] as possible usage because that would mean changing the attributes of type. I've created A.get(<something>) instead for that use case.

This differs from your attempt in that rather than returning a function from the decorator, we return a new class that extends from the original. That class stores all instances of instantiations of its parent class, and indexes them by a key which is created from the args and kwargs.

The __getitem__ has been overridden on the class, and then in the parent class, and it takes any of:

  • A tuple of args (tuple) and kwargs (dict)
  • Args as a tuple
  • A single arg

Anyway, on with the code:

from typing import Any, Dict


def remember(klass):
    def get_key(args, kwargs):
        return hash((args, tuple(sorted(kwargs.items()))))

    class RememberedClass(klass):
        _instances: Dict[int, Any] = {}

        def __new__(cls, *args, **kwargs):
            c = klass(*args, **kwargs)
            klass.__getitem__ = cls.__getitem__

            key = get_key(args, kwargs)
            cls._instances[key] = c
            return c

        @staticmethod
        def get(*args, **kwargs) -> klass:
            key = get_key(args, kwargs)
            return RememberedClass._instances[key]

        def __getitem__(self, item) -> klass:
            if isinstance(item, tuple) and len(item) == 2:
                # (args, kwargs)
                args, kwargs = item
                key = get_key(args, kwargs)
            elif isinstance(item, tuple):
                # (arg, arg, ...)
                key = get_key(item, {})
            else:
                # arg
                key = get_key((item,), {})
            return RememberedClass._instances.get(key)

    return RememberedClass


@remember
class Foo:
    def __init__(self, val):
        pass

I believe this is what you wanted?

>>> f = Foo(1)

>>> g = Foo(2)

>>> f is g
False

>>> f is f[1]
True

>>> g is g[2]
True

>>> g is f[2]
True

>>> f is Foo.get(1)
True

>>> type(f)
__main__.Foo

>>> Foo
__main__.remember.<locals>.RememberedClass
Related