Kent's Korner
by Kent S Johnson

2007-08-19 18:12:14

Descriptors and properties

Descriptors provide a way to customize access to attributes of an object. They are a fundamental mechanism of Python, introduced with new-style classes in version 2.2. Descriptors are the mechanism underlying properties and responsible for converting function attributes to bound and unbound method objects. They are rarely seen directly in common usage but it can be helpful to understand them.

Note: Descriptors only work with new-style classes! You can use a descriptor with an old-style class and it may superficially appear to work, but it isn't working!

Note: because the names are similar, descriptors are sometimes confused with decorators. Other than the name, they have little in common.

Yet another note: I'm not at all sure I can do a better job describing descriptors than Raymond Hettinger [1]. You be the judge...

Data descriptors

A data descriptor is any object that implements the methods

  • __get__(self, obj, type=None)
  • __set__(self, obj, value)

and optionally

  • __delete__(self, obj)

Data descriptors by themselves are not very useful. They become interesting when they are stored as attributes of classes.

Suppose object a is an instance of class A. Normally read access to a.x looks for an entry with key 'x' in a.__dict__, i.e. a.__dict__['x']. But if the class A has a data descriptor stored in it's attribute x, the lookup changes to A.__dict__['x'].__get__(a, A). In other words, the simple attribute access is transformed into a function call. (This transformation takes place in object.__getattribute__().)

Write access to attributes is similarly transformed. Normally write access to a.x becomes an assignment to a.__dict__['x']. But if A.x is a data descriptor, write access to a.x becomes a call to A.__dict__['x'].__set__(a, value). Once again, simple attribute access is transformed into a function call.

Properties

Notice that the syntax in the client code hasn't changed. You still write y = a.x or a.x = y, but adding the descriptor attribute to the class has changed the meaning significantly. Introducing a function call into attribute access allows you to manipulate the data in ways that are impossible with simple attribute access.

Properties are a shorthand way to define descriptors. property() is a built-in function that takes fget(), fset() and fdel() arguments (and an optional doc string) and returns a descriptor whose __get__(), __set__() and __del__() methods map to the provided arguments.

Typically the parameters passed to property() are member functions that get and set private attributes of the object while wrapping access to the attribute with some action.

Here is a class with a simple property that logs access to its underlying attribute:

class A(object):
  def __init__(self): self._x = 0

  def _getx(self):
    print 'Getting x'
    return self._x

  def _setx(self, val):
    print 'Setting x to', val
    self._x = val

  def _delx(self):
    print 'Deleting x'
    del self._x

  x = property(_getx, _setx, _delx, "X marks the spot")

Now simple attribute access such as this:

a = A()
y = a.x
a.x = 2

produces the output

Getting x
Setting x to 2

A disadvantage to defining a property this way is that the _getx(), _setx() and _delx() methods are still defined and accessible. One way around this is to delete the helpers after the property is defined:

del _getx()
del _setx()
del _delx()

For a read-only property, another option is to use property() as a decorator:

@property
def x(self):
  print 'Getting x'
  return self._x

For a read-write property, one option is a clever and obscure hack that uses apply() as a decorator and hides the helper functions in the scope of another function. Using this technique the original property in this section could be written as

@apply
def x():
    doc = "X marks the spot"
    def fget(self):
      print 'Getting x'
      return self._x

    def fset(self, val):
      print 'Setting x to', val
      self._x = val

    def fdel(self):
      print 'Deleting x'
      del self._x

    return property(**locals())

Whether this is elegant or abhorrent is in the eye of the beholder. It does define the desired property without leaking function names into the class namespace, using only built-in functions. However apply() is deprecated. Of course it could be replaced by the line

x = x()

after the definition of x which is perhaps cleaner and less obscure as well.

There are many alternatives in the Python Cookbook.

Update: In Python 2.6, properties have getter, setter and deleter methods that provide a simple way to create properties with multiple attributes.

Non-data descriptors

A non-data descriptor defines __get__() but not __set__(). The most important example of a non-data descriptor is an ordinary function, which does indeed have a __get__() method:

>>> def f(self): return 3
...
>>> dir(f)
['__call__', '__class__', '__delattr__', '__dict__', '__doc__', '__get__',
'__getattribute__', '__hash__', '__init__', '__module__', '__name__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__',
'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc',
'func_globals', 'func_name']

When a non-data descriptor is an attribute of a class, it has the same effect as a data descriptor - read access to an attribute, e.g. a.x, is converted to a function call A.__dict__['x'].__get__(a, A). What is the return value of the function call? A bound method:

>>> class A(object): pass
...
>>> a=A()
>>> f.__get__(a, A)
<bound method A.f of <__main__.A object at 0x6a890>>

If the object argument is omitted, the result is an unbound method:

>>> f.__get__(None, A)
<unbound method A.f>

Non-data descriptors are the mechanism by which ordinary functions, when stored as class methods, are converted to bound and unbound methods.

Overriding class descriptors in an instance

There is one big difference between data descriptors and non-data descriptors, besides the presence or absence of a __set__() method. Non-data descriptors can be overridden by assignment to an instance attribute; data descriptors cannot. What this means is, if access to a data attribute is governed by a property or other data descriptor, you cannot change this by assigning a new attribute in an instance. But methods, as non-data descriptors, can be overridden by instances.

References

[1]Raymond Hettinger's How-To Guide for Descriptors http://users.rcn.com/python/download/Descriptor.htm
[2]Descriptors from A. M. Kuchling's What's New in Python 2.2
 
© Kent S Johnson Creative Commons License

Short, introductory essays about Python.

kentsjohnson.com

Kent's Korner home

Weblog

Other Essays