Conditions

Introduction

Python lacks the power, flexibility — and also the quirks — of the C++ preprocessor. It does not support conditional compilation. When implementing numerical routines, one faces the dilemma: many sanity checks are essential during the research and development phase, but introduce a prohibitive performance hit in production. Yet, some checks are also essential in production — as evidenced by the relatively recent electronic trading disasters, which they could have helped avoid.

As we said, there is no conditional compilation in Python. But to some extent it may be simulated using decorators. We make extensive use of decorators in thalesians.tsa. One place, where they are particularly useful, is the evaluation of pre- and post-conditions.

First, let us load some modules...


In [1]:
import os, sys
sys.path.append(os.path.abspath('../../main/python'))
from thalesians.tsa.conditions import precondition, postcondition

Pre- and post-conditions using decorators

Consider the following (somewhat contrived) example:


In [2]:
class Subtractor(object):
    @precondition(lambda self, arg1, arg2: arg1 >= 0, 'arg1 must be greater than or equal to 0')
    @precondition(lambda self, arg1, arg2: arg2 >= 0, 'arg2 must be greater than or equal to 0')
    @postcondition(lambda result: result >= 0, 'result must be greater than or equal to 0')
    def subtract(self, arg1, arg2):
        return arg1 - arg2

(Notice how lambdas facilitate lazy evaluation. We often use them in thalesians.tsa to avoid computing things unnecessarily.)

Now, the following will pass the conditions:


In [3]:
subtractor = Subtractor()
subtractor.subtract(300, 200)


Out[3]:
100

Whereas the following would raise an AssertionError:

subtractor.subtract(-300, 200)
--------------------------------------------------------------------------- AssertionError Traceback (most recent call last) in () ----> 1 subtractor.subtract(-300, 200) S:\dev\tsa\src\main\python\thalesians\tsa\conditions.py in wrapper(*args, **kwargs) 14 def wrapper(*args, **kwargs): # NB: No self 15 if pre is not None: ---> 16 check(pre(*args, **kwargs), message=lambda: message, level=level) 17 retval = func(*args, **kwargs) 18 if post is not None: S:\dev\tsa\src\main\python\thalesians\tsa\checks.py in check(arg, message, level) 11 if is_callable(message): 12 message = message() ---> 13 raise AssertionError(message) 14 15 def check_none(arg, message='Argument is not None', level=1): AssertionError: arg1 must be greater than or equal to 0

How can we selectively enable/disable pre- and post-conditions? Notice that the decorators precondition and postcondition take the optional argument level, which defaults to 1. In tsa_settings.py we declare MIN_PRECONDITION_LEVEL and MIN_POSTCONDITION_LEVEL. They default to 1 if __debug__ and sys.maxsize if not. The user can override these in a local_tsa_settings module of his/her project, e.g.


In [4]:
MIN_PRECONDITION_LEVEL = 5
MIN_POSTCONDITION_LEVEL = 7

Then all the preconditions with levels strictly less than 5 and all the postconditions with levels strictly less than 7 will not be evaluated.

We note that our implementation of conditions through decorators was inspired by J.F. Sebastian's response on http://stackoverflow.com/questions/12151182/python-precondition-postcondition-for-member-function-how