A limitation of Python properties

April 8, 2007

A periodically annoying limitation of Python properties on instances is that you cannot remove or replace a property. (You can implement del in your property, but this is not the same thing.)

This restriction exists because properties are actually attributes on the class, not attributes on the instances. Because of how attribute lookup works, you can't even sneak a replacement in through the instance's __dict__.

This means that you can't use properties to implement a highly efficient 'generate once and then cache the answer' instance attribute, where the first time you access obj.x the value gets generated and thereafter access to it goes at plain instance attribute speeds. This matters, because property access is about three times slower than accessing a simple attribute (and a plain function call to a lambda is about twice as slow).

If you need this, the best way is to make a __getattr__ function that creates the variable the first time it's accessed. This is somewhat less elegant and more monolithic than a property approach, and has the other downside that pychecker will probably complain about access to those magically created variables.

Sidebar: an interesting gotcha in timing attribute access

I did my timing of various attribute access cases using single-letter variable names. To my surprise, there were significant performance differences between the same access method based on what letter I used for the variable name. I suspect that this is because of dictionary hash collisions.

I also accidentally discovered that accessing class attributes is slightly faster than accessing instance attributes (and I don't think this one is dependent on variable names).


Comments on this page:

From 75.75.26.88 at 2008-01-27 20:27:19:

You can't do it with the built-in property, but it's easy to write a non-data descriptor that does it. My version below is not entirely original; Googling on "cached property" or "memoized property" will turn up a number of cookbook recipes and such. However, your spec is stricter than the ones I've seen online, because you require that, once the attribute has been accessed once (and the expensive computation of its value is done), future accesses should be as fast as a normal __dict__ lookup (i.e., no method call allowed).

Here's my code:

(( class CachedProperty(object):

   """
   Non-data descriptor class for cached property. The
   expected typical use case is to be called from the
   cached_property function, which generates the name of
   the property automatically, but the class can also be
   instantiated directly with an explicit name supplied.
   """

   def __init__(self, aname, fget, doc=None):
       self.aname = aname
       self.fget = fget
       self.__doc__ = doc

   def __get__(self, obj, objtype=None):
       if obj is None:
           return self
       result = self.fget(obj)
       setattr(obj, self.aname, result)
       return result

def cached_property(fget, doc=None):

   """
   Function to return cached property instance. We need
   this as a wrapper to supply the name of the property by
   magic rather than force the user to enter it by hand;
   this is done by looking up the name of the fget function
   (which also allows this function to be used as a decorator
   and have the intended effect).
   """

   if doc is None:
       doc = fget.__doc__
   return CachedProperty(fget.__name__, fget, doc)

))

This works because a descriptor on a class that doesn't have a __set__ method can be masked by an instance attribute; this is discussed in the Python documentation here .

I actually had a use case for this recently, and came across your blog entry in the course of Googling for information; hence the much-delayed comment. I've done some informal timing tests with this compared to a version using the built-in property (so access after the first still involves a method call), and it does make a difference.

Peter Donis

Written on 08 April 2007.
« Weekly spam summary on April 7th, 2007
Why indirect xdm probably doesn't work on your Linux machine »

Page tools: View Source, View Normal.
Search:
Login: Password:

Last modified: Sun Apr 8 22:23:25 2007
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.