Garrett Motzner
Garrett Motzner

Reputation: 3230

Composing descriptors in python

Background

In python, a descriptor is an object that defines any of __get__, __set__ or __delete__, and sometimes also __set_name__. The most common use of descriptors in python is probably property(getter, setter, deleter, description). The property descriptor calls the given getter, setter, and deleter when the respective descriptor methods are called.

It's interesting to note that functions are also descriptors: they define __get__ which when called, returns a bound method.

Descriptors are used to modify what happens when an objects properties are accessed. Examples are restricting access, logging object access, or even dynamic lookup from a database.

Problem

My question is: how do I design descriptors that are composable?

For example:

Say I have a Restricted descriptor (that only allows setting and getting when a condition of some sort is met), and a AccessLog descriptor (that logs every time the property is "set" or "get"). Can I design those so that I can compose their functionality when using them?

Say my example usage looks like this:

class ExampleClient:
  # use them combined, preferably In any order 
  # (and there could be a special way to combine them, 
  # although functional composition makes the most sense)
  foo: Restricted(AccessLog())
  bar: AccessLog(Restricted())
  # and also use them separately
  qux: Restricted()
  quo: AccessLog()

I'm looking for a way to make this into a re-usable pattern, so I can make any descriptor composable. Any advice on how to do this in a pythonic manner? I'm going to experiment with a few ideas myself, and see what works, but I was wondering if this has been tried already, and if there is sort of a "best practice" or common method for this sort of thing...

Upvotes: 1

Views: 199

Answers (1)

Blckknght
Blckknght

Reputation: 104712

You can probably make that work. The tricky part might be figuring out what the default behavior should be for your descriptors if they don't have a "child" descriptor to delegate to. Maybe you want to default to behaving like a normal instance variable?

class DelegateDescriptor:
    def __init__(self, child=None):
        self.child = child
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name
        if self.child is not None:
            try:
                self.child.__set_name__(owner, name)
            except AttributeError:
                pass

    def __get__(self, instance, owner=None):
        if instance is None:
            return self
        if self.child is not None:
            return self.child.__get__(instance, owner)
        try:
            return instance.__dict__[self.name]   # default behavior, lookup the value
        except KeyError:
            raise AttributeError

    def __set__(self, instance, value):
        if self.child is not None:
            self.child.__set__(instance, value)
        else:
            instance.__dict__[self.name] = value  # default behavior, store the value

    def __delete__(self, instance):
        if self.child is not None:
            self.child.__delete__(instance)
        else:
            try:
                del instance.__dict__[self.name]  # default behavior, remove value
            except KeyError:
                raise AttributeError

Now, this descriptor doesn't actually do anything other than store a value or delegate to another descriptor. Your actual Restricted and AccessLog descriptors might be able to use this as a base class however, and add their own logic on top. The error checking is also very basic, you will probably want to do a better job raising the right kinds of exceptions with appropriate error messages in every use case before using this in production.

Upvotes: 1

Related Questions