
Lambdas and Functions in Python - mastro35
http://www.thepythoncorner.com/2018/05/lambdas-and-functions-in-python.html
======
roenxi
Python's lambda also has an interesting design flaw that anyone using it will
come up against sooner or later [1]. The way it interacts with Python's
scoping rules is quite broken. This hinders using it as freely as is possible
in, eg, Clojure.

    
    
      >>> fs = [(lambda n: i + n) for i in range(10)]
      >>> [f(4) for f in fs]
      [13, 13, 13, 13, 13, 13, 13, 13, 13, 13]
    

[1] [http://math.andrej.com/2009/04/09/pythons-lambda-is-
broken/](http://math.andrej.com/2009/04/09/pythons-lambda-is-broken/)

~~~
xapata
A lambda is no different in scoping rules than a regular function def. If you
put a def in a loop, you'll find the same behavior.

Closures are only created for functions defined inside other functions. Thus
the term non-local. They're variables that are neither local nor global.

Don't call it broken when it works as intended.

~~~
_sdegutis
“Works as intended” but also “unintuitive to most users” is still broken.
Disclaimer: I have hated python ever since falling out of love with it 8 years
ago for reasons just like this one so I’m definitely biased.

~~~
xapata
Since programming is a rapidly growing field and Python is the first language
for many, I suspect that Python's scoping rules are in fact intuitive to most
of its users. The ones who stumble are those with intuition built by some
other language.

~~~
_sdegutis
I'd argue that's the opposite. Python was the first dynamic language I
learned. The scoping rules in Python didn't bother me because I didn't know
any better, not because they're intuitive or well designed.

~~~
xapata
Fair anecdote. I'll offer that in my experience teaching hundreds of students,
most don't have trouble with the cascade from locals to globals to builtins,
and quickly grasp that globals are looked up for each call. That closures work
at all confuses the ones who are thinking carefully about local scope. I may
have a biased sample of students, but I think it's decent evidence.

------
pbreit
“Isn’t it better?”

No!!!!!!!!

The original code looks by far the best to me. Why over-complicate?

~~~
jewbacca
Most of the code by volume in this post's final product (and most of the
ugliness you're probably seeing in it) comes from dealing separately with the
arity of different operations (`compute_operation_with_one_operand`,
`compute_operation_with_two_operands`, etc) -- which retains a whole lot of
copy-paste-but-change-one-small-thing-in-the-middle boilerplate.

That can be taken further and generalized out.

It's been a while since I did much Python metaprogramming-navel-gazing, but if
you can't do something to automatically detect the arity of the operations'
lambdas/functions, you can at least explicitly annotate the operations data
with their arity -- which is already implicitly being done (as code instead of
data) in `compute`'s 3 if-statements + the 3 "compute_with_N_operands"
functions.

From there, you'll only have 1 case once, instead of 3 cases with each one
manifesting in 2 different places. And just about half of the code disappears.

\----

edit: since adjkant already covered the "automatically detect the arity of the
operations' lambdas/functions" approach to removing all the cases,
([https://news.ycombinator.com/item?id=17329772](https://news.ycombinator.com/item?id=17329772)),
here is the gist of what I meant by "explicitly annotate the operations data
with their arity":

    
    
        import math, operator
    
        class rpn_engine:
            def __init__(self):
                self.stack = []
                self.catalog = {"+":    (2, operator.add),
                                "-":    (2, operator.sub),
                                "*":    (2, operator.mul),
                                "/":    (2, operator.truediv),
                                "^2":   (1, lambda x: x * x),
                                "SQRT": (1, math.sqrt),
                                "C":    (0, self.stack.pop),
                                "AC":   (0, self.stack.clear)}
    
            def compute(self, operation):
                (arity, op) = self.catalog[operation]
                operands = reversed([self.stack.pop() for _ in range(arity)])
                return self.stack.push(op(*operands))

~~~
jewbacca
Someone replied with and then unfortunately deleted an extremely good
suggestion.

Instead of each operation taking a variable number of numbers and then
_either_ returning a value _or_ directly changing the state of the stack
(which was also a bug in my previous gist, in pushing the result of C and AC):
homogenize the types of the operations by making _all_ of them take a stack
and return a stack.

You could also wrap all of the basic arithmetic functions with a higher-level
function so that, eg, `add` would still not actually have to be aware of the
stack:

    
    
        import math, operator
    
        def stackFunction(arity, function):
            return lambda stack: stack[:-arity] + [function(*stack[-arity:])]
    
        class rpn_engine:
            def __init__(self):
                self.stack = []
                self.catalog = {"+":    stackFunction(2, operator.add),
                                "-":    stackFunction(2, operator.sub),
                                "*":    stackFunction(2, operator.mul),
                                "/":    stackFunction(2, operator.truediv),
                                "^2":   stackFunction(1, lambda x: x * x),
                                "SQRT": stackFunction(1, math.sqrt),
                                "C":    lambda stack: stack[:-1],
                                "AC":   lambda stack: []}
    
            def compute(self, operation):
                self.stack = self.catalog[operation](self.stack)
    
            def push(self, number):
                self.stack.append(number)

------
adjkant
This code can look a lot better by replacing the compute function with this:

[https://pastebin.com/fMtc8Git](https://pastebin.com/fMtc8Git)

~~~
abecedarius
It looks like that reverses the arguments? You could do

    
    
        args = self.stack[-n:]
        del self.stack[-n:]
        self.push(function_requested(*args))
    

or maybe

    
    
        self.stack[-n:] = [function_requested(*args)]

~~~
adjkant
Yeah oops coded that up quick. Either way you fix it, it’s far better than a
function for calling each number of args and keeps the code readable.

~~~
adjkant
For fun, an updated version that cleans up other things as well as the
ordering, not relying on signatures of lambdas, etc.

[https://pastebin.com/HF41J8KQ](https://pastebin.com/HF41J8KQ)

------
chriswarbo
It's interesting to note that a dictionary ("catalog") of values is
essentially the same concept as an object. This is clearer in Javascript,
where `{...}` notation is called an "object" rather than a "dictionary" and
the `.foo` and `["foo"]` notations are mostly interchangable.

Storing functions in a dictionary is _very_ close to storing methods in an
object. The only differences are sharing state via a `self` (or `this`)
argument, and inheritance chains (either from classes or prototypes).

Also interesting to note that many of the author's functions, like `lambda x,
y: x + y`, only make sense because the underlying language (Python) thought
that constructs like `+` shouldn't be functions for some reason. Many
languages seem to do this, e.g. here's a bug report I raised for PHP
[https://bugs.php.net/bug.php?id=66368](https://bugs.php.net/bug.php?id=66368)

When we introduce arbitrary distinctions, like dictionary vs object, named vs
anonymous functions, functions vs operators, etc. then we end up with all of
the choices and complications like those shown in this article.

One of the principles I try to follow when programming is to _avoid_ making
distinctions, or breaking symmetries, unless my hand is forced by the
underlying dynamics. Experience has taught me that introducing distinctions in
one place just introduces complication elsewhere (e.g. more cases to handle
when metaprogramming).

~~~
coldtea
> _Also interesting to note that many of the author 's functions, like `lambda
> x, y: x + y`, only make sense because the underlying language (Python)
> thought that constructs like `+` shouldn't be functions for some reason_

Actually python's + (and most operators) are available as functions, and allow
for overloading as well:

    
    
      >>> import operator
      >>> operator.__add__(1,2)
      3
    

> _Many languages seem to do this_

"Many"? Unless you ever only touched Smalltalk, Lisp or some fringe languages,
this is the norm for most languages -- operators are distinct from functions
(same as expressions are different than statements). Whether this is a good
idea or not, I wont go into, but it's not like this is something surprising,
or something that merely "many" languages seem to do. Not just many, but
almost ALL mainstream languages (C, C++, Java, C#, etc) do it.

~~~
kazinator
Lisp operators are distinct from functions. It's just that arithmetic
operations are functions; Lisp dialects use operators for various other
constructs that cannot follow the function call evaluation rules.

C++ sort of makes operators functions. Any overloaded operator is a function.
If _a_ is a class object then _a + b_ can be spelled otherwise as
_a.operator+(b)_. This doesn't work for the built-in definitions of the
operator, as in 2.operator+(2).

The built-in + operator in C++ can also do things that used to be impossible
in C++ until its template system became more powerful. Namely, the result type
of the built-in + is deduced from both operands and can be different from both
of them. If C++ were being redesigned clean-slate, it would likely be that
operator overloading is allowed for the non-class types like _int_ , and all
the built-in operators are defined in terms of templates, whose
deduction/instantiation reproduces all the rules.

~~~
cup-of-tea
The thing that makes it beautiful in Lisp is multiple dispatch. In higher
order functions, + is still just +, not some function that's already bound to
the left hand side.

------
Waterluvian
Has anyone ever experienced a time where assigning member variables on a
function made sense?

The thing I love about python is the freedom to do whatever I want and the
responsibility to almost never do it. I crave examples of exceptional cases.

~~~
guitarbill
The most common cases are decorator functions, e.g. one that counts the number
of invocations: [https://stackoverflow.com/questions/44968004/python-
decorato...](https://stackoverflow.com/questions/44968004/python-decorators-
count-function-call)

------
guitarbill
That code seems suspect. Throwing and catching BaseException is bad/has some
weird consequences and probably unintentional - I assume the author meant to
use Exception? Pathologically avoiding `elif`. Misspelling `inspect` in the
code examples which are ostensibly verbatim Python sessions.

In general, lambda functions are less necessary in Python because functions
are first-class. The lambda syntax is useful for extremely trivial functions,
e.g. `sorted(a, key=lambda value: value.foo_bar())`, but otherwise "normal"
functions work just fine/better - especially since you can define a function
wherever you want. (This is how it's supposed to be. E.g. in Java or
Javascript, code using many lambdas can be a royal pain to debug.)

Maybe related, I dislike the use of `map` and `filter`. Almost all real-world
cases are more readable using list/dict/set comprehension or generator
expressions. The only thing missing now is assignment expressions (PEP 572,
[0]) so I can do a map + filter in one comprehension.

[0]
[https://www.python.org/dev/peps/pep-0572/](https://www.python.org/dev/peps/pep-0572/)

~~~
mastro35
Hi guitarbill, the article is intended to point out how to review the original
code. I figured out a bad code written by anyone else and I tried to make it
better.

About the lambdas you are right, they’re good just if you have trivial
functions, like in other languages. If you need more “power” just use normal
functions.

PS: if I mispelled “inspect” or anything else... sorry, I’m not a native
speaker, I wish I could speak and write better but the truth is that my
English sucks... :(

~~~
guitarbill
Fair enough, I wasn't sure if the review was just fiction - a good idea to
base the article on.

However, I do want to refute that English skill has anything to do with the
things I'm pointing out. Your English is fine, plus I don't care, as I also
had to learn English as a second language. The issues are purely technical.

First, `BaseException` is introduced in the refactored versions, not in the
original code - so it's hardly Peter's fault. Second, the misspelling is in
code, and it stops the REPL example from working ("from inpect import
signature") - the REPL code looks like it's copy & pasted from the console,
but it can't be.

So "suspect" was probably the wrong word to use here, even if that code has
been altered. I hope you can use the feedback to improve the article, even if
the feedback is worded poorly!

~~~
mastro35
For the BaseException you’re right.

About the mispelled word I didn’t notice it... that’s weird, it IS a copy and
paste, probably when I pasted it I lost that line and I rewrote it?

Can’t remember, I posted the original article on Medium on December 2017.
However I will surely modify the article next week to address that typo,
thanks!

