So I'm going to start writing every day. Hold me to that, please.
Today I'm writing about one little corner of Python, the property function. It's a builtin function, around since at least 2.2.
I used a question that involved property as one of the
interview questions during a recent developer search, and I found
about a 50-50 split of people who knew about it, and those who didn't,
and without much correlation to how much Python experience the
developer had. So I think it's one of those things that you only
really use if you know about it - it's by no means essential, and you
can go your whole Python career not knowing about it, but, well, as
toolboxes go, this is a pretty nifty screwdriver.
I'm going to show basic usage, and then a couple ways to abuse it.
Basic Usage
- class Person(object):
- def __init__(self, first_name, last_name):
-
- # pretty straightforward initialization, just set a couple
- # attributes
- self.first_name = first_name
- self.last_name = last_name
-
- def get_full_name(self):
- return "%s %s" % (self.first_name, self.last_name)
-
- full_name = property(get_full_name)
And how it works in an interactive session:
>>> me = Person("Adam", "Gomaa")
>>> me.get_full_name()
'Adam Gomaa'
>>> me.full_name
'Adam Gomaa'
Note the lack of parens on the last input line; despite the lack of an
explicit call, get_full_name apparently got called anyway!
This is what the property builtin does: it allows you to
set getters and setters under some name on instances of the class. I
only did a getter in this case, but setters are also possible as the
optional second argument:
- class Person(object):
- # ...
-
- def get_full_name(self):
- return "%s %s" % (self.first_name, self.last_name)
-
- def set_full_name(self, full_name):
- self.first_name, self.last_name = full_name.split()
-
- full_name = property(get_full_name, set_full_name)
Why?
Now why would you want to do this? In many cases, it's because
something you used to have as an actual instance attribute (say,
.url) became a computed value, based on other instance
attributes. Now, you could update all your code to call
.get_url() instead, and write a .set_url()
if that's possible... or you could just turn .url into a
property.
Be sure, though, to set some ground rules for yourself. Remember, you're simulating an attribute lookup, which, in Python, usually means looking up a value in a dictionary based on a short string key - one of the fastest, most-optimized pieces of Python. So, don't make your property getter overly complex. In general, I'd say that it should have one and only one code path - conditionals are acceptable, but you're better off without them if you really want this to be a leakless abstraction. And stay far, far away from anything that has side effects in property getters - you'll drive yourself and other programmers crazy.
(Don't worry, I'll break all these rules by the time I finish this article.)
Decorator Tricks
The signature of property is:
- property(fget=None, fset=None, fdel=None, doc=None)
When you only need a getter - which is the most common use case - then the relevant part becomes:
- property(getter)
And so your code, inside your class is going to look something like:
- class MyObject(object):
-
- def get_something(self):
- return whatever
- something = property(get_something)
In fact, at that point, you don't really care about
get_something. We could even do something like:
- def something(self):
- return whatever
- something = property(something)
Which, as long as you're using Python 2.4 or above, can be shorthanded to:
- @property
- def something(self):
- return whatever
At that point, accessing .something will call this getter
function, without the parens. Be sure not to use
.something(), or you'll be calling whatever is returned
(helpfully named 'whatever' in the example above), and if it's not
callable, you'll get an exception.
Defining Getter, Setter, and Deller in One
Let's take an example adapted from one of the comments at ActiveState:
- def Property(func)
- return property(**func())
-
- class Person(object):
- @Property
- def name():
- doc = "The person's name"
-
- def fget(self):
- return "%s %s" % (self.first_name, self.last_name)
-
- def fset(self, name):
- self.first_name, self.last_name = name.split()
-
- def fdel(self):
- del self.first_name
- del self.last_name
-
- return locals()
The advantage to this strategy is that it allows you to define all the
arguments to property without filling up the class's
namespace with _get_foo, _set_foo, and so
on.
The disadvantage is that you already have 3 levels of indentation in your 'top-level' getter function code. I personally avoid it for this reason. As noted before, though, most of the time you can get away with not having a setter at all.
Example: Django URLs
For a long time, in the Django world, .get_absolute_url()
was the way you got URLs for
objects. django.core.urlresolvers.reverse has gained some
ground - and in terms of DRYness, is a better answer - but let me tell
you, reverse("appname-modelname", args=[object.slug]) is
not so fun, particularly when you change that URL pattern name and
have to update a few dozen {% url %} tags. (sed and grep
help a lot with that, but many developers, myself included on most
days, don't have enough sed-fu to use it without the man page open).
.get_absolute_url() has it's own set of problems. For
starters, it ties the model layer to the URL layer, kind of, sort of,
maybe. But reverse() is ugly because you have to remember
what the arguments are, what order they're in, and how you're supposed
to get the damned things anyway. If you're doing a RESTful,
hierarchy-oriented URL scheme, this can get ugly fast:
# to get '/books/a-midsummer-nights-dream/reviews/overall-nice-odd-grammar/update/':
review_update_url = reverse("books-review-update", args=[self.book.slug, self.slug])
And, there's no way of knowing that you need the book slug, as opposed
to the book id, unless you go look up the URLconf. And that'll
interrupt your flow, or you can just guess, and then you'll interrupt
your flow anyway to debug the NoReverseMatch that you just got.
Enter property. Who's really the authority on Review's
URLs? Well, technically, the URLconf. But in some ethereal sense,
shouldn't Review be arbitrating it's own URLs? That's
what .get_absolute_url() did, and it worked pretty well;
a considered, balanced 'denormalization' for the sake of convenience
at the expense of DRY.
Unfortunately, get_absolute_url() has its own problem (besides the
theoretical ones) - namely, there's only one of it. For each object,
typically, you'll have several URLs:
/books/ - an index/list/search page
/books/new/ - A new submission form
/books/(regexp)/ - a view/update page
/books/(regexp)/delete/ - A delete/confirm delete page
/books/(regexp)/reviews/ - *another* index/list/search page
...
At first, property only makes this less painful:
- from django.core.urlresolvers import reverse
-
- class Book(models.Model):
- # for compatibility, I'm leaving .get_absolute_url()
- def get_absolute_url(self):
- return reverse("books-view", args=[self.slug])
- absolute_url = property(get_absolute_url)
-
- @property
- def reviews_url(self):
- return reverse("books-reviews", args=[self.slug])
-
- @property
- def delete_url(self):
- return reverse("books-delete", args=[self.slug])
But if you're paying attention, you'll see all these seem to follow the same pattern:
- @property
- def SOMETHING_url(self):
- return reverse("books-SOMETHING", args=[self.slug])
If you can see where I'm going, now's probably the time to start running away.
Now, duplicated code means "think about an abstraction." I'm about to break the first rule I set, about not making properties overly complex. In reality, a quick little method could do what I'm about to show you in a much easier way:
- def object_url(self, url_type):
- if url_type == "reviews":
- return reverse(...
- elif url_type == "delete":
- # .. and so on
But come on now, that wouldn't be very much fun, would it?
Overly Complex Properties
So instead, let's think about how we would want to define a named set of urls for a single object. We could do it with multiple FOO_url properties:
book.absolute_url
book.reviews_url
book.delete_url
Or we could make a .url property with dictionary access:
book.urls['absolute']
book.urls['reviews']
book.urls['delete']
Or heck, even with attribute access
book.urls.absolute
book.urls.reviews
book.urls.delete
At that point, your view code for redirects becomes utterly wonderful:
return HttpResponseRedirect(book.urls.reviews)
and so on. I just love that, because it's so astonishingly close to what I'm trying to say: send them to the reviews page for this book.
I'm going to show the attribute access code, but you can pretty much
substitute for dictionary syntax by replacing __getattr__
with __getitem__ in this code:
- def attrproperty(getter_function):
-
- class _Object(object):
- def __init__(self, obj):
- self.obj = obj
- def __getattr__(self, attr):
- return getter_function(self.obj, attr)
-
- return property(_Object)
Of course, that's still rather indented, but at least its a library function rather than something you're putting into your model code. Usage looks something like this:
- class Book(models.Model):
-
- @attrproperty
- def urls(self, name):
- if name == "absolute":
- urlpattern_name = "books-view"
- elif name == "reviews":
- urlpattern_name = "books-reviews"
- elif name == "delete":
- urlpattern_name = "books-delete"
-
- return reverse(urlpattern_name, args=[self.slug])
Thus allowing book.urls.whatever. Dictionary syntax is
actually a little nicer, since you can more easily stick a variable in
there (books.urls[action], for example) but I like the
look of attribute access.
Anyway, it's pretty obvious that this is a gross abuse of property. But I'm not satisfied yet.
Caching: Side-Effect Properties
Let's go back to our .FOO_url properties from before. One
thing about reverse() calls is that if you have to
traverse intermediary models, building urls can be, well,
expensive. If Review #142 has to look up the slug of Book #21, it
probably has to load that object from the database (unless your PK is
also what you're using in the URL, but we can't count on that). That
can make rendering an HTML page, with dozens or hundreds of links and
.FOO_url accesses, kind of expensive.
But, of course, you're coding so those URLs don't change, right? So why recompute it each time? Just compute it once for each object, and throw it into a cache.
- from functools import wraps
- from django.core.cache import cache
-
- def cached_property(func):
- @wraps(func)
- def _closure(self):
- cache_key = "%s.%s.%s(%s)" % (self.__class__.__module__,
- self.__class__.__name__,
- func.__name__,
- self.pk)
- val = cache.get(cache_key)
-
- if val is None:
- val = func(self)
- cache.set(cache_key, val)
-
- return val
-
- return property(_closure)
Throw that around your .FOO_url properties:
- @cached_property
- def reviews_url(self):
- return reverse("books-reviews", args=[self.slug])
-
- @cached_property
- def delete_url(self):
- return reverse("books-delete", args=[self.slug])
and now they'll only make DB calls the first time they're called, per object that accessed on. That's certainly acceptable.
You could combine this with @attrproperty to get cached
obj.urls.foo access, but that will be left as an exercise
for the reader.
Finally...
That's about all I have for today. While researching this post I looked into the Python descriptor protocol, which I might make the subject of a future post.
Comments
1696 spam comments omitted.
I am no longer accepting new comments.
David Montgomery
#5811, 2008-08-11T19:30:54Z
In the "Defining Getter, Setter, and Deller in One" example, name is missing a "return locals()".
Adam Gomaa
#5814, 2008-08-11T19:56:14Z
It's there; you might have to scroll to see it.
Ronny Pfannschmidt
#5825, 2008-08-12T14:39:28Z
your caching sucks
use a non-data descriptor
that way the instance slot of the same name will override the class sloth, thus not polluting the instance with weird attributes
Lucy
#5826, 2008-08-12T14:44:11Z
cool. thx also for the "unifying types and classes" link , since that provides useful background.
Adam Gomaa
#5827, 2008-08-12T17:02:46Z
True enough, Ronny. That caching code is Django-specific, not like non-data descriptions. But, it doesn't pollute the class namespace - closure doesn't get captured in the class.
On the other hand, the advantage of using that over a non-data descriptor is that non-data descriptors will run once per process per object - whereas if you're using a one of the external-process cache engines in Django (like memcached) then it's only once per object, no matter how many processes you're running. Not a huge deal, but helps if you're using short-lived processes.
Damien
#6154, 2008-08-23T16:21:43Z
Shouldn't attrproperty return property(_Object)?
sean
#6168, 2008-08-23T21:51:48Z
quite a useful tutorial, i finally understand what the property is doing now, thanks!
Jason Seifer
#6174, 2008-08-24T01:23:36Z
Coming from a rails background this is thought provoking because in rails you wouldn't make a url part of your model code. However you could make the argument for it, especially how you demonstrate it, that doing it this way makes more sense -- the url for book reviews is _part of_ the domain model of the book. Great post, thank you for writing this.
onno
#6187, 2008-08-24T04:06:20Z
What about a RSS feed so I can follow your articles
Adam Gomaa
#6210, 2008-08-24T10:22:00Z
Damien: Yes, thanks for the heads-up.
onno: For some reason I only had the RSS link on the blog index page; it should be fixed now.
Empty
#7464, 2008-09-11T16:39:06Z
Excellent stuff Adam. You're one smart guy. I really like the instance.urls.action approach. That said I disagree with Jason. My feeling is that the routes can know about the models but not the other way around.
kaleissin
#9481, 2008-11-14T08:22:59Z
Why if.. elif.. elif.. when you can do:
or something similar. (Will be interesting to see how the code is formatted.) Then you can fetch the list of possible names from somewhere also.
Though, urls like {{ object.urls.subtype }} ought to be in core :)
Wes Winham
#9921, 2008-11-24T16:28:21Z
This is a really cool way to use properties. I'm a bit scared about using it myself, but it definitely gets me thinking.
Thanks for sharing.
David Zhou
#10114, 2008-12-04T03:49:52Z
Unless I'm misunderstanding something, shouldn't "return property(func)" instead be "return property(_closure)" in your cached_propert() decorator?
dude
#13668, 2009-02-21T14:33:11Z
Not sure why you'd want to do any of this. Especially with languages around like Ruby that come with better solutions to this baked in.
Runsun Pan
#19433, 2009-05-06T14:51:00Z
My recipe:
Easy Property Creation in Python http://code.activestate.com/recipes/576742/
rox0r
#26521, 2009-08-07T14:54:56Z
Ah, the old silver bullet. X solves that problem perfectly, why not use it? (While leaving out the trade offs)
(ED: Fixed formatting.)
Jacob Fenwick
#31831, 2010-01-10T14:42:01Z
The Defining Getter, Setter, and Deller in One code doesn't work, it's missing a colon after def Property(func).
Also, I'm not sure how I would access Person to see the getter, setter, and deller in action.