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:
parent
e74e7d8ba1
commit
8fba056c41
149
pep-0636.rst
149
pep-0636.rst
|
@ -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::
|
||||
|
||||
|
|
Loading…
Reference in New Issue