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?

N3buchadnezzar
  • 351
  • 2
  • 5
  • 31
    You seem to be tacitly assuming that all `def`s must be hoisted to the outermost (global) scope, but there is absolutely nothing in PEP 8 which demands this. – Kevin Aug 22 '21 at 05:01
  • 2
    As Kevin implied here, I usually suggest using inner functions to name block of codes or one liners that are only used once in a particular function. That gives you the benefit of breaking code down, while keeping things local, and making sure that other people won't just unexpectedly start calling the function from elsewhere. – Lie Ryan Aug 22 '21 at 05:42
  • 3
    Every snippet shown here is part of a larger function, so nothing is part of the global scope =) so all of those defs are inner functions. Perhaps a bigger question is whether it is "allowed" to write code in a more functional style in Python. I find it increases readability, even though it is easy to go too far. – N3buchadnezzar Aug 22 '21 at 08:34
  • 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. – Kilian Foth Aug 22 '21 at 09:48
  • Python doesn't lend itself well to functional programming. You _can_ write fairly functional code (the stuff true functional programmers scoff at), but to write _very_ functional code you have to fight the language. You should make the choice of what's more important; Python or a more functional programming language. – Peilonrayz Aug 22 '21 at 14:13
  • 14
    ‘it *feels* wrong using `def` for simple one-liners’: why does it? – user3840170 Aug 22 '21 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 Aug 22 '21 at 15:56
  • @user3840170 I am probably in wrong, but I sometimes think of `lambda`s and `def`s as atoms and molecules. I put a bigger emphasis / importance on `def`s. "Oh, look a definition this must be important" in contrast to lambdas which I regard as building blocks for `defs`. So I tend to put things that do something significant inside of `def`s (they can of course be nested) But i clearly see the point about initialization and lazy loading and will switch to using `def`s much more, even for small helper functions. – N3buchadnezzar Aug 22 '21 at 16:14
  • 17
    The last code block is by far the most well-written and most readable – theonlygusti Aug 22 '21 at 17:54
  • @njzk2 Or `return (s * sum(element) for s, element in zip(cycle([1, -1]), x))`, if you want to preserve the alternating `+`/`-` behavior rather than re-associating all the terms. (Both apporaches could cause under- and overflow issues with floating-point values.) – chepner Aug 22 '21 at 18:22
  • @chepner or `x @ cycle([1, -1])` in a near future hopefully. But we are getting off topic. – N3buchadnezzar Aug 22 '21 at 18:42
  • 2
    Style questions aside, `def` has some practical advantages over `lambda`: `def` can be pickled (which is important for some use cases like `multiprocessing`), and can have a docstring. – Anders Kaseorg Aug 22 '21 at 21:10
  • 7
    A lambda is an anonymous function. If you give a name to a lambda, you might as well use a function. Bonus point if you define it in the most specific scope possible (e.g. nested inside another function), so that the reader can safely ignore the definition when it's not relevant. – Eric Duminil Aug 23 '21 at 07:52
  • @njzk2: It's a good idea and it looks really clean, but you'd have to change some more code, because `x` appears to be a map object, which isn't subscriptable. – Eric Duminil Aug 23 '21 at 09:09
  • 1
    @EricDuminil the `map` can (and should) trivially be replaced by a list comprehension. Type hints wouldn't hurt making sense of all of it, either. – njzk2 Aug 23 '21 at 21:08
  • 2
    I highly recommend that you pick up a copy of Michael C. Feathers, "Working Effectively with Legacy Code," and understand that the PEP8 rule is trying to help the version of you in the future that comes back to maintain this code. Multiple one-line functions serve the purpose of introducing "seams" that you can pull apart for testing. Using a def instead of a lambda means the stack trace contains names. Your lambda-based code is cryptic to me, while your def-based code is clear. – ManicDee Aug 24 '21 at 00:01
  • @njzk2: Type hints would be great indeed. `divisors = proper(divisors)` looks weird, for example, and I'm not sure if the type is the same for both `divisors`. Actually defining every mentioned variable would be a good start. As is, the code cannot be tested/refactored. – Eric Duminil Aug 24 '21 at 13:21
  • As a side-note, the `sign` function should probably be renamed to reduce potential confusion with the more usual [sign function](https://en.wikipedia.org/wiki/Sign_function) – Jiří Baum Aug 26 '21 at 01:37

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
  • 146,727
  • 38
  • 279
  • 479
  • 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. – N3buchadnezzar Aug 22 '21 at 08: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. – Karl Bielefeldt Aug 22 '21 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?](https://stackoverflow.com/questions/50370917/what-is-the-best-way-in-python-to-write-docstrings-for-lambda-functions) – smci Aug 22 '21 at 22:39
  • Software is isomorphic to proofs ([nLab](https://ncatlab.org/nlab/show/computational+trilogy)), so this answer is factually wrong. In addition, it rudely revises definitions so that projects like [Metamath](http://us.metamath.org/) are no longer software engineering. – Corbin Aug 23 '21 at 15:30
  • 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 Aug 23 '21 at 16:00
  • @Corbin Beware the Turing Tarpit my friends, where everything is equivalent, but nothing of value is easy. The isomorphism between proofs and programs is *interesting* and *useful*, but the craft and style of writing a program and writing a proof is usually *different*. – Yakk Aug 24 '21 at 15:08
  • I have sketched a fragment of [native type theory](https://lobste.rs/s/ornuz9/native_type_theory) for Python. I expect that any syntactic Turing-complete language has such a type theory. @Yakk, I invite you to try out Idris, Coq, or Kind; there exist languages where proofs are programs. My point is that "like a mathematician" is a pretense; we're all computer scientists here! – Corbin Aug 24 '21 at 16:40
  • 3
    @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 Aug 24 '21 at 18:02
  • Rather than "You're trying to minimize the length of code a future maintainer actually has to read." it's more like "You are minimizing the *effort* a future maintainer has to *invest to understand and modify the code, which calls for a balance of simplicity, brevity, and abstraction*." – Deduplicator Nov 30 '22 at 16:25
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
  • 132,749
  • 27
  • 279
  • 375
  • 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. – Silvio Mayolo Aug 22 '21 at 04: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. – leftaroundabout Aug 23 '21 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. – Sriotchilism O'Zaic Aug 23 '21 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”. – leftaroundabout Aug 23 '21 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 Aug 24 '21 at 00: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.

Ben
  • 1,017
  • 6
  • 10
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
  • 241
  • 2
  • 10
  • The `lambda` keyword explicitly denotes a function, just like `def`. – Corbin Aug 23 '21 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 Aug 23 '21 at 16:03
  • @Caleth: As [this session shows](https://gist.github.com/MostAwesomeDude/ccc2086adb767e1eec8ce543257cb64e), the two keywords compile to exactly the same bytecode. I understand your anti-lambda opinion, but the facts should be carefully examined. – Corbin Aug 24 '21 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 Aug 24 '21 at 16:43
1

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,543
  • 1
  • 11
  • 15
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.

Jiří Baum
  • 129
  • 2
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,614
  • 1
  • 13
  • 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
  • 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. – N3buchadnezzar Aug 22 '21 at 10:50
  • 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 Aug 22 '21 at 15:14