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):
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:
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:
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 = KWARG(‘xdim’,300,int,kwargs)
027| ydim = 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:
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:
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:
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:
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:
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
!
∅
ATTENTION: The WordPress Reader strips the formatting of posts and doesn’t render them correctly. If you’re reading this in the Reader, I highly recommend and urge you to [A] stop using the Reader and [B] always read blog posts on their website.
This post is: Loving the Lambda
In fact, in a fully implemented mapping object, the
get
method allows a default value. Usually accessing a map with a key that doesn’t exist generates an error, but theget
method just returns the default (orNone
if no default is provided).An even easier way to deal with plurals is to not use a construct like:
And just use:
Which doesn’t have the ‘s’ problem with 1 that the first construct does.