Assigning (instead of defining) a __getitem__ magic method breaks indexing [duplicate]

Special methods (essentially anything with two underscores on each end) have to be defined on the class. The internal lookup procedure for special methods completely skips the instance dict. Among other things, this is so if you do

class Foo(object):
    def __repr__(self):
        return 'Foo()'

the __repr__ method you defined is only used for instances of Foo, and not for repr(Foo).


You can actually solve this by creating a new class for every type. If you want this to work transparently, __new__ is the place for it.

import weakref


class BigWrap(object):
    def __new__(cls, wrapped):
        wrapped_type = type(wrapped)
        print('Wrapping %s (%s)' % (wrapped, wrapped_type))
        # creates a new class, aka a new type
        wrapper_class = type(  # new_class = type(class name, base classes, class dict)
            '%s_%s_%d' % (cls.__name__, wrapped_type.__name__, id(wrapped)),  # dynamic class name
            (
                cls,  # inherit from wrap to have all new methods
                wrapped_type,  # inherit from wrap_type to have all its old methods
            ),
            {
                '__getitem__': wrapped.__getitem__,  # overwrite __getitem__ based on wrapped *instance*
                '__new__': wrapped_type.__new__,  # need to use wrapped_type.__new__ as cls.__new__ is this function
            })
        cls._wrappers[wrapped_type] = wrapper_class  # store wrapper for repeated use
        return cls._wrappers[wrapped_type](wrapped)

    # self is already an instance of wrap_<type(wrapped)>
    def __init__(self, wrapped):
        self.__wrapped__ = wrapped

Initial "solution":

import weakref
class wrap(object):
  _wrappers = weakref.WeakValueDictionary()  # cache wrapper classes so we don't recreate them

  def __new__(cls, wrapped):
    wrapped_type = type(wrapped)
    print('Wrapping %s (%s)' % (wrapped, wrapped_type))
    try:
      return object.__new__(cls._wrappers[wrapped_type])  # need to use object.__new__ as cls.__new__ is this function
    except KeyError:
      print('Creating Wrapper %s (%s)' % (wrapped, wrapped_type))
    # creates a new class, aka a new type
    wrapper_class = type(  # class name, base classes, class dict
      '%s_%s' % (cls.__name__, wrapped_type.__name__),  # dynamic class name
      (cls,),  # inherit from wrap to have all its method
      {'__getitem__': wrapped_type.__getitem__})  # overwrite __getitem__ based on wrapped class
    cls._wrappers[wrapped_type] = wrapper_class  # store wrapper for repeated use
    return cls._wrappers[wrapped_type](wrapped)

  # self is already an instance of wrap_<type(wrapped)>
  def __init__(self, wrapped):
    self._data = wrapped

Be careful however! This will do what you want - use the wrapped class' __getitem__. However, this doesn't always make sense! For example, list.__getitem__ is actually built into CPython's CAPI and not applicable to other types.

foo = wrap([1,2,3])
print(type(foo))  # __main__.wrap_list
foo[2]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-31-82791be7104b> in <module>()
----> 1 foo[2]

TypeError: descriptor '__getitem__' for 'list' objects doesn't apply to 'wrap_list' object