More Flexible Decorators
Here are resources for more information about decorators and PEP 614:
00:00 In the previous lesson, I showed you the new pipe and pipe equal operators for dictionaries. In this lesson, I’m going to show you the changes to decorator syntax.
00:10 A decorator is a callable that wraps a function or a class. They’re typically used to do something before and/or after a function operates. For example, you could create a decorator that logged when the function was entered, called the function, and then logged when the function left.
00:29 A common pattern in web frameworks is to use decorators to ensure that the user is logged in before the view that is the function that displays the web page is allowed to be called. Prior to Python 3.9, decorators had to be a named callable—only the name of a function or a class.
00:48 This means you couldn’t use the entry in a list or the entry in a dictionary as the decorator. You would have to dereference it first, and then decorate with the dereferenced value. Python 3.9 has loosened up the syntax here, and now anything that resolves into a callable expression can be used as a decorator.
01:08 Quite frankly, this is a bit of an edge case. You only come across it if you’re using certain kinds of toolkits, but for those people who are using those toolkits, this does actually simplify the code and mean there’s less code to write.
01:21
Let me show you some examples. In case you haven’t seen decorators before, here’s a quick introduction. Don’t forget that all the code being shown here is available in the supplementary materials pull down. This file, volume.py
, declares two decorators.
01:39
The first one, called normal
, does nothing. It just returns the function that it’s wrapping. Decorators take as a parameter the function being wrapped, and at some point inside of them have to return that function.
01:52
The second decorator, shout
, starting on line 7, is a little more complicated. The nested function inside of the decorator is where the work is done. Similar to the @normal
decorator, it’s just calling the passed-in function, but on line 10, it’s calling .upper()
on whatever the function returns.
02:11
This decorator will only work on a function that returns a string—otherwise, it’ll blow up. But now if you’ve got functions with strings, you can wrap it with @normal
or @shout
to change the returned result.
02:27
Here’s an example of them in use. Inside of the before file, I import the normal
and shout
decorators from volume
.
02:34
The function on line 5 is wrapped with @normal
. The function on line 9 is wrapped with @shout
.
02:45
If I run first()
, I get back the string—the decorator did nothing. If I run second()
, the string is returned, .upper()
is called on it inside of the decorator, and the end result is an all caps shouting. Dickens should cover his ears.
03:05
So far, so good. Now, here’s the change in Python 3.9. The decorator on line 11 is only legal in Python 3.9. The difference here is it’s using a dictionary to result in the callable that is wrapping the function called third()
. Prior to Python 3.9, you would have to resolve this in a separate variable, and then use that variable to wrap the function to get it to work. When this module gets loaded, the global value of voice
will be set.
03:37
It does that by prompting for input. The input prompt will allow you to choose a string and then use that string to dereference the decorator inside of the DECORATORS
dictionary.
03:50
Thus, at load time of the module, it’s decided whether or not @normal
or @shout
is being applied to the function called third()
. Let’s look at this in practice.
04:02
When I import the module, it prompts me to choose something. I’ve chosen shout
, and now if I call the function, I get back a very loud response.
04:15
One thing to notice here is that the decorator is bound to the function at the time of module load, so although this module has a global value called voice
, changing it afterwards has no effect.
04:31
voice
is currently 'shout'
, because it was set at the module load time.
04:38 I can change it without a problem,
04:43
but it has no effect on the function third()
. Decorators are bound when the module is loaded, so the value of voice
at the time of module load is how the function gets wrapped. Once it’s been wrapped, it stays wrapped.
04:58 As I mentioned before, this is all a little bit of an edge case. You might be thinking to yourself, “So what?” A common pattern in GUI and web programming is to use decorators to indicate the behavior of certain functions. For example, if I was using the GUI toolkit Qt, I can declare a button in the GUI and then use a decorator to associate that button with a function that gets called when the button is clicked.
05:26 This is called a slots pattern. Oftentimes with this kind of coding pattern, you end up with a whole bunch of globals or lists of globals or dictionaries full of globals that you then want to assign the slots for.
05:38 In that case, before Python 3.9, you had to do extra work to use these things as decorators. Now with the change in the syntax, it’ll make coding with these kinds of frameworks easier.
05:52 Next up, I’ll show you the new features for annotation inside of Python 3.9.
Christopher Trudeau RP Team on Sept. 26, 2021
Hi Tord,
The changes to how decorators worked would allow for dot-delimited names in the decorators, but you’d still need to write a decorator to take advantage of it. I’ve not written any Qt before, so I don’t know if their registration mechanism use decorators or whether they’re using the Py39 syntax changes.
If you’re curious about writing your own decorators, you might find the following useful:
realpython.com/primer-on-python-decorators/
Otherwise, maybe post in the Slack channels and you can find someone who has done Qt and provide specific advice.
Happy coding. …ct
Geir Arne Hjelle RP Team on Sept. 27, 2021
Hi Tord,
This is a slightly tricky use case for the new decorator syntax because the decorator function, in this case button_qpb.clicked.connect()
, must be available in the scope where you define the “slot function”.
One way to do this would be to drag the button out as a class variable:
class MainWindow(QtWidgets.QMainWindow):
button_qpb = QtWidgets.QPushButton("Button")
def __init__(self):
super().__init__()
self.setCentralWidget(self.button_qpb)
self.show()
@button_qpb.clicked.connect
def on_button_clicked(self):
print("clicked!")
I’ll leave it to you to judge whether this is an improvement :) I like the visibility the decorator brings. Having the button as a class variable could bring issues down the line, especially if you need several instances of the class (since those would share the same button).
Geir Arne Hjelle RP Team on Sept. 28, 2021
Hi again, Tord.
A small follow-up to my earlier comment. A possibly better way to use the dotted decorator would be to define the “slot function” as an inner function:
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.button_qpb = QtWidgets.QPushButton("Button")
self.setCentralWidget(self.button_qpb)
self.show()
@self.button_qpb.clicked.connect
def on_button_clicked(self):
print("clicked!")
This way, you’re keeping the button as an instance variable, while still using the decorator.
torddellsen on Oct. 3, 2021
Hi and thanks for your help!
I’ve tried them and they both work, with the small change that the decorated functions cannot
take self as an argument. The inner function approach seems best for me since that will have access
to self through the outer scope (the __init__
function), which means that i can call other
functions in the class.
I now understand decorators better and have a new+better way to write my PyQt/Pyside code!
Kind regards, Tord
For reference, here is the complete code i ended up with
import sys
import logging
from PyQt5 import QtWidgets
from PyQt5 import QtCore
logging.basicConfig(level=logging.DEBUG)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.button_qpb = QtWidgets.QPushButton("Button")
self.setCentralWidget(self.button_qpb)
self.show()
@self.button_qpb.clicked.connect
def on_button_clicked():
print("clicked!")
print(self.button_qpb.text())
self.my_print()
def my_print(self):
print("my_print entered")
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
logging.debug(f"Python version: {sys.version!r}")
logging.debug(f"Qt version: {QtCore.qVersion()!r}")
main_window = MainWindow()
main_window.show()
app.exec_()
Output at start:
DEBUG:root:Python version: '3.9.5 (default, May 11 2021, 08:20:37) \n[GCC 10.3.0]'
DEBUG:root:Qt version: '5.15.2'
Output at click:
clicked!
Button
my_print entered
Become a Member to join the conversation.
torddellsen on Sept. 25, 2021
Hi and thank you for this explanation of the new features in 3.9
I am using PyQt5 and wanted to ask if you can give an example of how the new decorators can be used. Let’s say that my code looks like this:
How then can i instead use decorators to connect the button to the “slot function” (on_button_clicked)?
Grateful for help and with kind regards, Tord