In python I can add a method to a class with the @classmethod
decorator. Is there a similar decorator to add a property to a class? I can better show what I'm talking about.
class Example(object):
the_I = 10
def __init__( self ):
self.an_i = 20
@property
def i( self ):
return self.an_i
def inc_i( self ):
self.an_i += 1
# is this even possible?
@classproperty
def I( cls ):
return cls.the_I
@classmethod
def inc_I( cls ):
cls.the_I += 1
e = Example()
assert e.i == 20
e.inc_i()
assert e.i == 21
assert Example.I == 10
Example.inc_I()
assert Example.I == 11
Is the syntax I've used above possible or would it require something more?
The reason I want class properties is so I can lazy load class attributes, which seems reasonable enough.
Here's how I would do this:
The setter didn't work at the time we call
Bar.bar
, because we are callingTypeOfBar.bar.__set__
, which is notBar.bar.__set__
.Adding a metaclass definition solves this:
Now all will be fine.
If you only need lazy loading, then you could just have a class initialisation method.
But the metaclass approach seems cleaner, and with more predictable behavior.
Perhaps what you're looking for is the Singleton design pattern. There's a nice SO QA about implementing shared state in Python.
If you define
classproperty
as follows, then your example works exactly as you requested.The caveat is that you can't use this for writable properties. While
e.I = 20
will raise anAttributeError
,Example.I = 20
will overwrite the property object itself.[answer written based on python 3.4; the metaclass syntax differs in 2 but I think the technique will still work]
You can do this with a metaclass...mostly. Dappawit's almost works, but I think it has a flaw:
This gets you a classproperty on Foo, but there's a problem...
What the hell is going on here? Why can't I reach the class property from an instance?
I was beating my head on this for quite a while before finding what I believe is the answer. Python @properties are a subset of descriptors, and, from the descriptor documentation (emphasis mine):
So the method resolution order doesn't include our class properties (or anything else defined in the metaclass). It is possible to make a subclass of the built-in property decorator that behaves differently, but (citation needed) I've gotten the impression googling that the developers had a good reason (which I do not understand) for doing it that way.
That doesn't mean we're out of luck; we can access the properties on the class itself just fine...and we can get the class from
type(self)
within the instance, which we can use to make @property dispatchers:Now
Foo().thingy
works as intended for both the class and the instances! It will also continue to do the right thing if a derived class replaces its underlying_thingy
(which is the use case that got me on this hunt originally).This isn't 100% satisfying to me -- having to do setup in both the metaclass and object class feels like it violates the DRY principle. But the latter is just a one-line dispatcher; I'm mostly okay with it existing, and you could probably compact it down to a lambda or something if you really wanted.
I think you may be able to do this with the metaclass. Since the metaclass can be like a class for the class (if that makes sense). I know you can assign a
__call__()
method to the metaclass to override calling the class,MyClass()
. I wonder if using theproperty
decorator on the metaclass operates similarly. (I haven't tried this before, but now I'm curious...)[update:]
Wow, it does work:
Note: This is in Python 2.7. Python 3+ uses a different technique to declare a metaclass. Use:
class MyClass(metaclass=MetaClass):
, remove__metaclass__
, and the rest is the same.As far as I can tell, there is no way to write a setter for a class property without creating a new metaclass.
I have found that the following method works. Define a metaclass with all of the class properties and setters you want. IE, I wanted a class with a
title
property with a setter. Here's what I wrote:Now make the actual class you want as normal, except have it use the metaclass you created above.
It's a bit weird to define this metaclass as we did above if we'll only ever use it on the single class. In that case, if you're using the Python 2 style, you can actually define the metaclass inside the class body. That way it's not defined in the module scope.