Tags

, , , ,

One part of Python I especially appreciate is lambda functions. While I’ve never pursued functional programming, I do like many things about it, particularly the notion of functions as native data objects. Programming with functional objects opens new vistas. Most languages handle it one way or another, but languages make it natural.

Python’s lambda is such a facility, and I use it often. This week I finally got around to writing a lambda function I’ve been meaning to for a long time, and it’s my new favorite. I thought I’d share it along with some of my other “one-liners”

The reason for this “macro” is that, more and more, I find myself using the following framework for complex functions (especially including object initializers):

001| # Using KWARGS allows passing lots of parameters to a function.
002| # It keeps the function signature very clean, but gives up the
003| # self-documenting of formal parameter names. It also allows
004| # unknown parameters, but this can be a feature if you pass
005| # KWARGS to multiple or sub functions. As always, it’s choice.
006| #
007| def interesting_function (**kwargs):
008|     ”’My interesting function needs many parameters!”’
009|     xdim = int(kwargs[‘xdim’]) if ‘xdim’ in kwargs else 300
010|     ydim = int(kwargs[‘ydim’]) if ‘ydim’ in kwargs else 300
011|     xmin = float(kwargs[‘xmin’]) if ‘xmin’ in kwargs else 5.0
012|     xmax = float(kwargs[‘xmax’]) if ‘xmax’ in kwargs else +5.0
013|     ymin = float(kwargs[‘ymin’]) if ‘ymin’ in kwargs else  0.0
014|     ymax = float(kwargs[‘ymax’]) if ‘ymax’ in kwargs else +1.0
015|     xaxis = kwargs[‘xaxis’] if ‘xaxis’ in kwargs else ‘Months’
016|     yaxis = kwargs[‘yaxis’] if ‘yaxis’ in kwargs else ‘Trains’
017|     #
018|     # …interesting stuff…
019|     #
020| 
021| def interesting_class (object):
022|     def __init__ (self, **kwargs):
023|         ”’My interesting object has lots of options!”’
024|         xdim = int(kwargs[‘xdim’]) if ‘xdim’ in kwargs else 300
025|         ydim = int(kwargs[‘ydim’]) if ‘ydim’ in kwargs else 300
026|         #
027|         # …etc…
028|         #
029| 
030| 

This provides maximum flexibility in terms of parameters (but [*]). I like writing functions with lots of optional settings, things that can usually be ignored because the default setting values work in most cases. But sometimes you want to tweak something, and it’s nice to have that option.

This does require good documentation discipline to record all those options, but I think it’s more than worth the effort involved. What’s a pain is writing that expression. It isn’t just the tedium factor, it’s that two names have double occurrences (kwargs and the key string). Besides being extra tedious, it’s a synchronization problem, especially for the key string. Note the similarity of some parameter names; it’s easy to make a typo.

[*] In this form, one of its features can be a limitation, but correcting the limitation removes the flexibility. The feature is allowing unknown or unused parameters in kwargs, but this limits error-checking. A misspelled parameter name can slip through, which only causes that parameter value to be ignored. Depending on default settings, that might be subtle bug.

§

I’ve always known I should make a macro (a lambda expression). Python very likely has something already built in (or in its library). but it’s just as simple to write my own. An obvious first pass is:

