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).

In [12]:
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.

In [10]:
check_type(24,int,'lucky_number')
check_type('Hello world', float, 'I thought this was a number')
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-10-bb70c914c0df> in <module>
      1 check_type(24,int,'lucky_number')
----> 2 check_type('Hello world', float, 'I thought this was a number')

<ipython-input-6-22c582f36d19> in check_type(value, variable_type, variable_name)
      1 def check_type(value, variable_type, variable_name):
      2     if type(value) != variable_type:
----> 3         raise Exception(f"Variable '{variable_name}' is invalid type! Expected: {variable_type}.")
      4 
      5 def check_exists(value,variable_name):

Exception: Variable 'I thought this was a number' is invalid type! Expected: <class 'float'>.
In [11]:
x = 55
y = None
check_exists(x,'Fifty five')
check_exists(y, 'Fifty six')
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-11-355540618803> in <module>
      2 y = None
      3 check_exists(x,'Fifty five')
----> 4 check_exists(y, 'Fifty six')

<ipython-input-6-22c582f36d19> in check_exists(value, variable_name)
      5 def check_exists(value,variable_name):
      6     if value is None:
----> 7         raise Exception(f"Variable '{variable_name}' is None! Check variable exists.")

Exception: Variable 'Fifty six' is None! Check variable exists.

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.

In [140]:
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.

In [141]:
validate['str']('This is a string!')

validate['int'](42)

validate['float'](42.42)

x = 'The number forty two'

validate['str'](x)
Out[141]:
'The number forty two'

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).

In [142]:
validate['str'](42)
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-142-fd464241a319> in <module>
----> 1 validate['str'](42)

<ipython-input-140-4899cd219b78> in <lambda>(value, variable_type)
      6     validators = {}
      7     for variable_type in types:
----> 8         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:")
      9     return validators
     10 

<ipython-input-140-4899cd219b78> in log_and_raise(exception_text)
      1 def log_and_raise(exception_text):
      2     # Add logging here
----> 3     raise Exception(exception_text)
      4 
      5 def create_validators(types):

Exception: Variable isn't of type 'str'! D:

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