Welcome, guest | Sign In | My Account | Store | Cart

A Python property is accessed like a normal attribute, but is implemented using method calls. This is a recipe for dictproperty: an attribute that is accessed like an indexed (dictionary) attribute, but is implemented using method calls.

Python, 35 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class dictproperty(object):

    class proxy(object):
        def __init__(self, fget, fset, fdel):
            self._fget = fget
            self._fset = fset
            self._fdel = fdel
            self._obj = None

        def setObj(self, obj):
            self._obj = obj

        def __getitem__(self, key):
            if self._fget is None:
                raise TypeError, "can't read item"
            return self._fget(self._obj, key)

        def __setitem__(self, key, value):
            if self._fset is None:
                raise TypeError, "can't set item"
            self._fset(self._obj, key, value)

        def __delitem__(self, key):
            if self._fdel is None:
                raise TypeError, "can't delete item"
            self._fdel(self._obj, key)

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self._proxy = dictproperty.proxy(fget, fset, fdel)
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None: return self
        self._proxy.setObj(obj)
        return self._proxy

Properties offer a way to access data like a normal attribute, but implemented using method calls. For example, rather than accessing an attribute of MyClass called foo with function calls like obj.getFoo and obj.setFoo, a property lets you simply write obj.foo. Reading from, writing to, or deleting obj.foo causes one of the corresponding functions to get invoked.

This recipe implements dictproperty, an analog to normal properties that can be used in cases where the attribute is a collection rather than a singleton. A dictproperty attribute foo can be accessed with dictionary-like syntax, i.e. obj.foo[key], but is actually implemented with calls to functions like obj.getFoo(key) and obj.setFoo(key).

In this example, MyClass contains a dictproperty attribute foo:

class MyClass(object):
    def __init__(self):
        self._foo = {}

    def getFoo(self, key):
        print 'Getting _foo[', key, ']'
        return self._foo[key]
    def setFoo(self, key, value):
        print 'Setting _foo[', key, '] to', value
        self._foo[key] = value
    def delFoo(self, key):
        print 'Deleting _foo[', key, ']'
        del self._foo[key]
    foo = dictproperty(getFoo, setFoo, delFoo, "something or other")


# A simple test

obj = MyClass()
obj.foo[3] = 5
assert obj._foo[3] == 5
assert obj.foo[3] == 5
del obj.foo[3]
assert not obj._foo.has_key(3)

Of course, in this example, the getFoo and setFoo do nothing more than access a real dictionary _foo, but in a real application these functions could be arbitrarily complex.

dictproperty is a minimal descriptor class that creates a proxy object, which implements __getitem__, __setitem__ and __delitem__, passing requests through to the functions that the user provided to the dictproperty constructor.

Note that since dictproperty reuses a single proxy object for each attribute access, the attribute is not thread-safe unless accesses are protected with some sort of lock. Thread safety could also be achieved by modifying dictproperty to create a new proxy instance on each attribute access.

See Recipe 205183 for a tidy property idiom.

1 comment

Steven Bethard 18 years, 7 months ago  # | flag

New proxy instance on each attribute access. I think you should definitely go with the new proxy instance each time. No reason to lose thread safety when you don't have to.

class dictproperty(object):

    class _proxy(object):

        def __init__(self, obj, fget, fset, fdel):
            self._obj = obj
            self._fget = fget
            self._fset = fset
            self._fdel = fdel

        def __getitem__(self, key):
            if self._fget is None:
                raise TypeError, "can't read item"
            return self._fget(self._obj, key)

        def __setitem__(self, key, value):
            if self._fset is None:
                raise TypeError, "can't set item"
            self._fset(self._obj, key, value)

        def __delitem__(self, key):
            if self._fdel is None:
                raise TypeError, "can't delete item"
            self._fdel(self._obj, key)

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self._fget = fget
        self._fset = fset
        self._fdel = fdel
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self._proxy(obj, self._fget, self._fset, self._fdel)
Created by Ed Swierk on Fri, 2 Sep 2005 (PSF)
Python recipes (4591)
Ed Swierk's recipes (1)

Required Modules

  • (none specified)

Other Information and Tasks