By James Owen Feb 15 2022 • 5 min read

# Adding elegant Getters and Setters with a Fluent Interface in Python with a Resource class

This blog describes a simple technique for implementing accessors with a fluent interface using a Resource class, which reduces the footprint of accessors to match that of using instance variables directly.

While Object Oriented Programming suggests using getters and setters (instead of accessing member variables directly) to provide Abstraction and Encapsulation (google it (opens new window)) , the use of get_xxx() and set_xxx() is not particularly easy to read, and it adds significant boilerplate code for each class member variable.

The use of the Resource class remedies both the readability and the bloat of using accessors by adding a fluent interface of accessors that can be chained together (google it (opens new window)). The idea is to dispense with the "get" and "set" prefixes in the function names and combine the two into a single function: instead of set_foo(5) and get_foo() we use foo(*args) for both. In addition, we implement easy to read method chains providing a fluent interface.

# How Elegant is it?

Once resources have been added to a class, it allows intuitive chains to set things up and then perform a substantial method:

Classname().variable1(value1).variable2(value2).process()

For example, a class for dividing two numbers could read:

Division().divisor(10).dividend(5).calculate()

and would return 2.

# How are Resources added to Existing Classes

The Resource class implements an add_to() member function that's used for each member variable of a target instance; we replace the variable declarations in the constructor with Resource classes that call add_to(self). For example, we translate the class:

class Classname:
    def __init__(self):
        self.variable1 = None
        self.variable2 = 'initial_value2'

to:

class Classname:
   def __init__(self):
        Resource('variable1').add_to(self)
        Resource('variable2').initial_value('initial_value2').add_to(self)

# How is the Resource class implemented

The Resource class implements a fluent interface including an _add_resource internal method that adds both a variable and a dual accessor function to a target instance; it also provides defaults for every setting with methods to override each setting so that resources can be added in a single line. The defaults are:

value Default override
_initial_value None initial_value()
_get_is_allowed True disable_get()
_set_is_allowed True disable_set()

The _add_resource method uses python's attribute functions to implement a single function for initializing, getting and setting member variables. If the number of *args is non-zero, we perform a set operation, and otherwise we perform the get operation. This method aovids adding resources that already exist.

The add_to method calls the _add_resource internal function above guiding it to allow getting, setting or the default of both getting and setting internal variables.

class Resource:
    """
    Allows a single line of code to add a variable with accessors. For example, to add self._foo, self.foo() and self.foo(input):
    Resource('foo').initial_value(5).add_to(self). Has defaults for most settings to minimize boilerplate work.
    """

    def __init__(self, value):
        self._resource = value # accessor method name
        if value.endswith('_'): # we want a public variable
            self._variable = value[:-1]
        else: # private variable
            self._variable = '_' + value
        self._initial_value = None
        self._get_is_allowed = True # get calls allowed
        self._set_is_allowed = True # get calls allowed


    def variable(self, value):
        self._variable = value
        return self

    def initial_value(self, value):
        self._initial_value = value
        return self

    def disable_get(self):
        self._get_is_allowed = False
        return self

    def disable_set(self):
        self._set_is_allowed = False
        return self

    def add_to(self, instance):
        if self._get_is_allowed and self._set_is_allowed:
            self._add_resource(0, instance)
        elif self._set_is_allowed:
            self._add_resource(1, instance)
        else:
            self._add_resource(-1, instance)

    def _add_resource(self, access, instance):
        def add_fn(attr_name, parent):
            cls = parent.__class__

            def fn(instance, *args):
                if len(args) != 0:
                    if access < 0:
                        raise AttributeError
                    setattr(instance, self._variable, args[0])
                    return instance
                if access > 0:
                    raise AttributeError
                return getattr(instance, self._variable)

            setattr(cls, self._resource, fn)

        if (self._resource not in dir(self)):
            _resources = getattr(instance, '_resources', [])
            _resources.append(self._variable)
            setattr(instance, '_resources', _resources)
            setattr(instance, self._variable, self._initial_value)
            add_fn(self._resource, instance)

#Using Resources in everyday Operations For most variables, you will just use the one line declarations in the constructor. When you want to customize an accessor function, simply write the method however you want it; you don't even have to delete the one line declaration, as the code avoids accessors that are customized.

Typically you will use a consistent naming convention for the accessors and the variables. We use Python's standard convention for private variables of prefixing them with an underbar (google it (opens new window)), and for the accessor method we use the convention of the variable name by itself. On the other hand, if you want to access public variables for legacy compatibility (with no underbar prefix), then the accessor method uses an underbar suffix. For example:

  • foo() and foo(bar) access self._foo
  • baz_() and baz_(bar) access self.baz

# How slow are Accessors and Resources?

Using accessor methods conventionally or using the Resource class adds significant overhead (3 to 7 times slowdown on my machine), and creating the accessors for each instance as I'm suggesting adds more (another 100 times slowdown on my machine). You find the timing code here (opens new window). While Python isn't intended for performance, you may need to optimize critical portions by avoiding using the Resource class.

# Conclusion

Using Resources allows readable code based on chains of methods with little boilerplate code, which enables classes with the same footprint as direct use of class variables. You can find the code here (opens new window).

If you have any comments or questions, please contact me at james.owen@virtualtwigs.com or if you have a github account you can leave a comment below.

© James Owen, Feb 15 2022 (updated April 2 2022)