One-liner lambda expressions as function decorators: (ab)using Python 3.9’s new PEP 614 feature

Alan Wang
5 min readOct 7, 2020
Photo by Andrew Le on Unsplash

The sole goal of this is to use (or abuse) one new feature comes with Python 3.9: Relaxing Grammar Restrictions On Decorators (PEP 614). Since almost no one mentioned about it before, I decided to write a little article here.

Of course, the way I use it is very probably not “Pythonic”, and maybe shouldn’t be used at all in your work, but we can still have some fun here, right?

Apparently, PEP 614 is mainly designed for PyQt5, which you can attach a button clicking method as a decorator to another function to create button events. But the PEP also says “The decision to allow any valid expression”. What does that mean?

It turns out “any expression” should still return a decorator wrapper. It’s just that you don’t have to assign the decorator to a name anymore.

I’ll assume that you have the basic idea of what decorators are, and how to write them normally in Python. If don’t, you can take a look at Primer on Python Decorators on RealPython.com.

Two Lambs, One Func

Photo by Matt Seymour on Unsplash

Is it possible to write a one-line decorator expression? The first thing I’ve tried is using two lambda functions:

@lambda func: (lambda *para: func(*para).upper())
def greet(name):
return f'Hello, {name}!'
print(greet('Arthur Dent'))

Result:

HELLO, ARTHUR DENT!

This is a very simple decorator, it converts the result text to all upper case. The second lambda is the wrapper or closure that would be returned and replace the function greet.

To reuse the decorator:

@shout := lambda func: (lambda *para: func(*para).upper())
def greet(name):
return f'Hello, {name}!'
@shout
def reminder(name, thing):
return f'Don\'t forget your {thing}, {name}!'
print(greet('Arthur Dent'))
print(reminder('Arthur Dent', 'towel'))

See? This is a good place to use the walrus operator (:=) added in Python 3.8.

Result: (You can see *para can cope with different number of parameters too:)

HELLO, ARTHUR DENT!
DON'T FORGET YOUR TOWEL, ARTHUR DENT!

Build a Logger Cabin

Photo by Geran de Klerk on Unsplash

You can write only one statement in lambdas, or is it though? Let’s abuse Python more, by using or operator to add a print() function:

@lambda func: lambda *para: \
print('Func called:', func) or func(*para).upper()
def greet(name):
return f'Hello, {name}!'
print(greet('Arthur Dent'))

It returns

Func called: <function greet at 0x00000212EEFC38B0>
HELLO, ARTHUR DENT!

Since print() returns None (all Python functions without explicit return statements do so), the expression None or value still returns the value itself. So by this way, you can do some logging before running the decorated function.

The one problem is: if you put print() behind the or operator, it won’t be executed at all, because as the first part of the logical expression, func() would return a non-empty string (which equals True),there is no need for Python to check the second part. So it looks like you can only do things before the decorated function.

Or…is it?

Once Upon a Timer

Photo by Fabrizio Verrecchia on Unsplash

I really do want to do something after the decorated function as well, like timing the code. And, indeed, it is possible after all.

This is what I came up with: use a list as the return value of wrapper to execute a bunch of stuff. The list comprehension outside of it will filter out the only value we need.

import time@lambda func: lambda *para: \
[_ for _ in [ \
print('Func called:', func),
print('Start:', time.time()),
func(*para),
print('End:', time.time()),
] if _][0]
def calculate(n):
x = 0
for i in range(n):
x += i ** n
return x
print(calculate(3000))

Result:

Func called: <function calculate at 0x00000254CA9638B0>
Start: 1602050421.943357
End: 1602050422.5030253
13440703023871524924619199858289162761099130089897931730777...

However, I can’t calculate time duration in the decorator, because using walrus operators inside list comprehensions is now illegal.

So for the second version, I use the filter() function instead:

import time@lambda func: lambda *para: \
list(filter(lambda _: _,[
print(f'Func called: {func}'),
(start := time.monotonic_ns()) and False,
func(*para),
(end := time.monotonic_ns()) and False,
print(f'Duration: {(end - start) / 1000000} ms')
]))[0]
def calculate(n):
x = 0
for i in range(n):
x += i ** n
return x
print(calculate(3000))

What a Frankenstein! But it works, and technically still in one (very long) line.

Now, there are also two expressions in the list without using print(). After using := to assign time data to variables start and end, these expressions would combine with False so the results (become Falses) will be emitted by filter(). Now we can calculate the time in the decorator!

Result:

Func called: <function calculate at 0x0000024F9EE048B0>
Duration: 531.0 ms
13440703023871524924619199858289162761099130089897931730777...

Wrappering Up

Photo by Nick Bolton on Unsplash

Actually, if you just want to decorate something very simple or two on functions, using this lambda decorator expression may not be a really bad idea. After all, you need to write at least 4–5 lines to implement a basic decorator otherwise.

RealPython.com also demonstrated that you can wrap up different decorators in a dictionary and select one of them with a key (see here). Although, you’ll have to input the key before defining the function, and the function will be forever changed unless you redefine it again.

--

--

Alan Wang

Technical writer, former translator and IT editor.