0

I have given up memoization of a class as a bag-of-worms that I didn't want to explore and here is one example of why. The question I ask is "how does one extend or inherit from a memoized class" but it's very possible I have made a mistake. The memoize class below is a cut-down version of the one by brandizzi in How can I memoize a class instantiation in Python? and googling the subject finds more involved such classes.

class memoize(object):
    def __init__(self, cls):
        self.cls = cls
        # I didn't understand why this was needed
        self.__dict__.update(cls.__dict__)

        # bit about static methods not needed

    def __call__(self, *args):
        try:
            self.cls.instances
        except:
            self.cls.instances = {}    
        key = '//'.join(map(str, args))
        if key not in self.cls.instances:
            self.cls.instances[key] = self.cls(*args)
        return self.cls.instances[key]

class Foo():
    def __init__(self,val):
        self.val = val

    def __repr__(self):
        return "{}<{},{}>".format(self.__class__.__name__,self.val,id(self))

class Bar(Foo):
    def __init__(self,val):
        super().__init__(val)

f1,f2,f3 = [Foo(i) for i in (0,0,1)]
print([f1,f2,f3])
b1,b2,b3 = [Bar(i) for i in (0,0,1)]
print([b1,b2,b3])

# produces exactly what I expect
# [Foo<0,3071981964>, Foo<0,3071982092>, Foo<1,3071982316>]
# [Bar<0,3071983340>, Bar<0,3071983404>, Bar<1,3071983436>]

Foo = memoize(Foo)
f1,f2,f3 = [Foo(i) for i in (0,0,1)]
print([f1,f2,f3])
b1,b2,b3 = [Bar(i) for i in (0,0,1)]
print([b1,b2,b3])

# and now Foo has been memoized so Foo(0) always produces the same object
# [Foo<0,3071725804>, Foo<0,3071725804>, Foo<1,3071726060>]
# [Bar<0,3071711916>, Bar<0,3071711660>, Bar<1,3071725644>]

# this produces a compilation error that I don't understand

class Baz(Foo):
    def __init__(self,val):
        super().__init__(val)

# Traceback (most recent call last):
#   File "/tmp/foo.py", line 49, in <module>
#     class Baz(Foo):
# TypeError: __init__() takes 2 positional arguments but 4 were given
Haydon Berrow
  • 485
  • 2
  • 6
  • 14
  • This "recipe" is indeed a very bad idea - once you rebind `Foo` to `memoize(Foo)`, `Foo` is a `memoize` instance and not class `Foo` anymore. This breaks all expectations wrt/ python's `type` and the whole object model. – bruno desthuilliers Jan 31 '18 at 16:01

1 Answers1

0

This "recipe" is indeed a very bad idea - once you rebind Foo to memoize(Foo), Foo is a memoize instance and not class Foo anymore. This breaks all expectations wrt/ python's type and the whole object model. In this case, it about how the class statement works. Actually, this:

class Titi():
    x = 42
    def toto(self):
        print(self.x)

is syntactic sugar for:

def toto(self):
    print(self.x)
Titi = type("Titi", (object,), {x:42, toto:toto})
del toto

Note that this happens at runtime (like everything in Python except parsing / bytecode compilation), and that type is a class so calling type creates a new class which is a type instance (this is named a 'metaclass' - the class of a class - and type is the default metaclass).

So with Foo being now a memoize instance instead of a Type instance, and since memoize is not a proper metaclass (it's __init__ methods signature is incompatible), the whole thing just cannot work.

To get this to work, you'd have to make memoize a proper metaclass (this is a simplified example assuming a single arg named param but it can be generalized if you want to):

class FooType(type):
    def __new__(meta, name, bases, attrs):
        if "_instances" not in attrs:
            attrs["_instances"] = dict()
        return type.__new__(meta, name, bases, attrs)

    def __call__(cls, param):
        if param not in cls._instances:
            cls._instances[param] = super(FooType, cls).__call__(param)
        return cls._instances[param]


class Foo(metaclass=FooType):
    def __init__(self, param):
        self._param = param
        print("%s init(%s)" % (self, param))

    def __repr__(self):
        return "{}<{},{}>".format(self.__class__.__name__, self._param, id(self))


class Bar(Foo):
    pass


f1,f2,f3 = [Foo(i) for i in (0,0,1)]
print([f1,f2,f3])
b1,b2,b3 = [Bar(i) for i in (0,0,1)]
print([b1,b2,b3])
bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118