Python Coding Style – Is Using Lambdas to Express Intent Pythonic?

coding-stylelambdapythonpython-3.x

PEP 8 states the following about using anonymous functions (lambdas)

Always use a def statement instead of an assignment statement that
binds a lambda expression directly to an identifier:

# Correct: def f(x): return 2*x

# Wrong: f = lambda x: 2*x

The first form means that the name of the resulting function object is
specifically f instead of the generic <lambda>. This is more
useful for tracebacks and string representations in general. The use
of the assignment statement eliminates the sole benefit a lambda
expression can offer over an explicit def statement (i.e. that it can
be embedded inside a larger expression)

However, I often find myself being able to produce clearer and more readable code using lambdas with names. Consider the following small code snippet (which is part of a larger function)

divisors = proper(divisors)
total, sign = 0, 1

for i in range(len(divisors)):
    for perm in itertools.combinations(divisors, i + 1):
        total += sign * sum_multiplies_of(lcm_of(perm), start, stop - 1)
    sign = -sign
return total

There is nothing wrong with the code above from a technical perspective. It does precisely what it intends to do. But what does it intend to do? Doing some digging one figures out that oh right, this is just using the inclusion-exclusion principle on the powerset of the divisors. While I could write a long comment explaining this, I prefer that my code tells me this. I might do it as follows

powerset_of = lambda x: (
    itertools.combinations(x, r) for r in range(start, len(x) + 1)
)
sign = lambda x: 1 if x % 2 == 0 else -1
alternating_sum = lambda xs: sum(sign(i) * sum(e) for (i, e) in enumerate(xs))
nums_divisible_by = lambda xs: sum_multiplies_of(lcm(xs), start, stop - 1)

def inclusion_exclusion_principle(nums_divisible_by, divisors):
    return alternating_sum(
        map(nums_divisible_by, divisor_subsets_w_same_len)
        for divisor_subsets_w_same_len in powerset_of(proper(divisors))
    )

return inclusion_exclusion_principle(nums_divisible_by, divisors)

Where lcm_of was renamed to lcm (computes the lcm of a list, not included here). Two keypoints 1) The lambdas above will never be used elsewhere in the code 2) I can read all the lambdas and where they are used on a single screen.

Contrast this with a PEP 8 compliant version using defs

def powerset_of(x):
    return (itertools.combinations(x, r) for r in range(start, len(x) + 1))

def sign(x):
    return 1 if x % 2 == 0 else -1

def alternating_sum(x):
    return (sign(i) * sum(element) for (i, element) in enumerate(x))

def nums_divisible_by(xs):
    return sum_multiplies_of(lcm(xs), start, stop - 1)

def inclusion_exclusion_principle(nums_divisible_by, divisors):
    return alternating_sum(
        map(nums_divisible_by, divisor_subsets_w_same_len)
        for divisor_subsets_w_same_len in powerset_of(proper(divisors))
    )

return inclusion_exclusion_principle(nums_divisible_by, divisors)

Now the last code is far from unreasonable,but it feels wrong using def for simple one-liners. In addition the code length quickly grows if one wants to stay PEP 8 compliant. Should I switch over to using defs and reserve lambdas for truly anonymous functions, or is it okay to throw in a few named lambdas to more clearly express the intent of the code?

Best Answer

You're sort of approaching it like a mathematician, where the purpose of writing the supporting functions is to "prove your work." Software isn't generally read that way. The goal is usually to choose good enough names that you don't have to read the helper functions.

You likely know what a powerset or alternating sum is without reading the code. If you're writing a lot of code like this, those sorts of helper functions are even likely to end up grouped in a common module in a completely separate file.

And yes, defining a named function feels a little verbose for a short function, but it's expected and reasonable for the language.You're not trying to minimize the overall code length. You're trying to minimize the length of code a future maintainer actually has to read.

Related Topic