001| # KWARG macro takes a parameter name, default value, and a
002| # set of keyword/values (a dict or similar duck). If the
003| # parameter exists in the dict, the function returns its
004| # value, otherwise it returns the default value.
005| #
006| KWARG = lambda name,dflt,args:args[name] if name in args else dflt
007| 
008| # This saves typing and eliminates a bug opportunity:
009| def interesting_function (**kwargs):
010|     ”’My interesting function needs many parameters!”’
011|     xdim = int(KWARG(‘xdim’,300,kwargs))
012|     ydim = int(KWARG(‘ydim’,300,kwargs))
013|     xmin = float(KWARG(‘xmin’,5.0,kwargs))
014|     xmax = float(KWARG(‘xmax’,+5.0,kwargs))
015|     ymin = float(KWARG(‘ymin’, 0.0,kwargs))
016|     ymax = float(KWARG(‘ymax’,+1.0,kwargs))
017|     xaxis = KWARG(‘xaxis’,‘Months’,kwargs)
018|     yaxis = KWARG(‘yaxis’,‘Trains’,kwargs)
019|     #
020|     # …interesting stuff…
021|     #
022| 
023| def interesting_class (object):
024|     def __init__ (self, **kwargs):
025|         ”’My interesting object has lots of options!”’
026|         xdim = int(KWARG(‘xdim’,300,kwargs))
027|         ydim = int(KWARG(‘ydim’,300,kwargs))
028|         #
029|         # …etc…
030|         #
031| 

This eliminates the double occurrence of names and makes it easier to spot bugs. I’ve found I often need to cast the input argument (typically a string from the command line or config file) to a numeric type. This version is less clean than it could be, because the default value is invariably already the right type.

There is also that the visual left-right offset due to the type name and parentheses make it harder to visually check correctness (which depends, in part, on alignment; see the example in this post).

Thanks to Python treating types as first-class objects, here’s a better way:

001| # KWARG macro takes a parameter name, default value, and set
002| # of keyword/values (a dict or similar duck). If the parameter
003| # exists in the dict, the function returns its value. If not,
004| # it returns the default value.
005| #
006| KWARG = lambda nam,val,typ,kws: typ(kws[nam]) if nam in kws else val
007| 
008| # This saves typing and eliminates a bug opportunity:
009| def interesting_function (**kwargs):
010|     ”’My interesting function needs many parameters!”’
011|     xdim = KWARG(‘xdim’,300,int,kwargs)
012|     ydim = KWARG(‘ydim’,300,int,kwargs)
013|     xmin = KWARG(‘xmin’,5.0,float,kwargs)
014|     xmax = KWARG(‘xmax’,+5.0,float,kwargs)
015|     ymin = KWARG(‘ymin’, 0.0,float,kwargs)
016|     ymax = KWARG(‘ymax’,+1.0,float,kwargs)
017|     xaxis = KWARG(‘xaxis’,‘Months’,str,kwargs)
018|     yaxis = KWARG(‘yaxis’,‘Trains’,str,kwargs)
019|     #
020|     # …interesting stuff…
021|     #
022| 
023| def interesting_class (object):
024|     def __init__ (self, **kwargs):
025|         ”’My interesting object has lots of options!”’
026|         xdim = int(KWARG(‘xdim’,300,int,kwargs))
027|         ydim = int(KWARG(‘ydim’,300,int,kwargs))
028|         #
029|         # …etc…
030|         #
031| 

Things align much nicer now. A vast improvement!

§ §

As an aside, using types as objects is a handy feature. Suppose we want something like the range object, but want it to provide float or string values as well as int values? We can make a simple range factory:

001| # Fun with types…
002| 
003| def range_factory (typ=int):
004|     def _range (*args):
005|         for x in range(*args):
006|             yield typ(x)
007|     return _range
008| 
009| ri = range_factory()
010| 
011| print( [n for n in ri(4)] )
012| print( [n for n in ri(8)] )
013| print( [n for n in ri(12)] )
014| print()
015| 
016| rf = range_factory(float)
017| 
018| print( [n for n in rf(4)] )
019| print( [n for n in rf(8)] )
020| print( [n for n in rf(12)] )
021| print()
022| 
023| rs = range_factory(str)
024| 
025| print( [n for n in rs(4)] )
026| print( [n for n in rs(8)] )
027| print( [n for n in rs(12)] )
028| print()
029| 

If we run this, it prints:

[0, 1, 2, 3]
[0, 1, 2, 3, 4, 5, 6, 7]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

[0.0, 1.0, 2.0, 3.0]
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]
[0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0]

['0', '1', '2', '3']
['0', '1', '2', '3', '4', '5', '6', '7']
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']

Very handy!

§ §

