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)