Changes in decorator handling
Prior to version 3.9, decorators could be functions or classes, but not objects like lists or dictionaries. Let’s look at an example where this would be convenient to use. Suppose there is a UI application with buttons, and you need to add some kind of message on pressing each button. It would be convenient to do it with a decorator, but what to do if you need to print a different message for each button. You can do it this way:In [ ]:
buttons = [QPushButton(f'Button {i}') for i in range(10)]
button_0 = buttons[0]
button_1 = buttons[1]
@button_0.clicked.connect
def say_hello():
message.setText("Hello, World!")
@button_1.clicked.connect
def say_goodbye():
message.setText("Goodbye, World!")
We create a list comprehension from functions, explicitly assign values to the list items, and they can be used as decorators. This will work, but it won’t be efficient if you have a lot of objects from which decorators will be created.
This is not the only way, but others will be rather ambiguous. Let’s talk about two of them. In the first case, let’s create a function that will return the necessary function for the decorator:In [ ]:
def _(x):
return x
@_(buttons[0].clicked.connect)
def say_hello():
…
Using eval:In [ ]:
@eval("buttons[1].clicked.connect")
def say_bye():
…
In Python 3.9 it became possible to create decorators from any objects, such as list items and dictionaries. You can consider the syntax that implements the same example:
In [ ]:
@buttons[0].clicked.connect
def say_hello():
message.setText("Hello, World!")
@buttons[1].clicked.connect
def say_goodbye():
message.setText("Goodbye, World!")
The syntax for the dictionary values would look similar:
In [ ]:
buttons = {'hello': QPushButton('Hello!'), 'goodbye': QPushButton('Goodbye!')}
@buttons['hello'].clicked.connect
def say_hello():
message.setText('Hello, World!')
@buttons['goodbye'].clicked.connect
def say_goodbye():
message.setText("Goodbye, World!")
This functionality, while not bringing any new features, allows us to write cleaner code and avoid risky solutions like using eval.
Changes in typing syntax
Generics
Generics are types which can be parameterized which are usually containers, like a dict. Parameterized generics are types that have an internal type, such as dict[str, int].
Since Python 3.7 it became possible to specify the type of an object by specifying the type of the internal elements of containers. But to do this you had to import such types as List from the typing module:
In [ ]:
from typing import List, Dict
def find(haystack: Dict[str, List[int]]) -> int:
def find(haystack: dict) -> int:
…
It was possible not to specify the type of internal values, it was not necessary to import additional types and you could use the standard types dict and list. Thanks to this innovation, external libraries such as Mypy began to recognize generics.
Also, in version 3.9, it became possible to use annotations without explicitly importing them from the future module.