1

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?

O Nichols
  • 23
  • 5

2 Answers2

1

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))
Ajax1234
  • 69,937
  • 8
  • 61
  • 102
  • While this works,I don't think it's a great answer. Obviously `from_colour` was supposed to be part of the public interface of `Sprite`. But instead, you now have to call it via a private attribute. Worse, a private attribute of a class that users of `Sprite` shouldn't have to know about. Especially since you ought to be able to drop `@flyweight` in and take it back out without changing anything but performance, and you definitely can't if it obscures the interface of the decorated class. – abarnert Jun 23 '18 at 00:53
0

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.

abarnert
  • 354,177
  • 51
  • 601
  • 671