[TL;DR? You can skip to the end for a code example.]
I actually prefer to use a different idiom, which is a little involved for using as a one off, but is nice if you have a more complex use case.
A bit of background first.
Properties are useful in that they allow us to handle both setting and getting values in a programmatic way but still allow attributes to be accessed as attributes. We can turn 'gets' into 'computations' (essentially) and we can turn 'sets' into 'events'. So let's say we have the following class, which I've coded with Java-like getters and setters.
class Example(object):
def __init__(self, x=None, y=None):
self.x = x
self.y = y
def getX(self):
return self.x or self.defaultX()
def getY(self):
return self.y or self.defaultY()
def setX(self, x):
self.x = x
def setY(self, y):
self.y = y
def defaultX(self):
return someDefaultComputationForX()
def defaultY(self):
return someDefaultComputationForY()
You may be wondering why I didn't call defaultX
and defaultY
in the object's __init__
method. The reason is that for our case I want to assume that the someDefaultComputation
methods return values that vary over time, say a timestamp, and whenever x
(or y
) is not set (where, for the purpose of this example, "not set" means "set to None") I want the value of x
's (or y
's) default computation.
So this is lame for a number of reasons describe above. I'll rewrite it using properties:
class Example(object):
def __init__(self, x=None, y=None):
self._x = x
self._y = y
@property
def x(self):
return self.x or self.defaultX()
@x.setter
def x(self, value):
self._x = value
@property
def y(self):
return self.y or self.defaultY()
@y.setter
def y(self, value):
self._y = value
# default{XY} as before.
What have we gained? We've gained the ability to refer to these attributes as attributes even though, behind the scenes, we end up running methods.
Of course the real power of properties is that we generally want these methods to do something in addition to just getting and setting values (otherwise there is no point in using properties). I did this in my getter example. We are basically running a function body to pick up a default whenever the value isn't set. This is a very common pattern.
But what are we losing, and what can't we do?
The main annoyance, in my view, is that if you define a getter (as we do here) you also have to define a setter.[1] That's extra noise that clutters the code.
Another annoyance is that we still have to initialize the x
and y
values in __init__
. (Well, of course we could add them using setattr()
but that is more extra code.)
Third, unlike in the Java-like example, getters cannot accept other parameters. Now I can hear you saying already, well, if it's taking parameters it's not a getter! In an official sense, that is true. But in a practical sense there is no reason we shouldn't be able to parameterize an named attribute -- like x
-- and set its value for some specific parameters.
It'd be nice if we could do something like:
e.x[a,b,c] = 10
e.x[d,e,f] = 20
for example. The closest we can get is to override the assignment to imply some special semantics:
e.x = [a,b,c,10]
e.x = [d,e,f,30]
and of course ensure that our setter knows how to extract the first three values as a key to a dictionary and set its value to a number or something.
But even if we did that we still couldn't support it with properties because there is no way to get the value because we can't pass parameters at all to the getter. So we've had to return everything, introducing an asymmetry.
The Java-style getter/setter does let us handle this, but we're back to needing getter/setters.
In my mind what we really want is something that capture the following requirements:
Users define just one method for a given attribute and can indicate there
whether the attribute is read-only or read-write. Properties fail this test
if the attribute writable.
There is no need for the user to define an extra variable underlying the function, so we don't need the __init__
or setattr
in the code. The variable just exists by the fact we've created this new-style attribute.
Any default code for the attribute executes in the method body itself.
We can set the attribute as an attribute and reference it as an attribute.
We can parameterize the attribute.
In terms of code, we want a way to write:
def x(self, *args):
return defaultX()
and be able to then do:
print e.x -> The default at time T0
e.x = 1
print e.x -> 1
e.x = None
print e.x -> The default at time T1
and so forth.
We also want a way to do this for the special case of a parameterizable attribute, but still allow the default assign case to work. You'll see how I tackled this below.
Now to the point (yay! the point!). The solution I came up for for this is as follows.
We create a new object to replace the notion of a property. The object is intended to store the value of a variable set to it, but also maintains a handle on code that knows how to calculate a default. Its job is to store the set value
or to run the method
if that value is not set.
Let's call it an UberProperty
.
class UberProperty(object):
def __init__(self, method):
self.method = method
self.value = None
self.isSet = False
def setValue(self, value):
self.value = value
self.isSet = True
def clearValue(self):
self.value = None
self.isSet = False
I assume method
here is a class method, value
is the value of the UberProperty
, and I have added isSet
because None
may be a real value and this allows us a clean way to declare there really is "no value". Another way is a sentinel of some sort.
This basically gives us an object that can do what we want, but how do we actually put it on our class? Well, properties use decorators; why can't we? Let's see how it might look (from here on I'm going to stick to using just a single 'attribute', x
).
class Example(object):
@uberProperty
def x(self):
return defaultX()
This doesn't actually work yet, of course. We have to implement uberProperty
and
make sure it handles both gets and sets.
Let's start with gets.
My first attempt was to simply create a new UberProperty object and return it:
def uberProperty(f):
return UberProperty(f)
I quickly discovered, of course, that this doens't work: Python never binds the callable to the object and I need the object in order to call the function. Even creating the decorator in the class doesn't work, as although now we have the class, we still don't have an object to work with.
So we're going to need to be able to do more here. We do know that a method need only be represented the one time, so let's go ahead and keep our decorator, but modify UberProperty
to only store the method
reference:
class UberProperty(object):
def __init__(self, method):
self.method = method
It is also not callable, so at the moment nothing is working.
How do we complete the picture? Well, what do we end up with when we create the example class using our new decorator:
class Example(object):
@uberProperty
def x(self):
return defaultX()
print Example.x <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x <__main__.UberProperty object at 0x10e1fb8d0>
in both cases we get back the UberProperty
which of course is not a callable, so this isn't of much use.
What we need is some way to dynamically bind the UberProperty
instance created by the decorator after the class has been created to an object of the class before that object has been returned to that user for use. Um, yeah, that's an __init__
call, dude.
Let's write up what we want our find result to be first. We're binding an UberProperty
to an instance, so an obvious thing to return would be a BoundUberProperty. This is where we'll actually maintain state for the x
attribute.
class BoundUberProperty(object):
def __init__(self, obj, uberProperty):
self.obj = obj
self.uberProperty = uberProperty
self.isSet = False
def setValue(self, value):
self.value = value
self.isSet = True
def getValue(self):
return self.value if self.isSet else self.uberProperty.method(self.obj)
def clearValue(self):
del self.value
self.isSet = False
Now we the representation; how do get these on to an object? There are a few approaches, but the easiest one to explain just uses the __init__
method to do that mapping. By the time __init__
is called our decorators have run, so just need to look through the object's __dict__
and update any attributes where the value of the attribute is of type UberProperty
.
Now, uber-properties are cool and we'll probably want to use them a lot, so it makes sense to just create a base class that does this for all subclasses. I think you know what the base class is going to be called.
class UberObject(object):
def __init__(self):
for k in dir(self):
v = getattr(self, k)
if isinstance(v, UberProperty):
v = BoundUberProperty(self, v)
setattr(self, k, v)
We add this, change our example to inherit from UberObject
, and ...
e = Example()
print e.x -> <__main__.BoundUberProperty object at 0x104604c90>
After modifying x
to be:
@uberProperty
def x(self):
return *datetime.datetime.now()*
We can run a simple test:
print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()
And we get the output we wanted:
2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310
(Gee, I'm working late.)
Note that I have used getValue
, setValue
, and clearValue
here. This is because I haven't yet linked in the means to have these automatically returned.
But I think this is a good place to stop for now, because I'm getting tired. You can also see that the core functionality we wanted is in place; the rest is window dressing. Important usability window dressing, but that can wait until I have a change to update the post.
I'll finish up the example in the next posting by addressing these things:
We need to make sure UberObject's __init__
is always called by subclasses.
- So we either force it be called somewhere or we prevent it from being implemented.
- We'll see how to do this with a metaclass.
We need to make sure we handle the common case where someone 'aliases'
a function to something else, such as:
class Example(object):
@uberProperty
def x(self):
...
y = x
We need e.x
to return e.x.getValue()
by default.
- What we'll actually see is this is one area where the model fails.
- It turns out we'll always need to use a function call to get the value.
- But we can make it look like a regular function call and avoid having to use
e.x.getValue()
. (Doing this one is obvious, if you haven't already fixed it out.)
We need to support setting e.x directly
, as in e.x = <newvalue>
. We can do this in the parent class too, but we'll need to update our __init__
code to handle it.
Finally, we'll add parameterized attributes. It should be pretty obvious how we'll do this, too.
Here's the code as it exists up to now:
import datetime
class UberObject(object):
def uberSetter(self, value):
print 'setting'
def uberGetter(self):
return self
def __init__(self):
for k in dir(self):
v = getattr(self, k)
if isinstance(v, UberProperty):
v = BoundUberProperty(self, v)
setattr(self, k, v)
class UberProperty(object):
def __init__(self, method):
self.method = method
class BoundUberProperty(object):
def __init__(self, obj, uberProperty):
self.obj = obj
self.uberProperty = uberProperty
self.isSet = False
def setValue(self, value):
self.value = value
self.isSet = True
def getValue(self):
return self.value if self.isSet else self.uberProperty.method(self.obj)
def clearValue(self):
del self.value
self.isSet = False
def uberProperty(f):
return UberProperty(f)
class Example(UberObject):
@uberProperty
def x(self):
return datetime.datetime.now()
[1] I may be behind on whether this is still the case.