12.2 Tools for Representing Functions
12.2.1 Functions Defined by String Formulas
Matlab has a nice feature in that string representations of mathematical for- mulas can be turned into standard Matlab functions. Our aim is to implement this feature in Python. We shall do this in a pedagogical way, starting with very simple code snippets, then adding functionality, and finally we shall
12.2. Tools for Representing Functions 619 remove all the overhead associated with turning string representations into callable functions (!).
The functionality we would like to have can be sketched through an ex- ample:
f = StringFunction(’1+sin(2*x)’) print f(1.2)
That is, the first line turns the formula ’1+sin(2*x)’ into a function-like object, here stored inf, wherexis the independent variable. The new function object f can be used as an ordinary function, i.e., function values can be computed using a call syntax likef(1.2).
A very simple implementation may be based oneval(see Chapter 8.1.3):
class StringFunction_v1:
def __init__(self, expression):
self._f = expression def __call__(self, x):
return eval(self._f) # evaluate function expression For efficiency we should compile the formula:
class StringFunction_v2:
def __init__(self, expression):
self._f_compiled = compile(expression, ’<string>’, ’eval’) def __call__(self, x):
return eval(self._f_compiled)
These simple classes have very limited use since the formula must be a function of x only. Supplying an expression like ’1+A*sin(w*t)’ requires defining the independent variable as t, with A and w as known parameters.
We may include functionality for this:
f = StringFunction_v3(’1+A*sin(w*t)’, independent_variable=’t’, set_parameters=’A=0.1; w=3.14159’) print f(1.2)
f.set_parameters(’A=0.2; w=3.14159’) print f(1.2)
The set_parameter argument or method takes a string containing Python code for initializing parameters in the function formula. The class now be- comes a bit more involved as we must bring the independent variable and other parameters into play in the__call__ method. This can easily be done withexec(see Chapter 8.1.3):
class StringFunction_v3:
def __init__(self, expression,
independent_variable=’x’, set_parameters=’’):
self._f_compiled = compile(expression, ’<string>’, ’eval’)
self._var = independent_variable # ’x’, ’t’ etc.
self._code = set_parameters def set_parameters(self, code):
self._code = code def __call__(self, x):
# assign value to independent variable:
exec ’%s = %g’ % (self._var, x)
# execute some user code (defining parameters etc.):
if self._code: exec(self._code) return eval(self._f_compiled)
The basic problem with this simple extension is that efficiency is lost. Con- sider the formula sin(x) + x**3 + 2*x. Setting the CPU time of a pure Python function returning this expression to 1.0, I found that the various versions of the three classes above ran at these speeds:
StringFunction_v1: 13 StringFunction_v2: 2.3 StringFunction_v3: 22
That is, compilation of the expression is important, but theexecstatements are very expensive. We can do much better that this: we can in fact obtain the speed as if the formula was hardcoded in a callable instance:
class Func:
def __call__(x):
return sin(x) + x**3 + 2*x
How the overhead of using a string formula as a function can be totally eliminated is explained below.
Our next step in optimizing the string function class is to replace the codeparameter by keyword arguments. This means that the usage is slightly changed:
f = StringFunction_v4(’1+A*sin(w*t)’, A=0.1, w=3.14159) print f(1.2)
f.set_parameters(A=2) print f(1.2)
We introduce a dictionary in the class to hold both the parameters and the independent variable:
class StringFunction_v4:
def __init__(self, expression, **kwargs):
self._f_compiled = compile(expression, ’<string>’, ’eval’) self._var = kwargs.get(’independent_variable’, ’x’) self._prms = kwargs
try: del self._prms[’independent_variable’]
except: pass
def set_parameters(self, **kwargs):
12.2. Tools for Representing Functions 621 self._prms.update(kwargs)
def __call__(self, x):
self._prms[self._var] = x
return eval(self._f_compiled, globals(), self._prms) Now we make use of running eval in a restricted local namespace, here self._prms (see Chapter 8.7.3). First, we simply put all keyword arguments sent to the constructor in this dictionary, and then we remove arguments that are not related to values or parameters. This provides a substantial speed-up:
StringFunction_v4 runs at the same speed as the trivialStringFunction_v2 class.
A natural next step is to allow an arbitrary set of independent variables:
f = StringFunction_v5(’A*sin(x)*exp(-b*t)’, A=0.1, b=1, independent_variables=(’x’,’t’)) print f(1.5, 0.01) # x=1.5, t=0.01
This extension can easily be coded as a subclass ofStringFunction_v4. The idea is just to hold the names of the independent variables as a tuple of strings:
class StringFunction_v5(StringFunction_v4):
def __init__(self, expression, **kwargs):
StringFunction_v4.__init__(self, expression, **kwargs) self._var = tuple(kwargs.get(’independent_variables’,’x’)) try: del self._prms[’independent_variables’]
except: pass
def __call__(self, *args):
# add independent variables to self._prms:
for name, value in zip(self._var, args):
self._prms[name] = value
return eval(self._f_compiled, self._globals, self._prms) This class runs a bit slower than StringFunction_v4: 3.1 versus 2.3 in the previously cited test. This is natural since we run a loop in the __call__
method.
As a test on the understanding of these constructs, the reader is encour- aged to go through an example, say
f = StringFunction_v5(’a + b*x’, b=5) f.set_parameters(a=2)
f(2)
and write down how the internal data structures in thef object change and how this affects the calculations.
We may in fact remove all the overhead of evaluating string expressions if we use the string to construct a (lambda) function and then bind this function to the __call__ attribute (the idea is due to Mario Pernici). Let us assume that the constructor have defined the same attributes as in class StringFunction_v5:
class StringFunction:
def _build_lambda(self):
s = ’lambda ’ + ’, ’.join(self._var)
# add parameters as keyword arguments:
if self._prms:
s += ’, ’ + ’, ’.join([’%s=%s’ % (k, self._prms[k]) \ for k in self._prms])
s += ’: ’ + self._f
self.__call__ = eval(s, self._globals) For a call
f = StringFunction(’A*sin(x)*exp(-b*t)’, A=0.1, b=1, independent_variables=(’x’,’t’)) thesstring in the_build_lambda method becomes
lambda x, t, A=0.1, b=1: A*sin(x)*exp(-b*t)
This is a pure stand-alone Python function, and a call likef(1.2)is of course as efficient as if we had hardcoded the string formula in a separate function.
There is some overhead inf(1.2)because the call is done via a class method, but this overhead can be removed by using the underlying lambda function directly:
f = f.__call__
Because __call__ is a function with parameters as keyword arguments, we may also set parameters in a call asf(x,t,A=0.2,b=1).
So far we have only usedStringFunctionto represent scalar multi-variable functions. We can without any modifications useStringFunction for vector fields. This is just a matter of using standard Python list or tuple notation when specifying the string:
>>> f = S(’[a+b*x,y]’, independent_variables=(’x’,’y’), a=1, b=2)
>>> f(2,1) # [1+2*2, 1]
[5, 1]
Our final, efficient StringFunction class is imported by from scitools.StringFunction import StringFunction
The class has other nice features, e.g., the string formula can be dumped to Fortran 77, C, or C++ code, it has a troubleshoot method for helping to resolve problems with calls, and it has__str__ and__repr__methods. Run
pydoc scitools.StringFunction.StringFunction to see a full documentation with lots of examples.
12.2. Tools for Representing Functions 623