Python

Instance-Level Properties in Python

Making Descriptors that act as specialized properties can be tricky, especially when it comes to storing the data the property controls. I should know, I literally wrote the book. Looking at how other languages do it – especially Kotlin’s Delegated Properties – I felt that Python could use a system that works more like that.

Delegated Properties are simply objects stored on an instance that are accessed differently. When accessing the attribute on the instance, instead of returning the object stored there, it calls the object’s getter or setter, depending on how it’s being accessed.

When designing properties for instances (think read-only attributes or properties that validate incoming data) as opposed to class-level descriptors (think classmethod or staticmethod), using regular descriptors can be a pain, since storing the value that they use is tricky. Storing it safely on the descriptor itself requires specialized storage (to avoid running into memory leaks or unhashable keys) whereas storing it on the instance itself requires avoiding using an attribute name that the instance already uses. The instance-level properties I will show you today allows you to avoid all of that.

Requirements

Before I show you what I made, I will guide you through some of the thought process.

What do we require the system to do?

  • We need to be able to store an object (which will be the property) under an attribute’s name that will not be returned, but rather have its getter/setter/deleter used.
  • We need a simple and preferably quick way to determine if an object is a property.
  • We need to not be required to implement all of the getter, setter, and deleter methods unless they’re useful

And How

So, to accomplish the first goal, we will need to override the __getattribute__(), __setattr__(), and __delattr__() methods of a class to 1) check that an attribute is actually property object and 2) if it is, call the appropriate method. Also, to accomplish the last point, we should catch AttributeErrors that may be thrown if the method is unimplemented. I’ve also decided that when these AttributeErrors are caught, we will raise a new AttributeError that has a message along the lines of “This property does not support such and such action”.

In order to not have to implement this every time, we’ll create a “mixin” base class that already does it, called `WithObjectProperties`:

class WithObjectProperties(object):


    def __getattribute__(self, key):
        attr = super().__getattribute__(key)
        if isObjectProperty(attr):
            try:
                return attr.get(???)
            except AttrbuteError:
                raise AttributeError("Property '{}' on object {j} does not allow read access".format(key, self))
        else:
            return attr


     def __setattr__(self, key, value):
        try:
            attr = super().__getattribute__(key)
       except AttributeError:
            attr = None

        if isObjectProperty(attr):
            try:
                attr.set(???)
            except AttributeError:
                raise AttributeError("Property '{}' on object {} does not allow write access".format(key, self))
        else:
            super().__setattr__(key, value)

    def __delattr__(self, key):
        attr = super().__getattribute__(key)
        if isObjectProperty(attr):
            try:
                attr.delete(???)
            except AttributeError:
                raise AttributeError("Property '{}' on object {} does not allow deletion".format(key, self))
        super().__delattr__(key)

The one thing else that needs to be noted is that __delattr__() is designed in that it will actually remove the attribute if the calling the deleter method doesn’t raise an AttributeError. So, if the property is designed to take care of deletion without itself actually being deleted, it should raise an AttributeError after doing what it does.

Okay, so we still need to address the second point. In the previous code example, it called isObjectProperty(), but we haven’t seen how that’s implemented yet. After a bit of thinking, I decided to make it so that all object properties would have a boolean flag called . If the flag exists and is True, then it’s a object property. Otherwise, it’s not.

def isObjectProperty(obj):
    try:
        if obj._objprop:
            return True
        else:
            return False
    except AttributeError:
        return False

So, this allows an object property class to define the attribute at its level if it never changes (the typical use case), but it also allows for some objects to decide at runtime whether it’s an object property, and it can even change if it needs to. This allows a little more flexibility into the system, though admittedly, it’s not likely to be used much or maybe not even at all. I don’t really mind; it makes it so that it doesn’t need to do up to three checks for whether any of the getter, setter, or deleter methods exist. It’s quick and simple, so I’m sticking with it. If you want to reimplement how the check works, that’s completely fine.

Next, we need to determine the parameters for the getter, setter, and deleter methods. Obviously, they’ll have the self parameter (unless you implement them as class or static methods). Other than that, the setter method is the only one that needs a parameter, that being the new value to set to. I took a tip from normal descriptors and decided to include a parameter for the instance that the value is being held for, called inst – short for ‘instance’. One last parameter I decided to include because of my work with the descriptors book: the name of the attribute on the instance. This allows the property to store the actual attribute back onto the instance using a modified name (usually by adding an underscore to the beginning). Also, the inclusion of the instance and the name allows for well written error messages if something goes wrong.

So that means that the three potential methods that an object property can have are:

get(self, inst, name)
set(self, inst, name, value)
delete(self, inst, name)

So, in the above code, the ??? spots should be changed to, in order, self, key, self, key, value, and self, key. These all happen to be the same set of parameters coming in to the surrounding method.

If we are to make a base class for object properties to optionally derive from, It would be super simple and look like this:

class ObjectProperty:
    _objprop = True

We could potentially write some sort of ABC that checks that at least one of the methods are implemented, but that’s kind of a waste of time.

Example!

So let’s look at an example, shall we?

Here’s an object property that makes the attribute that it represents read-only:

class ReadOnly(ObjectProperty):
    def __init__(self, attr):
        self.attr = attr

    def get(self, inst, name):
        return self.attr
And an example of class using it:
class C(WithObjectProperties):
    def __init__(self, a):
        self.a = ReadOnly(a)

That’s all! The class simply needs to derive from WithObjectProperties and assign an attribute with the desired object property. Here’s what it’s like to use this:

>>> c = C(5)
>>> c.a
5
>>> c.a = 6
…
AttributeError: Property 'a' on object <C …> does not allow write access
>>> del c.a
…
AttributeError: Property 'a' on object <C …> does not allow deletion

Just accomplishing this goes a fair way towards making immutability easier in Python (though it’s still not actually immutable, since you can get direct access to the property object via the instance’s __dict__.

Interesting Side Note

One interesting tid bit here is that if we make ReadOnly inherit from WithObjectProperties, it can suddenly be a wrapper that turns other object properties into read-only versions of themselves.

And I love me some wrapper action.

Outro

So, yeah, I’ve gone and created instance-level properties. If this gets enough recognition, I may actually put up a library for it on GitHub and PyPI. Let me know what you think? Am I the only one who cares about this? I know that it’s entirely possible.

Reference: Instance-Level Properties in Python from our WCG partner Jacob Zimmerman at the Programming Ideas With Jake blog.

Jacob Zimmerman

Jacob is a certified Java programmer (level 1) and Python enthusiast. He loves to solve large problems with programming and considers himself pretty good at design.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button