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?

18
  • 31
    You seem to be tacitly assuming that all defs must be hoisted to the outermost (global) scope, but there is absolutely nothing in PEP 8 which demands this.
    – Kevin
    Commented Aug 22, 2021 at 5:01
  • 8
    If a one-line function definition feels wrong to you, I pray that we never have to work together. The code base of my dreams would consist only of one-line functions. Commented Aug 22, 2021 at 9:48
  • 15
    ‘it feels wrong using def for simple one-liners’: why does it? Commented Aug 22, 2021 at 15:43
  • 8
    (sign(i) * sum(element) for (i, element) in enumerate(x)) can be written as sum(x[::2]) - sum(x[1::2]), and is better at expressing intent
    – njzk2
    Commented Aug 22, 2021 at 15:56
  • 18
    The last code block is by far the most well-written and most readable
    – minseong
    Commented Aug 22, 2021 at 17:54

8 Answers 8

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.

9
  • 1
    Yes, in my real code, those helper functions were actually part of a different module. I included them to make my point clearer. Good catch! Perhaps a bigger question is, should I avoid writing my code in a more function style at all? I do feel the code is more readable regardless if this is done with def's or lambda's. Commented Aug 22, 2021 at 8:30
  • 3
    I'm a full-time functional programmer, so I'm a bit biased, but I think problems like this are especially well-suited to FP. Commented Aug 22, 2021 at 20:34
  • 1
    "Perhaps a bigger question is, should I avoid writing my code in a more function style at all?" Ok your question is kind of morphing into "Is functional programming better for code reuse than imperative programming?" which is slightly subjective and covered elsewhere. But as to whether to use def or lambda, that's kind of red-herring, it only costs 1 line. Btw, even lambdas can have a docstring: What is the best way in python to write docstrings for lambda functions?
    – smci
    Commented Aug 22, 2021 at 22:39
  • 1
    @Corbin I'm not sure your comment went where you intended it. Or you have missed the point of the question and this answer. We aren't talking about the output of a proof assistant
    – Caleth
    Commented Aug 23, 2021 at 16:00
  • 4
    @corbin No. Programming/software engineering has different goals than mathematics and computer science, even if the tools are isomorphic and one can help the other. It is akin to the difference between astronomy and building telescopes. You are falling into the turing tarpit, equivalence and isomorphism isn't the same as being identical, because the transformations matter. I am well aware of curry howard etc.
    – Yakk
    Commented Aug 24, 2021 at 18:02
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.

5
  • 11
    Extra emphasis on point 3. What's idiomatic in one language is not necessarily the right way to do something in another. In Haskell, that code would look right at home, and I (wearing my Haskell hat) would understand it instantly. In Python, I would don my Python hat and find it odd that all of these local variables are functions in disguise, and I would have to look twice to figure out what's going on. Commented Aug 22, 2021 at 4:47
  • 3
    Even in Haskell I would not write the helpers as lambdas, but with normal function definitions, for example alternating_sum x = [signum i * sum element | (i, element) <- zip [0..] x]. And I would add type signatures as well. Perhaps keep all the helpers in a where scope. Using lambdas instead of function-defs is a superficial change that does not make the code more “functional elegant”. Only if you can avoid giving them a name at all do lambdas shine. Commented Aug 23, 2021 at 11:46
  • 1
    @leftaroundabout That's certainly true. However I think the important thing is that Haskell's normal function definitions look a lot like Python's lambdas in the first place. At the very least they look more like Python's lambdas then they do Python's normal function definitions. Commented Aug 23, 2021 at 12:26
  • 1
    @SriotchilismO'Zaic Well, I'd rather say Python's lambdas are restricted to the subset of the language that translates easily to Haskell. Specifically, no imperative sequencing of statements. In Haskell, multi-statement expressions always require a suitable do or let construct, regardless of whether you're in a lambda or in a named function. However, good Haskell code actually makes plenty use of named definitions. Yes, it's possible to write point-free composition chains that are no less cryptic than golfed Perl, but I wouldn't say that's “what Haskell looks like”. Commented Aug 23, 2021 at 13:04
  • 1
    It helps that in Haskell whether something is a function declaration or not isn't such an important determiner of whether you need to read it now (in case it alters the context you're keeping in your head) or can ignore it until you see a usage point. In Haskell you can safely ignore any binding until you see a usage point. In Python function definitions are very different than other chunks of code, so making them look different helps.
    – Ben
    Commented Aug 24, 2021 at 0:16
24

"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.

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.

4
  • The lambda keyword explicitly denotes a function, just like def.
    – Corbin
    Commented Aug 23, 2021 at 15:27
  • 3
    @Corbin a lambda is an expression, a def is a statement. Binding the result of a lambda expression to a name is valid, but unidiomatic Python
    – Caleth
    Commented Aug 23, 2021 at 16:03
  • @Caleth: As this session shows, the two keywords compile to exactly the same bytecode. I understand your anti-lambda opinion, but the facts should be carefully examined.
    – Corbin
    Commented Aug 24, 2021 at 16:36
  • 1
    The bytecode might be the same but it does not look the same to the most important part - the user. ALso the IDE and other program support tools will show different things for def and lambda. You are defining functions so make it look like a function.
    – mmmmmm
    Commented Aug 24, 2021 at 16:43
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.

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.

-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.

2
  • 1
    Exactly but what is easier to read? It is a question about intent (what does my code do) versus how does it do it. Personally I find my first version crystal clear in how it does it, but completely lacks any sort of explanation of what it does / why it does what it do. I admit my last example is the extreme case, as the intent is clear but the implementation is hidden behind a veil of lambdas. Code has to strike a balance between what and how right? I would dread reading generic code, with no attempt at explaining what it does, only showing how. Commented Aug 22, 2021 at 10:50
  • 1
    I would say that if after a week of not working on it, you are able to understand your code easily, it meets the requirements of being readable.
    – Daone
    Commented Aug 22, 2021 at 15:14

Not the answer you're looking for? Browse other questions tagged or ask your own question.