Statistic Dean
Statistic Dean

Reputation: 5270

Is it possible to override __getitem__ at instance level in Python?

With the following code :

import types

class Foo():
    def __getitem__(self, x):
        return x

def new_get(self, x):
    return x + 1

x = Foo()
x.__getitem__ = types.MethodType(new_get, x)

x.__getitem__(42) will return 43, but x[42] will return 42.

Is there a way to override __getitem__ at instance level in Python?

Upvotes: 15

Views: 9314

Answers (4)

blhsing
blhsing

Reputation: 106435

You can make __getitem__ a descriptor that would first try to return the same-named attribute of a given instance before defaulting to binding the original method to the instance:

class customizable:
    def __init__(self, method):
        self.method = method

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, obj_type=None):
        if obj:
            try:
                return vars(obj)[self.name]
            except KeyError:
                return self.method.__get__(obj, obj_type)
        return self.method

so that:

import types

class Foo():
    @customizable
    def __getitem__(self, x):
        return x

def new_get(self, x):
    return x + 1

x = Foo()
y = Foo()
x.__getitem__ = types.MethodType(new_get, x)
print(Foo.__getitem__)
print(x[42])
print(y[42])

outputs:

<function Foo.__getitem__ at 0x154f7e55c310>
43
42

Demo: https://ideone.com/fLjlWD

Note that if you don't own the code of Foo you can either patch its __getitem__ method with the descriptor:

Foo.__getitem__ = customizable(Foo.__getitem__) # would not work on built-in types

or override __getitem__ in a subclass of Foo instead:

class Bar(Foo):
    __getitem__ = customizable(Foo.__getitem__)

Upvotes: 2

Charlie Parker
Charlie Parker

Reputation: 5201

I ended up having to do something stupid like this just making a new object, calls the old __getitem__ and does something different:

class USLDatasetFromL2L(datasets.Dataset):

    def __init__(self, original_l2l_dataset: datasets.Dataset):
        self.original_l2l_dataset = original_l2l_dataset
        self.transform = self.original_l2l_dataset.transform
        self.original_l2l_dataset.target_transform = label_to_long
        self.target_transform = self.original_l2l_dataset.target_transform

    def __getitem__(self, index: int) -> tuple:
        """ overwrite the getitem method for a l2l dataset. """
        # - get the item
        img, label = self.original_l2l_dataset[index]
        # - transform the item only if the transform does exist and its not a tensor already
        # img, label = self.original_l2l_dataset.x, self.original_l2l_dataset.y
        if self.transform and not isinstance(img, Tensor):
            img = self.transform(img)
        if self.target_transform and not isinstance(label, Tensor):
            label = self.target_transform(label)
        return img, label

    def __len__(self) -> int:
        """ Get the length. """
        return len(self.original_l2l_dataset)

Upvotes: 0

Olivier Melan&#231;on
Olivier Melan&#231;on

Reputation: 22294

Don't do it...

The item lookup protocol will always recover __getitem__ from the class, it will not even look at instance __dict__. This is actually a good thing in general as doing otherwise would allow instances of the same class to be conceptually different from one another, which goes against the whole idea behind classes.

But...

Nonetheless, there are situation where this could potentially be helpful, by example when monkey-patching for test purpose.

Because the dunder is looked up directly at class level, the item lookup logic must also be updated at the class level.

A solution is thus to update __getitem__ so that it first looks for an instance-level function in the instance __dict__.

Here is an example where we are subclassing dict to allow for instance-level __getitem__.

class Foo(dict):
    def __getitem__(self, item):
        if "instance_getitem" in self.__dict__:
            return self.instance_getitem(self, item)
        else:
            return super().__getitem__(item)

foo = Foo()
foo.instance_getitem = lambda self, item: item + 1
print(foo[1]) # 2

Upvotes: 5

ruohola
ruohola

Reputation: 24018

This is unfortunately, and quite surprisingly, not allowed:

For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary.

Source: https://docs.python.org/3/reference/datamodel.html#special-lookup

Upvotes: 11

Related Questions