Python

Using the New Python Instance Properties

Last week, I showed you my new implementation for instance-level properties in Python. This week, we’ll take another look at it by implementing a few Delegated Properties and helpers to make using them just a hair nicer.

Recreating Kotlin’s Built-In Delegates

For inspiration of what Delegated Properties to create, we’ll start by recreating the ones built into Kotlin, starting with Lazy.

Lazy

The Lazy Delegated Property allows you to initialize it with a function that takes no parameters and returns an object – preferably one that is relatively expensive to create. Then, the first time the property is retrieved, the object is created a stored, which subsequent lookups will use.

To start us off, let’s create an object that we can use as a flag to represent that the value isn’t initialized yet (we could use None, but it’s possible that the function could return None, and we’d start having some difficulties).

no_value = object()

Using that, here’s Lazy:

class Lazy(DelegatedProperty):
    def __init__(self, initializer, *, readonly=False):
        self.value = no_value
        self.initialize = initializer
        self._readonly = readonly
 
    @classmethod
    def readonly(cls, initializer):
        return cls(intializer, readonly=True)
 
    def get(self, instance, name):
        if self.value is no_value:
            self.value = self.initialize()
            self.initialize = None  # release the reference to prevent memory leak
        return self.value
 
    def set(self, value, instance, name):
        if self._readonly:
            raise AttributeError()
        else:
            self.value = value
            if self.initializer:  # if it's not cleaned up, clean it up
                self.initializer = None

Note the optional read-only with the convenient readonly() class method. I’m not sure what else to say about it. If you have any trouble understanding any of these, ask me about it in the comments.

LateInit

Next up is LateInit. This isn’t quite as handy in Python as it is in Kotlin, since it is largely provided to allow for non-null properties in a class that implements the bean protocol. The problem is that the bean protocol requires a constructor that takes no arguments, where all the fields on the class are initialized to null. In Kotlin, you have null safety, and people like to mark their fields as non-nullable. So, in order to make the bean field non-nullable and yet have them start of null, Kotlin includes lateinit, which represents an uninitialized non-nullable field. If the field is accessed before it is ever set to something, then it triggers an exception. Technically, this isn’t a Delegated Property in Kotlin; it’s a language modifier, but it could be implemented as a Delegated Property.

What use does it have in Python? Well, it’s possible you might want to create a class where some or all of the attributes aren’t initialized right away, – rather they’re set later – but the class requires some or all of those attributes for certain actions. This way, instead of writing verifiers at the beginning of those actions to be certain that they were set, you simply access the attribute, and if it wasn’t set, it’ll raise an exception on its own.

Here’s what such a Delegated Property looks like:

class LateInit(DelegatedProperty):
    def __init__(self, value=no_value, *, readonly=False):
        self.value = value
        self._readonly = readonly
 
    @classmethod
    def readonly(cls, value=no_value):
        return cls(value, readonly=True)
 
    def get(self, instance, name):
        if self.value is no_value:
            raise AttributeError(f"Attribute, {name}, not yet initialized")
        else:
            return self.value
 
    def set(self, value, instance, name):
        if self._readonly and self.value is not no_value:  # the second part allows you to use set to initialize the value later, even if it's read-only
            raise AttributeError()
        else:
            self.value = value

The constructor and readonly() class method include the ability to initialize the property immediately, even though that’s not the typical case, but I like the idea of making it a little more flexible that way.

ByMap

Sometimes, you just want to take in a dictionary in the constructor and have attributes refer to that dictionary for their values. That’s what this Delegated Property does for you. So, it would be used like this:

class ByMapUser:
    a = InstanceProperty(ByMap)
    b = InstanceProperty(ByMap.readonly)
 
    def __init__(self, dict_of_values):
        # dict_of_values should have entries for 'a' and 'b', or else
        # using either property could cause a KeyError, although setting
        # before getting will add the entry to the dict if it wasn't
        # already there
        self.a.initialize(dict_of_values)
        self.b.initialize(dict_of_values)

And here, a and b will read from and write to the provided dictionary instead of to the instance’s __dict__.

class ByMap(DelegatedProperty):
    def __init__(self, mapping, *, readonly=False):
        self.mapping = mapping
        self._readonly = readonly
 
    @classmethod
    def readonly(cls, mapping):
        cls(mapping, readonly=True)
 
    def get(self, instance, name):
        return self.mapping[name]
 
    def set(self, value, instance, name):
        if self._readonly:
            raise AttributeError()
        else: 
            self.mapping[name] = value

