Error when using staticmethod with flyweight decorator

61 views Asked by At

I have a class for making sprites flyweight and I am using a decorator to call this class. Here is some code:

class flyweight:
    def __init__(self, cls):
        self._cls = cls
        self.__instances = dict()

    def __call__(self, title):
        return self.__instances.setdefault((title), self._cls(title))

In this question I'll just simplify the code to show what is relevant.

@flyweight
class Sprite:
    def __init__(self, title, surf=None):
        self.title = title
        self.surf = surf if surf is not None else pygame.image.load('Images/Sprites/'+title+'.png').convert_alpha()
        self.w, self.h = self.surf.get_size()

    @staticmethod
    def from_colour(colour, size=(40,40)):
        surf = pygame.Surface(size).convert(); surf.fill(colour)
        return Sprite(colour, surf)

red = Sprite.from_colour((125,0,0))

But this gives me the error:

AttributeError: 'flyweight' object has no attribute 'from_colour'

Should I remodel my flyweight implementation or is there some way around this?

2

There are 2 answers

1
abarnert On BEST ANSWER

A flyweight decorator really ought to pass through all constructors to the underlying class, including @classmethod and @staticmethod alternate constructors. In fact, more generally, a class decorator really ought to preserve the wrapped class's entire public interface.

And, while we could easily modify flyweight to specifically pass through the rest of the Sprite interface, which in this case is just that from_colour method, that would be a pain for a less trivial class, or for a class that ever changes. And really, what's the point of making a decorator that only works with a single class?

So, let's change it to:

  • Take any constructor signature. Ideally we'd want to make it configurable on what part of the signature counts as the key,1 but to keep things from getting too complicated, let's just fix it as the first argument.

  • Pass through the entire public interface of the class, not just its __call__ interface.

So:

class flyweight:
    def __init__(self, cls):
        self._cls = cls
        self.__instances = dict()

    def __call__(self, key, *args, **kw):
        return self.__instances.setdefault(key, self._cls(key, *args, **kw))

    def __getattr__(self, name):
        if not name.startswith('_'):
            return getattr(self._cls, name)

1. Some other library I've used has a nice design for this for function memo caches. Probably cachetools. And it ought to make just as much sense for class construction caches.

1
Ajax1234 On

Once decorated, the name of the wrapped object automatically points to the returned results of the decorator. In this case, Sprite now stores an instance of flyweight, which in turns contains an attribute storing an instance of the original wrapped class Sprite. For instance, printing Sprite after the declarations gives: <__main__.flyweight object at 0x102373080>. However, the staticmethod from_colour can be called from _cls:

red = Sprite._cls.from_colour((125,0,0))