PEP 636: Add section on built-in class patterns and qualified names (#1673)

Some rearranging of sections to connect the story more
linearly after adding the new sections.

Minor editorial updates
This commit is contained in:
Daniel F Moisset 2020-10-23 00:35:30 +01:00 committed by GitHub
parent e74e7d8ba1
commit 8fba056c41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 99 additions and 50 deletions

View File

@ -82,7 +82,7 @@ bound variables. If there's no match, nothing happens and the statement after
``match`` is executed next.
Note that, in a similar way to unpacking assignments, you can use either parenthesis,
brankets, or just comma separation as synonyms. So you could write ``case action, obj``
brackets, or just comma separation as synonyms. So you could write ``case action, obj``
or ``case (action, obj)`` with the same meaning. All forms will match any sequence (for
example lists or tuples).
@ -134,6 +134,9 @@ element equal to ``"get"``. It will also bind ``obj = subject[1]``.
As you can see in the ``go`` case, we also can use different variable names in
different patterns.
Literal values are compared with the ``==`` operator except for the constants ``True``,
``False`` and ``None`` which are compared with the ``is`` operator.
Matching multiple values
------------------------
@ -282,51 +285,11 @@ pattern matches but the condition is falsy, the match statement proceeds to chec
next case as if the pattern hadn't matched (with the possible side-effect of
having already bound some variables).
Going to the cloud: Mappings
----------------------------
Adding an UI: Matching objects
------------------------------
You have decided to make an online version of your game with a richer interface. All
of your logic will be in a server, and the UI in a client which will communicate using
JSON messages. Via the ``json`` module, those will be mapped to Python dictionaries,
lists and other builtin objects.
Our client will receive a list of dictionaries (parsed from JSON) of actions to take,
each element looking for example like these:
* ``{"text": "The shop keeper says 'Ah! We have Camembert, yes sir'", "color": "blue"}``
* If the client should make a pause ``{"sleep": 3}``
* To play a sound ``{"sound": "filename.ogg", format: "ogg"}``
Until now, our patterns have processed sequences, but there are patterns to match
mappings based on their present keys. In this case you could use::
for action in message:
match action:
case {"text": message, "color": c}:
ui.set_text_color(c)
ui.display(message)
case {"sleep": duration}:
ui.wait(duration)
case {"sound": url, "format": "ogg"}
ui.play(url)
case {"sound": _, "format": _}
warning("Unsupported audio format")
The keys in your mapping pattern need to be literals, but the values can be any
pattern. As in sequence patterns, all subpatterns have to match for the general
pattern to match.
You can use ``**rest`` within a mapping pattern to capture additional keys in
the subject. Note that if you omit this, extra keys in the subject will be
ignored while matching, i.e. the message
``{"text": "foo", "color": "red", "style": "bold"}`` will match the first pattern
in the example above.
Matching objects
----------------
Our adventure is being a success and we have been asked to implement a graphical
interface. Our UI toolkit of choice allows us to write an event loop where we can get a new
Your adventure is being a success and you have been asked to implement a graphical
interface. Your UI toolkit of choice allows you to write an event loop where you can get a new
event object by calling ``event.get()``. The resulting object can have different type and
attributes according to the user action, for example:
@ -337,7 +300,7 @@ attributes according to the user action, for example:
* A ``Quit`` object is generated when the user clicks on the close button for the game
window.
Rather than writing multiple ``isinstance()`` checks, we can use patterns to recognize
Rather than writing multiple ``isinstance()`` checks, you can use patterns to recognize
different kinds of objects, and also apply patterns to its attributes::
match event.get():
@ -377,7 +340,7 @@ the UI framework above defines their class like this::
@dataclass
class Click:
position: tuple
button: str
button: Button
then you can rewrite your match statement above as::
@ -399,11 +362,94 @@ this alternative definition::
def __init__(self, position, button):
...
The ``__match_args__`` special attribute defines an explicit order for your attribtues
The ``__match_args__`` special attribute defines an explicit order for your attributes
that can be used in patterns like ``case Click((x,y))``.
# TODO: special rules for builtin classes
# TODO: matching foo.bar as a constant
Matching against constants and enums
------------------------------------
Your pattern above treats all mouse buttons the same, and you have decided that you
want to accept left-clicks, and ignore other buttons. While doing so, you notice that
the ``button`` attribute is typed as a ``Button`` which is an enumeration built with
``enum.Enum``. You can in fact match against enumeration values like this::
match event.get():
case Click((x, y), button=Button.LEFT): # This is a left click
handle_click_at(x, y)
case Click():
pass # ignore other clicks
This will work with any dotted name (like ``math.pi``). However an unqualified name (i.e.
a bare name with no dots) will be always interpreted as a capture pattern, so avoid
that ambiguity by always using qualified constants in patterns.
Going to the cloud: Mappings
----------------------------
You have decided to make an online version of your gam. All
of your logic will be in a server, and the UI in a client which will communicate using
JSON messages. Via the ``json`` module, those will be mapped to Python dictionaries,
lists and other builtin objects.
Our client will receive a list of dictionaries (parsed from JSON) of actions to take,
each element looking for example like these:
* ``{"text": "The shop keeper says 'Ah! We have Camembert, yes sir'", "color": "blue"}``
* If the client should make a pause ``{"sleep": 3}``
* To play a sound ``{"sound": "filename.ogg", format: "ogg"}``
Until now, our patterns have processed sequences, but there are patterns to match
mappings based on their present keys. In this case you could use::
for action in message:
match action:
case {"text": message, "color": c}:
ui.set_text_color(c)
ui.display(message)
case {"sleep": duration}:
ui.wait(duration)
case {"sound": url, "format": "ogg"}
ui.play(url)
case {"sound": _, "format": _}
warning("Unsupported audio format")
The keys in your mapping pattern need to be literals, but the values can be any
pattern. As in sequence patterns, all subpatterns have to match for the general
pattern to match.
You can use ``**rest`` within a mapping pattern to capture additional keys in
the subject. Note that if you omit this, extra keys in the subject will be
ignored while matching, i.e. the message
``{"text": "foo", "color": "red", "style": "bold"}`` will match the first pattern
in the example above.
Matching builtin classes
------------------------
The code above could use some validation. Given that messages came from an external
source, the types of the field could be wrong, leading to bugs or security issues.
Any class is a valid match target, and that includes built-in classes like ``bool``
``str`` or ``int``. That allows us to combine the code above with a class pattern.
So instead of writing ``{"text": message, "color": c}`` we can use
``{"text": str() as message, "color": str() as c}`` to ensure that ``message`` and ``c``
are both strings. For many builtin classes (see PEP-634 for the whole list), you can
use a positional parameter as a shorthand, writing ``str(c)`` rather than ``str() as c``.
The fully rewritten version looks like this::
for action in message:
match action:
case {"text": str(message), "color": str(c)}:
ui.set_text_color(c)
ui.display(message)
case {"sleep": float(duration)}:
ui.wait(duration)
case {"sound": str(url), "format": "ogg"}
ui.play(url)
case {"sound": _, "format": _}
warning("Unsupported audio format")
.. _Appendix A:
@ -538,6 +584,9 @@ Several other key features:
case (Point(x1, y1), Point(x2, y2) as p2): ...
- Most literals are compared by equality, however the singletons ``True``,
``False`` and ``None`` are compared by identity.
- Patterns may use named constants. These must be dotted names
to prevent them from being interpreted as capture variable::