24

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?

8 Answers8

68

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.

Karl Bielefeldt
  • 148,830
27

Despite the Zen of Python, there is sometimes more than one obvious way to do it.

I agree that your preferred way to phrase this code has a certain functional elegance to it.

But it's also plain to see that your preference for this style is purely aesthetical/subjective, and that PEP-8 gives objective reasons why named defs are preferable.

My recommendations:

  • Are you writing this code for yourself? Do whatever you prefer. Don't stick slavishly to a standard that annoys you. PEP-8 is not infallible, and it can be entirely reasonable to deviate from it.

  • Are you sharing this code with other people? Just stick with PEP-8 and common formatting/linting tools for uniformity's sake, unless you have a really strong argument why they are wrong in a specific case. For example, a lot of people reasonably disagree with the PEP-8 line length limit of 79 columns. I also disagree with pylint's default setting of requiring a docstring for every function.

  • Consider whether you'd prefer using Haskell for these kind of problems. The Python you want to write looks a lot like idiomatic Haskell code.

amon
  • 135,795
23

"Pythonic" is not an objective standard. It really means "code that an experienced python programmer likes". Turns out "experienced python programmers" don't all universally have the same taste in code.1

As someone who has written a lot of functional style code in Python, and who frequently takes PEP-8 with a grain of salt, I personally think PEP-8 gets this one right. I have written many functions containing other local function definitions, and usually do them with def statements rather than assigning lambdas straight to variables. The main reason is that when I see:

def some_function(...):
  ...

I know I can skim over the indented block if I'm not reading in depth; it wont do anything "now", only when it's called. The indentation naturally "highlights" the shape of the code I don't need to read, and my editor is probably syntax highlighting the def some_function part, so this is extremely recognisable and readable.

As such, a function that starts with a few local function definitions is very easily skimmable; I know it has a collection of "auxiliary definitions" and can start reading what this function actually does after those definitions.

On the other hand, when a function starts with:

powerset_of = ...(
    ...
)
sign = ...
alternating_sum = ...
nums_divisible_by = ...

I'm normally expecting to have to glance at those ...s a bit. The code there is running "now", and may have side effects. It takes a little more effort to recognise (and delimit the extent of) lambda expressions to verify that all of those assignments aren't doing anything yet.

To me, lambdas are for functions that are so short and simple that reading them on their own, out-of-line from where they are used, makes them harder to understand. If you're defining the function out-of-line and giving it a name anyway, def is easier to read and more flexible. Consider that even your powerset_of is long and complex enough that you felt the need to split it over more than one line.

But that's all my taste. If my arguments haven't convinced you, and you're the only one responsible for the coding style of your codebase, feel free to do it the way that feels most readable to you.


1 Things like PEP-8, Zen of Python, and declarations from the BDFL about whether something is "pythonic", are all attempts to sway python programmers in general towards all having the same tastes. They are "propaganda" of a sort, not objective truth.

Of course coding style tastes are almost never wholly arbitrary either, and I'm certainly not saying that PEP-8 et al are totally just someone's subjective preference. There is objective reasoning behind these rules, but in the end they come down to subjective value judgements about which objective criteria should be traded off against another in various situations.

Ben
  • 1,047
4

lambdas remove an indication that the definition is a function. I think this makes it harder to read as you have lost information.

IDEs work less well you lose their searching and autocompletion of function names and the highlighting of the use of the function. (or even simple grep for functions)

As for length lets see as you are doing one liners

sign = lambda x: 1 if x % 2 == 0 else -1

and

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

It is only 3 characters difference.

mmmmmm
  • 238
  • 2
  • 10
2

The answer is right there, in your quote:

This is more useful for tracebacks and string representations in general

That is, if you have an exception thrown from inside your function, it would be named, if you have declared it with def:

Traceback (most recent call last):
  File "p_fun.py", line 6, in <module>
    main()
  File "p_fun.py", line 4, in main
    f(0)
  File "p_fun.py", line 1, in f
    def f(x): return 2/x
ZeroDivisionError: division by zero

versus

Traceback (most recent call last):
  File "p_lambda.py", line 6, in <module>
    main()
  File "p_lambda.py", line 4, in main
    f(0)
  File "p_lambda.py", line 1, in <lambda>
    f = lambda x: 2/x
ZeroDivisionError: division by zero

Well, since the guide was written, the interpreter started to also print the source line, but maybe in some cases it does not.

max630
  • 2,605
1

Remember that defs can be nested, so another PEP8-compliant implementation would be

def inclusion_exclusion_principle(nums_divisible_by, divisors):
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)

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)

That has the benefits of both: (a) the PEP8 clarity (and compliance) of using def rather than = lambda; and (b) the local scope clarity of the inner functions never being used (or even visible) elsewhere in the code and being adjacent to where they're used.

0

A positive approach to answer this: lambdas come from the "lambda calculus", a rather important area of Computer Science, which, in a nutshell is concerned with exploring what happens if you use functions as "first class citizens". I.e., functions which can themselves be used as values and thus can be given as parameters to other functions. This used to be uncommon in practical programming languages (i.e., many languages are not able to do so, or require quirks like pointers or wrapper objects to emulate functions as values).

This is represented in the Python implementation of lambda through the fact that lambda is an expression, not a statement, i.e., the result of the lambda itself is a value which you can assign to variables as shown in your example; but can also and more importantly used anonymously, for example within a method call itself:

my_func(lambda ...)

This is the unique aspect of lambdas which sets them apart from definitions - you cannot write the following:

my_func(def ...)

This is exactly what PEP 8 says about it. If you are directly assigning your lambda to a variable, and then doing nothing with your variable except using it to call the function defined thusly, there is simply no point to using lambda in the first place. Exaggerated: If that is all we would ever be doing, they could have simply left the lambda keyword out of the language, and it would be better to do so as it would keep the language simpler while not losing any expressiveness.

You do not even need to go to the PEP 8 - it starts with the lambda chapter in the official language reference, which begins with:

Lambda expressions (sometimes called lambda forms) are used to create anonymous functions.

The reference also is rather short and goes on to say that it is equivalent to def (aside of the expression vs. statement aspect).

So if this language statement has one reason for existence (the anonymity) it stands to reason that it should be used in this way if at all. Of course, opinions, taste etc. matter too, but the function of a style guide is to provide an opinion, so it makes sense that it is in accordance with the language feature itself.

AnoE
  • 5,874
  • 1
  • 16
  • 17
-1

Honestly, this looks like a Haskel code more than anything else. Python is all about readability. Your code looks a lot like my practice math register. No one reads my register so it's all good. But when someone does need to read it, he will need probably the same amount of time I needed to solve it or more.

So if you are doing this for yourself and do not have the idea to showcase it or anything of that sort then it's all great. Though if you are, then it's better to make it 'easier to read' as much as possible.

Daone
  • 7
  • 2