Define Functions Iteratively With Python
Posted on Fri 01 January 2021 in Python • 4 min read
An interesting problem came up recently, there was a piece of code absolutely full of the same function calls over and over again, meaning if anything ever need to change, that would have to be changed in over 500 places, not ideal. Thoughts go back to single responsbility, and don't repeat yourself principles for software engineering. So research & thinking begun on the best way to manage this issue. The first thing that came to mind, how could we define these functions and their combinations iteratively.
Before we dive into this could be implemented, we need to really understand the problem.
The use case for this repeated code, was to check the variables being passed to an endpoint were what they were expected to be. For example, if an endpoint is awaiting for a string, and an optional number, we want to check these before the operation goes through and potentially breaks something else down the line (bringing us back to the crash early principle).
We'll start by defining two functions which will check that a variable is the type it's expected to be, and another to ensure it exists (not None in Python).
def check_type(value, variable_type, variable_name):
if type(value) != variable_type:
raise Exception(f"Variable '{variable_name}' is invalid type! Expected: {variable_type}.")
return value
def check_exists(value,variable_name):
if value is None:
raise Exception(f"Variable '{variable_name}' is None! Check variable exists.")
return value
Now that we've defined these functions, let's test that they work as expected and raise Exceptions when a problem statement comes up.
check_type(24,int,'lucky_number')
check_type('Hello world', float, 'I thought this was a number')
x = 55
y = None
check_exists(x,'Fifty five')
check_exists(y, 'Fifty six')
Defining Functions Iteratively¶
Now let's make use of the beauty that is looping to create all the combinations for us to use! We're going to encapsulate all these functions inside a dictionary to encapsulate them and provide a common interface for developers to use.
def log_and_raise(exception_text):
# Add logging here
raise Exception(exception_text)
def create_validators(types):
validators = {}
for variable_type in types:
validators[f"{variable_type.__name__}"] = lambda value, variable_type=variable_type: value if type(value) == variable_type else log_and_raise(f"Variable isn't of type '{variable_type.__name__}'! D:")
return validators
validate = create_validators([str,float, int])
Now in a handful lines of code, we've created a dictionary with a way to easily generate functions to check variable types, and then log out the error (eg, write to a file) and raise an exception.
Before we deconstruct what's happening here, let's see it in action.
validate['str']('This is a string!')
validate['int'](42)
validate['float'](42.42)
x = 'The number forty two'
validate['str'](x)
Fantastic, as we can see, it's not throwing any errors and continuing through our validations, now let's ensure our exception is raised (and subsequently any logging would be completed).
validate['str'](42)
Even better, we get raise an exception when our validation fails ensuring to alert the developers with information about why it failed. Now let's deconstruct how we created it in depth.
Deconstruction of How¶
Admittedly, there's a lot going on in those handful of lines which isn't obvious as to whats happening.
First we define the overarching functions which contains the creation of all these functions, and thereafter initialise a dictionary to store all the following functions within. Next we loop over each of the types provided as a list to the function to create an entry in the dictionary using the __name__
dunder function (eg, str
has a dunder __name__
of 'str'), this let's our developers use the type they want as the key of the dictionary when wanting to validate a variables type.
Lambdas!¶
The trickiest part here is how we are actually defining the functions. We make use of the lambda operator in Python to create anonymous functions. The structure of a lambda function definition follows:
lambda arguments: true_statement if conditional_statement else false_statement
We make use of a keyword argument of the variable_type
in our loop otherwise the variable_type
from the list passed in won't be correctly passed into the lambda function (which we won't discuss in this post).
Finally we make use of an external function to centralise how we handle errors (making it easy to keep a consistent logging approach), and raise an Exception within that function to ensure any logging occurs before the program ultimately exits.
Conclusion¶
There are pros and cons to this approach to this problem.
Pros:
- Concise way of creating lots of functions
- Consistent interface to use
- Stores all similar functions inside one object (dictionary)
Cons:
- Not straightforward as to how it works
- Not straightforward to change functionality