Once you understand their purpose and power, playing with descriptors is a lot of fun.
I recently found myself needing to set an instance of a class as a class attribute on the class in question. I'm not crazy, I swear. It's for pynads' implementation of Monoids. However, doing this:
class Thing(object):
mempty = Thing()
Doesn't work. Wah wah wah. So my brain began turning, and I first tried this:
class Thing(object):
@classmethod
@property
def mempty(cls):
return cls()
Thing.mempty
At least that didn't blow up..., what if I swapped them?
class Thing(object):
@property
@classmethod
def mempty(cls):
return cls()
Thing.mempty
Well, that didn't work either. Hm. I had hoped that composing classmethod
and property
together would work and that Python would magically infer what I wanted. However, Python does no such thing, unsurprisingly. So, what if I made my own classproperty
descriptor?
class classproperty(object):
def __init__(self, method):
self.method = method
def __get__(self, _, cls):
return self.method(cls)
class Thing(object):
@classproperty
def mempty(cls):
return cls()
Thing.mempty
It works! Five lines of code plus another three to use it! Yeah! Except that three line use was repeated at least three times in pynads. I'm not sure about you, but I hate repeating myself. I'm gonna make a mistake somewhere (and did with List). Mainly because I was caching the instance like this:
class Thing(object):
_mempty = None
@classproperty
def mempty(cls):
if cls._mempty is None:
cls._mempty = cls()
return cls._mempty
assert Thing.mempty is Thing.mempty
That's great, but that's also six lines of boilerplate, which is one line longer than the implementation. ): After the benadryl wore off and I was able to think straight, what if I just moved the boiler plate into the descriptor?
class Instance(object):
def __get__(self, _, cls):
return cls()
class Thing(object):
mempty = Instance()
print(repr(Thing.mempty))
print(Thing.mempty is Thing.mempty)
Awesome. That's a much nicer use of it. The caching can also be moved into the descriptor too:
class Instance(object):
def __init__(self):
self._inst = None
def __get__(self, _, cls):
if self._inst is None:
self._inst = cls()
return self._inst
class Thing(object):
mempty = Instance()
assert Thing.mempty is Thing.mempty
And, if needed, a specific instance can be created and cached:
class Instance(object):
def __init__(self, *args, **kwargs):
self._inst = None
self.args = args
self.kwargs = kwargs
def __get__(self, _, cls):
if self._inst is None:
self._inst = cls(*self.args, **self.kwargs)
return self._inst
class Thing(object):
mempty = Instance(hello='world')
def __init__(self, hello):
print(hello)
Thing.mempty
And to show it's cached:
Thing.mempty
Ta-da! Setting an instance of a class on the class as a class attribute -- a sentence that is no less confusing to read now than before.
No comments:
Post a Comment