(using a lambda as a decorator requires Python 3.9 or later, for anyone wondering what's going on here)
For future readers wondering what the fuss is about, in older versions of Python, the decorator syntax was restricted to identifier expressions possibly containing attribute access .
and up to one call ()
at the end.
Of course, the call could always contain arbitrary expressions as arguments, which made it easy to sneak in arbitrary decorator expressions via an identity function.
def me(x):
return x
@me(lambda f: print("hi"))
def add(a, b):
return a + b
This was the topic of one of my StackOverflow answers.
Meta: This seems like a good format to spread knowledge from people who have it and are willing to teach, but either suck at writing or don't want to spend time writing and editing. Become an apprentice in return for writing a guide.
Is this still worth doing? Is the community getting anything out of these? The first post in this series has 39 voters so far. This one only has seven.
Oh, I forgot that karma 2.0 is not the number of voters.
Dunno, maybe diminshing returns, or people are on holidays? The drop from 39 to currently 9 seems significant, but it's still just two data points. If you don't mind writing, I would suggest to post another part in September and see whether the reaction increases.
From my perspective (although I have upvoted both, so I am not the right audience for your question) there are diminishing returns. I used Python briefly in the past, and maybe will use it in future again, but I am not using it currently... so I was kinda excited to see some cool tricks about something I studied for a while, but more cool tricks are at this moment only insight porn for me. On the other hand, if I ever use Python in future again, I wish I would remember to read these articles again.
konstell seems to be getting all the Karma though. Is there a way to do dual authorship on a post or something? Not sure what to do about it.
At the moment, users can message LW admins to add more authors, so I asked to add you as an author too. However, "only the first author gets karma, regrettably."
In the "Preprint" example, you use @wraps
without explaining it. I think it's worth noting it is imported from functools
and it's general purpose?
The section "Branching without if" is a bit confusing, because it is unclear if those examples work or need more code.
This might be out of scope of the mentorship, but I'd like gilch's opinion/heuristics on:
Re #1.
Readability counts.
[...]
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
—excerpts from The Zen of Python
This part of Python's culture was a reaction to Perl's motto, "There's more than one way to do it". Perl has been derided as a Write-only language, and despite its initial popularity, has been far eclipsed by Python. We can speculate about the various reasons, but I think this is one of them. Other ecosystems have their own cultures which emphasize different tradeoffs. Ruby, for example, seems to have inherited Perl's take.
The Zen is full of allusions and apparent contradictions. It's not meant to be obeyed as much as meditated upon; less about answers, and more about which questions to ask in the first place. So take my commentary as interpretation, not law.
Among other things, this part of The Zen is pointing out that code is read more often than it is written. It's more important to make code easy to read than easy to write. It's possible to write bad code in any language, but "bad" is partly a cultural interpretation. (And partly not.)
In Python, in the interest of readability, one should first do things in the most obvious way, until one has very good reason to do otherwise.
But "obvious" is also cultural. What is idiomatic in C is not, in Python, idiomatic (or "pythonic"), the "Dutch" part alluding to Guido van Rossum, the creator of Python, who made a lot of judgement calls about how Python was going to be, and what counts as "normal".
So "obvious" here means "obvious to acculturated Python programmers". It's a warning about being "too clever", and thereby being obtuse. But clever is OK when it's easy to read! Are you trying to show off or are you trying to write good code? If you are an acculturated Python programmer, then you can judge what's obvious and isn't. But culture isn't a static thing. If you're a part of it, and you know better, you can push boundaries a little.
(To read all of The Zen, import this
.)
My exercises for konstell are not necessarily pythonic. They illustrate particular points I was trying to teach; weaknesses I noticed in her understanding at the time. That you can do something doesn't mean you should.
So, like anything else in Python, my main heuristic would be, does your decorator make the code easier or harder to read, for an acculturated Python programmer? Does it have benefits worth the costs? Decorators are a Python feature, and they're used a lot. Excessive length is bad for readability too, and often neglected. Being more concise can be worth being slightly less obvious. Does it make tooling easier or harder to use? Does it make testing more or less difficult?
Re #2.
The multi-line lambda one always makes me want to facepalm. I hear it a lot, but this one is so confused that it's not even wrong.
First, lambdas in Python can have as many lines as you want. That makes the premise invalid. Seriously. My Hissp project is a Lisp compiler that targets a subset of Python. Its lambdas have the implicit PROGN typical of Lisp, and they compile to Python lambdas just fine. You could wrap your entire Hissp module in a progn
and it will compile to a lambda expression that goes on for pages.
So why are people confused about this? I think they're conflating "lines of code" with "statements", which are not at all the same thing. It's true that certain kinds of Python statements typically fit on one line, but none of them have to, and many kinds (block statements, e.g. try-except/try-finally) typically don't.
So let's try to steelman this: even multi-line lambdas in Python can't directly contain statements. (Or, from a certain point of view, they contain only one: an implicit return
. But they can call exec() on a string that does, or call other things that do.)
Second, true functional languages don't have statements to begin with, only expressions. (Or, from a certain point of view, they only have "expression statements".) Statements are a holdover from assembly, when the original Fortran mixed math expressions with machine code instructions (the "statements").
When programming in the functional style, which is when you want lambdas, you don't use statements anyway. Expressions are all you need! You don't even need a progn unless you have side effects, which is also not functional style.
So then the argument becomes "Python would not need decorators if anonymous functions could have statements."
Now what does the "need" part mean? Decorators are just syntactic sugar. You can get exactly the same behavior without them, so what use are decorators at all? Let's look at what the sugar does:
def <name>(<args>):
<body>
<name> = (<decorator expression>)(<name>)
becomes
@<decorator expression>
def <name>(<args>):
<body>
"Need" might be overstated. But why is the decorator better? The main reason is that it eliminates the duplication of <name>, which appears three times in the desugared version. It's also shorter and allows you to write the decorator expression before the function. Do lambdas have <name>s? No.
So what are they suggesting we could do instead? It's probably
<name> = (<decorator expression>)(
lambda <args>:
<body>
)
which does already work in Python if <body> doesn't have statements (and it wouldn't in the functional style). But we're still missing the function's docstring, and its __name__
attribute will be '(lambda)'
instead of <name>. Hypothetically, to fix these, it then becomes something like,
<name> = (<decorator expression>)(
def <name>(<args>):
<doc>
<body>
)
This doesn't work because def
isn't an expression in Python. It's not about lambdas anymore.
Now the steelman argument has become "Python would not want decorators if function definitions were expressions."
But we can see that the decorator version is still better. It doesn't duplicate the <name>. It doesn't need another layer of indentation, which gets even worse when nesting these. Can we fix this? Maybe?
def defn(*decorators):
def _(name, f):
while decorators:
f = decorators.pop()(f)
globals()[name] = f
f.__name__ = name # etc.
return _
defn(<decorator expression>)(
"<name>", lambda <args>:
<body>
)
This only assigns to the top level because it uses globals, even if it's nested in a class or function. Perhaps if we had some kind of preprocessor macro that expanded to an assignment? But now we've just re-implemented decorators.
The section "Branching without if" is a bit confusing, because it is unclear if those examples work or need more code.
The relevant context is the earlier definition of @if_
.
def if_(condition):
def decorator(c):
if condition:
return c.then()
else:
return c.else_()
return decorator
So
def if_(condition):
def decorator(c):
return [c.then, c.else][not condition]()
return decorator
would have the same behavior, and does not itself have an if
statement. I've implemented if
without using if
.
Ah, I missed a section on @wraps
. Added it here.
Also renamed "Branching without if" to "Making Branching Statements without Using if"
Also added some command line outputs for a couple examples at the end.
functions that use variables that are in scope but don't get passed in as parameters
A function object combining both executable code and some variables from the surrounding lexical environment is called a lexical closure.
Different languages handle this kind of thing differently. Closures were motivated by lambda calculus, so they're a common feature of languages with higher-order functions. (Although lambda calculus itself has no such concept, it emerges naturally as an optimization when trying to write an efficient evaluator for it.)
Python added these with PEP 227 when it gained lexical scoping, years before I started using the language.
I asked gilch if it was possible to pipe outputs from python shell to bash tools, gilch answered that we could not do it directly
Maybe not entirely accurate as worded. I don't remember exactly how this part of the conversation went, but (for example)
>>> import os; from unittest.mock import patch
>>> with os.popen('less', 'w') as p, patch('sys.stdout.write', p.write):
... copyright
...
would pop up less with the builtin copyright statement. Not as simple as the |
in Bash. It might take me a while to explain how exactly how this works, but it's basically monkeypatching stdout to pipe to less. Redirecting outputs of the python shell is not the normal way of doing things. You can be more direct about it.
You can use bash to pipe things into or out of calls to Python (or both). Python can also use variations on popen
to pipe to or from other utilities, although it's not nearly as easy as |
.
I can't reproduce the help(bob)
. Where is the # @wraps(function)
coming from?
>>> def preprint(greetings): # factory
... def greet_by_name(function): # decorator
... def wrapper(): # replaces the function being decorated
... print(greetings)
... return function()
... return wrapper
... return greet_by_name
...
>>> @preprint("hi")
... def bob():
... """
... prints 'bob'
... """
... print("bob")
...
>>> help(bob)
Help on function wrapper in module __main__:
wrapper()
Sorry, this is what happens when I don't keep a good trace of changes and try to reconstruct code snippets. When I was writing this post, the version of the function in my file was
def preprint(greetings): # factory
def greet_by_name(function): # decorator
@wraps(function)
def wrapper(): # replaces the function being decorated
print(greetings)
return function()
return wrapper
return greet_by_name
In order to reproduce the effect on docstring of bob
without using wraps(function)
, I simply commented that line out, so the function definition became
def preprint(greetings): # factory
def greet_by_name(function): # decorator
# @wraps(function)
def wrapper(): # replaces the function being decorated
print(greetings)
return function()
return wrapper
return greet_by_name
And # @wraps(function)
became the docstring...
I have fixed this in the post.
Looks like help()
will fall back to reading a comment above a definition if it can't find a docstring, but only if it's in an accessible source file. Functions do know their line numbers for debugging purposes. If you try this with a definition in the REPL, or in an exec
'd string or .pyc
file (without corresponding source), this won't work, because there is no file to read the comments from, unlike a docstring which would be available at runtime when read from any of these sources.
Also, if you modify the source file after importing the object, then the line numbers won't match up and it might find the wrong comment (when it reads the modified source file to look for comments). This can't happen with docstrings either, since they're saved when the object is created.
See also inspect.getcomments()
.
Gilch illustrated the difference between a function and a callable
This part still seems confused. What you remember about this part isn't quite what I was trying to say. Functions are callables, but they're not the only type of callable. Classes are also callable, for example. Lambdas are of the function type in Python, so these aren't exactly separate categories.
>>> type(lambda:0)
<class 'function'>
Callables are anything you can call; they understand the function call operator. You can use the builtin predicate callable
to tell if an object is callable or not.
[Epistemic status: me trying to recall what happened in a 4.5-hour pair-programming session, during which I was focused on solving problems. This post was reconstructed from scratch code files, written messages (the session was over voice call) and my python shell history.]
Previously: https://www.lesswrong.com/posts/kv3RG7Ax8sgn2eog7/an-apprentice-experiment-in-python-programming
Three days ago gilch and I had another session on Python Programming, where we continued talking about decorators.
Lambda decorator
So far, all of the decorators we've seen have been functions that take in another function as parameter, then used by the syntax
@<decorator_name>
. Turns out we can use a lambda function as a decorator too:When this file gets run in Python 3.9 interactive mode, the output is
and when I call the
add
function:Gilch asked me to write out the desugared version of this code. I wrote
add = print("hi")
, which was not correct. Gilch gave me the correct desugared code:Partial
We did a brief introduction to
partial
. We had code from last time:Accessing doc from within python shell:
Using partial to create a function with the first and second arguments passed in:
Gilch pointed out that the line above,
partial_sub = partial(subtract, 1, 2)
, could've been written aspartial_sub = _
because in python shell,_
returns the last value that's not None.Testing the partial function:
1 - 2 - 3 equals -4, so we're getting what we expected.
Gilch then pointed out that arguments can be passed into
partial
as keyword arguments:Finally, there's a way to implement partial functions using lambda:
Running Bash in Python
While running
help(partial)
in the python shell, the topic of usingless
came up. I asked gilch if it was possible to pipe outputs from python shell to bash tools, gilch answered that we could not do it directly, but just like how bash can run python, python can also run bash:Now we're in a bash shell, run by python:
At the last line, we're back in python shell again.
Lambda functions
Gilch gave me a number of lambda expressions in increasing complexity, asked me to explain what they did, then verify:
(lambda x: x+x)(3)
evaluates to 6, this one was straightforward(lambda x: lambda y: x + y)(2)(3)
I assumed that this statement expanded inside out (i.e.2
was passed in asy
and3
was passed in asx
) so I concluded that this would evaluate to 5. I got the correct answer, but my mistake was exposed by the next example:(lambda x: lambda y: x + y)('a')('b')
Using the same logic, I concluded that the result would be'ba'
, but the actual result was'ab'
. Why did I make this mistake? Gilch gave me the next question:(lambda x: lambda y: x + y)('a')
evaluates to a lambda. A lambda ofx
ory
? The first part of the expression,(lambda x: lambda y: x + y)
, is a lambda function that takes in a parameterx
, and returns a lambda functionlambda y: x + y
. This lambda function that takes inx
is then called with argument'a'
. The return value islambda y: 'a' + y
. And then it made sense why the previous question evaluated to'ab'
instead of'ba'
.I think what gilch was getting at here was the syntax of function calls other than the format of
f(x)
. Gilch also at some point pointed out the difference between a callable and a function.Preprint
Gilch gave me the next challenge: write a decorator
preprint
such that it can be used in the following way:I struggled here. I attempted to write out some vague idea in my head:
Gilch reminded me that the use case was already specified; functions
alice
andbob
were not to be modified. They then suggested that I wrote out the desugared version of the code. I struggled and gave an incorrect answer again until gilch provided me with the correct desugaring:is equivalent to
bob = (preprint("hi"))(bob)
.Understanding decorator de-sugaring
As you can see, at this point, I was struggling with desugaring. So gilch presented me with a formula:
is equivalent to
Preprint
My memory of what exactly happened here is spotty, perhaps because I was focused on finding out how to write the
preprint
decorator. Eventually I came up with something like this:This worked, but you may be able to see from the way I passed in
greetings
intogreet_by_name
, I was confused about why there needed to be 3 layers of definitions. Gilch gave me an alternative solution, and explained the pattern:Gilch explained that, for decorators that take in parameters, they are like a factory that produces different decorators based on input. Thus the first layer of definition processes the input, the second layer of definition is the decorator itself, and the innermost
wrapper
function is what replaces the function that ends up being decorated. Thisis equivalent to
At this point, I was not quite getting the 3-layer structure--I identified that there was a pattern of factory-decorator-function but I was mostly mimicking the pattern. This lack of understanding would be exposed in a later example.
wraps
In the previous example, we had the decorator
preprint
:Gilch instructed me to add a doc string to a function being decorated by this decorator. So we have:
What happens when we try to access the doc string of
bob
?Gilch then introduced
wraps
. According to the documentation, it calls update_wrapper to reassign some attributes of the wrapped function to the wrapper function.map
Gilch asked me if I knew what
map
in python did. I saidmap
took in a function and an array and return the result of the function applied to each element of the array. Gilch responded that it was mostly correct, with the caveat that map took in an iterable instead of an array.Gilch gave me a test case for which I needed to implement the decorator:
I came up with
When we run
assert list(double) == ['aa','bb','cc','dd']
, python did not complain. However when we ranlist(double)
again, we saw an empty array:Gilch explained that
double
was not an array, it was an iterable. After we had iterated throughdouble
in the assert statement, it became empty.Callable
Gilch illustrated the difference between a function and a callable:
in which
then if b else else_
is a callable. Two use cases:In the first case,
print("yes")
andprint("no")
are not callables (they get run and evaluated toNone
) whereas thelambda:print('yes')
andlambda:print('no')
in the second case are callables.Similarly, when we put the print statements in functions, the functions are callables:
Branching, with 1 branch
Gilch illustrated how we could use decorator for branching:
Branching, with 2 branches
Gilch asked me how I would implement branching such that if a given condition is true, one branch gets evaluated; if condition is false, another branch gets evaluated. This would essentially mean we decorate two functions. I came up with this solution:
And with
condition = 1>2
:The problem of this solution was too much code repetition. Also, I used 2 decorators instead of 1. Gilch said one way to do this was to decorate a class instead of a function.
Decorating a Class
Gilch gave me the test case, and asked me to write the decorator:
Eventually I came up with the decorator:
with condition reversed (
@if_(1<2)
):Decorating a Class, Example 2
Gilch gave me another test case for which I needed to write the decorator:
I modified the solution from the previous question to answer this one:
This was basically the same example as the previous one in the sense that I didn't need to change the structure of the decorator, only the specific functions within the innermost layer.
Making Branching Statements without Using
if
I asked gilch if there were ways to make if statement without using
if
. Gilch gave me some examples:return [c.then,c.else][not condition]()
return {True:c.then,False:c.else_}[bool(condition)]()
A More Difficult Example
Gilch gave me another test case:
And when the first line is changed to
@if_(1>2)
,assert result == -1
should pass.I figured out how to make the first branch work but not the second. At this point, it was getting quite late in my timezone and by brain was slowing down after hours of coding, so I told gilch I'd work on it the next day.
When I picked this problem up the next day, all of the wobbly parts of my understanding came back to bite me. I was running into a bunch of error messages like
TypeError: 'NoneType' object is not callable
andTypeError: <function> missing 1 required positional argument: 'greetings'
(orTypeError: <function> takes 1 positional argument but 2 were given
), and since the error messages would point at the line whereresult
was defined instead of the line within the decorator where the problem was, I was struggling a lot debugging my code. I poked around the internet a lot to look at other examples as an attempt to understand how to expand decorators in my head, and eventually came across this slide from a PyCon talk that made things a bit more clear to me.I sent gilch my solution:
With the condition flipped:
Gilch noticed my confusion: "The way you named things indicates a lack of understanding. Naming should have been more like this."
Alternatively, gilch provided two more solutions:
"They're both a decorator you apply to the second definition, regardless of which branch is taken. So calling the first one a wrapper is the wrong way to think of it," explained gilch.
Observations