Here is where passing in name to get() and set() methods really comes in handy. Without the automatic calculating and passing of names, this would be more tedious to implement and use. The name would have to be taken in through the constructor, which means the user would have to provide it with initialize call. And that means that it would have to be updated by hand if they ever decide to change the name in a refactoring.

Observable

Kotlin also has the Observable Delegated Property, which I don’t see as being all that useful, so I won’t bother showing you that one.

Custom Delegated Property: Validate

One of the biggest reasons we ever need properties is to ensure that what is provided is a valid value to be given. While using the built-in Python property isn’t bad for this use case, it does still require the user to have to think about where to actually store the value, and it requires the verbosity of creating a method. But the biggest shortfall is when you need to reuse the logic of that property elsewhere. You could implement a full-blown descriptor for it, but that’s also excessive. With Validate, all you really need to implement is the validation function and possibly a helper function for creating the specific validator.
def positive_int(num):
return isinstance(num, int) and num > 0

def positive_int_property():
return InstanceProperty(Validate.using(positive_int))

class Foo:
bar = positive_int_property()

def __init__(self, num):
self.bar.initialize(num)

See how you could just write a simple validator function and use that with InstanceProperty(Validate.using(<function>))? Pretty nice, right? Let’s see what Validate looks like, shall we?

class InvalidValueError(TypeError): pass
 
 
class Validate(DelegatedProperty):
    def __init__(self, validator, value,  *, readonly=False):
        self.validate = validator
        if self.validate(value):
            self.value = value
        else:
            raise InvalidValueError()
        self._readonly = readonly
 
    @classmethod
    def using(cls, validator, *, readonly=False):
        return lambda value: cls(validator, value, readonly=readonly)
 
    @classmethod
    def readonly_using(cls, validator):
        return cls.using(validator, readonly=True)
    
    def get(self, *args):
        return self.value
 
    def set(self, value, instance, name):
        if self._readonly:
            raise AttributeError()
        elif self.validate(value):
            self.value = value
        else:
            Raise InvalidValueError(f"{name}, {value}")  # needs a better message, I know

We had to unfortunately use lambda to implement the class methods. It could have been done by returning an inner function as well, but the lack of need for a name makes that excessive.

It also kind of sucks that we can’t do a proper error message in __init__() because we don’t have access to the name and instance. To get around this, I’m passing those arguments in as named arguments in initialize in the final version on GitHub (I wrote Validate for the first time after last week’s article and didn’t want to present any changes to last week’s code here). The Delegated Properties that don’t use them can just add **kwargs into their __init__() signature and ignore it.

Helpers

Shortening InstanceProperty

Always instantiating an InstanceProperty using that full name is tedious, and could potentially be enough to keep people from using it in the first place. So let’s make a shorter-named function that delegates to it:

def by(instantiator):
    return InstanceProperty(instantiator)

I chose “by” as the name since it reflects the keyword used in Kotlin. You can use whatever name you want.

Why don’t I just rename InstanceProperty to something shorter? Because anything shorter would likely lose meaning, and we always want our names to have meaning.

Read Only Wrappers

I have a couple ideas for this, both of which aren’t compatible with 100% of Delegated Properties. Both of these make it so we don’t need to have a class method named “readonly”, and otherwise make implementing Delegated Properties easier.

The first is just a convenience function:

def readonly(instantiator):
    return partial(instantiator, readonly=True)

Which can then be used like this:

class Foo:
    bar = by(readonly(LateInit))
    …

Seeing that Delegated Properties aren’t required to have the readonly parameter, this isn’t 100% compatible. The respective properties should document whether or not they work with this function or not. This applies to the next one as well.

class ReadOnly(DelegatedProperty):
    def __init__(self, delegate):
        self.delegate = delegate
 
    @classmethod
    def of(cls, delegate):
        return lambda *args, **kwargs: cls(delegate(*args, **kwargs))
 
    def get(self, instance, name):
        return self.delegate.get(instance, name)

Readonly is used in the following way:

class Foo:
    bar = by(ReadOnly.of(LateInit))
    ...

It’s annoying that we had to return a function within a function again, but when InstanceProperty needs a function for instantiating a Delegated Property, there’s not a whole lot of choice.

Outro

So that’s everything. Again, this is all in a GitHub repo, which turns out to be the same repo (descriptor-tools) I made for my book. The new code is added, but not all the tests and documentation are written for it yet. When that’s done, I’ll submit the rest to PyPI so you can easily install it with pip.

Thanks for reading, and I’ll see you next week with an article about the MVP pattern.

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
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button