Back to the lambda, here are some from my goodies module:

001| ”’A ‘s’ unless n=1.”’
002| plural = lambda n:  if n == 1 else ‘s’
003| 
004| ”’Turn a list into a string.”’
005| lst2str = lambda lst: .join([str(x) for x in lst])
006| lst2str2 = lambda lst,delim: delim.join([str(x) for x in lst])
007| 
008| ”’Pretty Print a complex number.”’
009| zstr = lambda z: (‘(%+.5f, %+.5fi)’ % (z.real,z.imag))
010| 
011| ”’Return an object’s typename.”’
012| typename = lambda obj: type(obj).__name__
013| 
014| ”’Compare two numerical values.”’
015| comp = lambda a,b: ((b < a)  (a < b))
016| 
017| ”’Fraction.”’
018| frac = lambda a,b: (float(a) / float(b)) if b else 0.0
019| 
020| ”’Return a Pythagorean distance on a vector of coordinates.”’
021| distance = lambda ds: sqrt(sum([float(d)*float(d) for d in ds]))
022| 
023| ”’Return the dot product of two vectors.”’
024| dot_product = lambda a,b: sum([float(x)*float(y) for x,y in zip(a,b)])
025| 

They should be more or less self-explanatory. The plural function is useful in cases like this:

001| records = Query(query_parameters)
002| 
003| n = len(records)
004| s = plural(records)
005| print(‘Found %d record%s’ % (n,s))
006| 

The two forms of joining a list into a string are so common I made functions for both. The lst2str for all those times of just concatenating list items, and lst2str2 for when I want some kind of delimiter between items. (Implementing this as a single function would allow making the delimiter default to the empty string. Two birds!)

The zstr function is just a pretty print for complex numbers, and the typename function extracts a type’s actual name. (Which is nicer.)

The comp function replaces Python’s now obsolete (and gone) cmp function. It returns +1 if the first item is larger, -1 if it’s smaller, and 0 if they equal. (Alternate view: +1 means first item is larger; -1 means second item is larger.) Weird thing is, I missed cmp at first, but since I made comp, I’ve hardly used it.

I’ve had a similar experience with reduce, which is a formerly beloved function now obsolete (and gone). I quickly wrote a replacement, but found I hardly use it. Having to import a function, rather than having it ever-present, apparently makes me think twice about using it.

The frac function is a safe way to divide two numbers. It just returns 0.0 if the divisor is zero. It prevents errors in, for example output routines that calculate averages and can sometimes try to average a list of zero things, which leads to:

\displaystyle\frac{\mathsf{sum}(list)}{\mathsf{count}(list)}

And if the count is zero, then the math blows up, but in most output cases it’s okay to just output 0.0 when the divisor is zero. The frac function is helpful in such cases. (The real answer is neither zero nor infinity, but undefined. There are sound mathematical reasons the first two don’t work.)

The distance and dot_product functions are more to show it’s possible; I don’t think I’ve ever actually used them. When I do work with vectors, I use classes that have these as methods, and I rarely (if ever) work with vectors as just lists. (But if I ever do, I’ve got these handy.)

§

Lastly, here are some just for fun:

001| Factorial = lambda n: 1 if n<1 else n*Factorial(n1)
002| 
003| Collatz_nbr = lambda n: ((n*3)+1) if (n % 2) else (n//2)
004| 
005| Collatz_seq = lambda n: [n] if (n in [1,0,+1])
006|                             else [n]+Collatz_seq(Collatz_nbr(n))
007| 

It’s easy to implement the factorial function (although the version in the math module is much better; use that).

There are two Collatz functions: one, given a number, returns the next number in the sequence, the other returns the entire sequence given a starting number. Note the latter is recursive (lambda functions can call themselves) and it also calls the first Collatz function. Just a sampling of what’s possible with one-line lambda functions!

§ §

They’re also very handy for setting up conversions, say between kilometers and miles. My code is peppered with them (a quick scan turns up 454 occurrences among my Python files). I do love the lambda!