Simplifications of PEP 653: (#1919)

* Make 0 the defualt for __match_class__ and drop ability to not match class patterns

* Use equality not bitwise ands to check for matches

* Add to rejected and deferred sections, explaining the above
This commit is contained in:
Mark Shannon 2021-04-12 12:17:50 +01:00 committed by GitHub
parent eda5cfd5d6
commit 4134c46262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 83 additions and 103 deletions

View File

@ -16,7 +16,7 @@ but is more precise, easier to reason about, and should be faster.
The object model will be extended with two special (dunder) attributes, ``__match_container__`` and The object model will be extended with two special (dunder) attributes, ``__match_container__`` and
``__match_class__``, in addition to the ``__match_args__`` attribute from PEP 634, to support pattern matching. ``__match_class__``, in addition to the ``__match_args__`` attribute from PEP 634, to support pattern matching.
Both of these new attributes must be integers and ``__match_args__`` is required to be a tuple. Both of these new attributes must be integers and ``__match_args__`` is required to be a tuple of unique strings.
With this PEP: With this PEP:
@ -114,29 +114,34 @@ The ``__match_container__ ``and ``__match_class__`` attributes will be added to
``__match_container__`` must be an integer and should be exactly one of these:: ``__match_container__`` must be an integer and should be exactly one of these::
0 0
MATCH_SEQUENCE MATCH_SEQUENCE = 1
MATCH_MAPPING MATCH_MAPPING = 2
``MATCH_SEQUENCE`` is used to indicate that instances of the class can match sequence patterns.
``MATCH_MAPPING`` is used to indicate that instances of the class can match mapping patterns.
``__match_class__`` must be an integer and should be exactly one of these:: ``__match_class__`` must be an integer and should be exactly one of these::
0 0
MATCH_ATTRIBUTES MATCH_SELF = 8
MATCH_SELF
``MATCH_SELF`` is used to indicate that for a single positional argument class pattern, the subject will be used and not deconstructed.
.. note:: .. note::
It does not matter what the actual values are. We will refer to them by name only. In the rest of this document, we will refer to the above values by name only.
Symbolic constants will be provided both for Python and C, and once defined they will Symbolic constants will be provided both for Python and C, and the values will
never be changed. never be changed.
``object`` will have the following values for the special attributes:: ``object`` will have the following values for the special attributes::
__match_container__ = 0 __match_container__ = 0
__match_class__= MATCH_ATTRIBUTES __match_class__= 0
__match_args__ = () __match_args__ = ()
These special attributes will be inherited as normal. These special attributes will be inherited as normal.
If ``__match_args__`` is overridden, then it is required to hold a tuple of strings. It may be empty. If ``__match_args__`` is overridden, then it is required to hold a tuple of unique strings. It may be empty.
.. note:: .. note::
``__match_args__`` will be automatically generated for dataclasses and named tuples, as specified in PEP 634. ``__match_args__`` will be automatically generated for dataclasses and named tuples, as specified in PEP 634.
@ -153,7 +158,6 @@ In the following, all variables of the form ``$var`` are temporary variables and
They may be visible via introspection, but that is an implementation detail and should not be relied on. They may be visible via introspection, but that is an implementation detail and should not be relied on.
The psuedo-statement ``FAIL`` is used to signify that matching failed for this pattern and that matching should move to the next pattern. The psuedo-statement ``FAIL`` is used to signify that matching failed for this pattern and that matching should move to the next pattern.
If control reaches the end of the translation without reaching a ``FAIL``, then it has matched, and following patterns are ignored. If control reaches the end of the translation without reaching a ``FAIL``, then it has matched, and following patterns are ignored.
All the translations below include guards. If no guard is present, simply substitute the guard ``if True`` when translating.
Variables of the form ``$ALL_CAPS`` are meta-variables holding a syntactic element, they are not normal variables. Variables of the form ``$ALL_CAPS`` are meta-variables holding a syntactic element, they are not normal variables.
So, ``$VARS = $items`` is not an assignment of ``$items`` to ``$VARS``, So, ``$VARS = $items`` is not an assignment of ``$items`` to ``$VARS``,
@ -241,7 +245,7 @@ A pattern not including a star pattern::
translates to:: translates to::
$kind = type($value).__match_container__ $kind = type($value).__match_container__
if $kind & MATCH_SEQUENCE == 0: if $kind != MATCH_SEQUENCE:
FAIL FAIL
if len($value) != len($VARS): if len($value) != len($VARS):
FAIL FAIL
@ -256,7 +260,7 @@ A pattern including a star pattern::
translates to:: translates to::
$kind = type($value).__match_container__ $kind = type($value).__match_container__
if $kind & MATCH_SEQUENCE == 0: if $kind != MATCH_SEQUENCE:
FAIL FAIL
if len($value) < len($VARS): if len($value) < len($VARS):
FAIL FAIL
@ -275,7 +279,7 @@ translates to::
$sentinel = object() $sentinel = object()
$kind = type($value).__match_container__ $kind = type($value).__match_container__
if $kind & MATCH_MAPPING == 0: if $kind != MATCH_MAPPING:
FAIL FAIL
# $KEYWORD_PATTERNS is a meta-variable mapping names to variables. # $KEYWORD_PATTERNS is a meta-variable mapping names to variables.
for $KEYWORD in $KEYWORD_PATTERNS: for $KEYWORD in $KEYWORD_PATTERNS:
@ -293,7 +297,7 @@ A pattern including a double-star pattern::
translates to:: translates to::
$kind = type($value).__match_container__ $kind = type($value).__match_container__
if $kind & MATCH_MAPPING == 0: if $kind != MATCH_MAPPING:
FAIL FAIL
# $KEYWORD_PATTERNS is a meta-variable mapping names to variables. # $KEYWORD_PATTERNS is a meta-variable mapping names to variables.
$tmp = dict($value) $tmp = dict($value)
@ -317,12 +321,6 @@ translates to::
if not isinstance($value, ClsName): if not isinstance($value, ClsName):
FAIL FAIL
.. note::
``case ClsName():`` is the only class pattern that can succeed if
``($kind & (MATCH_SELF|MATCH_ATTRIBUTES)) == 0``
Class pattern with a single positional pattern:: Class pattern with a single positional pattern::
case ClsName($VAR): case ClsName($VAR):
@ -330,7 +328,7 @@ Class pattern with a single positional pattern::
translates to:: translates to::
$kind = type($value).__match_class__ $kind = type($value).__match_class__
if $kind & MATCH_SELF: if $kind == MATCH_SELF:
if not isinstance($value, ClsName): if not isinstance($value, ClsName):
FAIL FAIL
$VAR = $value $VAR = $value
@ -346,17 +344,13 @@ translates to::
if not isinstance($value, ClsName): if not isinstance($value, ClsName):
FAIL FAIL
$kind = type($value).__match_class__ $attrs = ClsName.__match_args__
if $kind & MATCH_ATTRIBUTES: if len($attr) < len($VARS):
$attrs = ClsName.__match_args__ raise TypeError(...)
if len($attr) < len($VARS): try:
raise TypeError(...) for i, $VAR in enumerate($VARS):
try: $VAR = getattr($value, $attrs[i])
for i, $VAR in enumerate($VARS): except AttributeError:
$VAR = getattr($value, $attrs[i])
except AttributeError:
FAIL
else:
FAIL FAIL
Example: [6]_ Example: [6]_
@ -369,15 +363,11 @@ translates to::
if not isinstance($value, ClsName): if not isinstance($value, ClsName):
FAIL FAIL
$kind = type($value).__match_class__ try:
if $kind & MATCH_ATTRIBUTES: for $KEYWORD in $KEYWORD_PATTERNS:
try: $tmp = getattr($value, QUOTE($KEYWORD))
for $KEYWORD in $KEYWORD_PATTERNS: $KEYWORD_PATTERNS[$KEYWORD] = $tmp
$tmp = getattr($value, QUOTE($KEYWORD)) except AttributeError:
$KEYWORD_PATTERNS[$KEYWORD] = $tmp
except AttributeError:
FAIL
else:
FAIL FAIL
Example: [7]_ Example: [7]_
@ -390,23 +380,19 @@ translates to::
if not isinstance($value, ClsName): if not isinstance($value, ClsName):
FAIL FAIL
$kind = type($value).__match_class__ $attrs = ClsName.__match_args__
if $kind & MATCH_ATTRIBUTES: if len($attr) < len($VARS):
$attrs = ClsName.__match_args__ raise TypeError(...)
if len($attr) < len($VARS): $pos_attrs = $attrs[:len($VARS)]
raise TypeError(...) try:
$pos_attrs = $attrs[:len($VARS)] for i, $VAR in enumerate($VARS):
try: $VAR = getattr($value, $attrs[i])
for i, $VAR in enumerate($VARS): for $KEYWORD in $KEYWORD_PATTERNS:
$VAR = getattr($value, $attrs[i]) $name = QUOTE($KEYWORD)
for $KEYWORD in $KEYWORD_PATTERNS: if $name in pos_attrs:
$name = QUOTE($KEYWORD) raise TypeError(...)
if $name in pos_attrs: $KEYWORD_PATTERNS[$KEYWORD] = getattr($value, $name)
raise TypeError(...) except AttributeError:
$KEYWORD_PATTERNS[$KEYWORD] = getattr($value, $name)
except AttributeError:
FAIL
else:
FAIL FAIL
Example: [8]_ Example: [8]_
@ -425,7 +411,7 @@ For example, the pattern::
translates to:: translates to::
$kind = type($value).__match_class__ $kind = type($value).__match_class__
if $kind & MATCH_SEQUENCE == 0: if $kind != MATCH_SEQUENCE:
FAIL FAIL
if len($value) != 2: if len($value) != 2:
FAIL FAIL
@ -457,11 +443,10 @@ All classes should ensure that the the values of ``__match_container__``, ``__ma
and ``__match_args__`` follow the specification. and ``__match_args__`` follow the specification.
Therefore, implementations can assume, without checking, that the following are true:: Therefore, implementations can assume, without checking, that the following are true::
(__match_container__ & (MATCH_SEQUENCE | MATCH_MAPPING)) != (MATCH_SEQUENCE | MATCH_MAPPING) __match_container__ == 0 or __match_container__ == MATCH_SEQUENCE or __match_container__ == MATCH_MAPPING
(__match_class__ & (MATCH_SELF | MATCH_ATTRIBUTES)) != (MATCH_SELF | MATCH_ATTRIBUTES) __match_class__ == 0 or __match_class__ == MATCH_SELF
Thus, implementations can assume that ``__match_container__ & MATCH_SEQUENCE`` implies ``(__match_container__ & MATCH_MAPPING) == 0``, and vice-versa. and that ``__match_args__`` is a tuple of unique strings.
Likewise for ``__match_class__``, ``MATCH_SELF`` and ``MATCH_ATTRIBUTES``.
Values of the special attributes for classes in the standard library Values of the special attributes for classes in the standard library
-------------------------------------------------------------------- --------------------------------------------------------------------
@ -518,9 +503,9 @@ Implementations are allowed to make the following assumptions:
* ``isinstance(obj, cls)`` can be freely replaced with ``issubclass(type(obj), cls)`` and vice-versa. * ``isinstance(obj, cls)`` can be freely replaced with ``issubclass(type(obj), cls)`` and vice-versa.
* ``isinstance(obj, cls)`` will always return the same result for any ``(obj, cls)`` pair and repeated calls can thus be elided. * ``isinstance(obj, cls)`` will always return the same result for any ``(obj, cls)`` pair and repeated calls can thus be elided.
* Reading any of ``__match_container__``, ``__match_class__`` or ``__match_args__`` is a pure operation, and may be cached. * Reading any of ``__match_container__``, ``__match_class__`` or ``__match_args__`` is a pure operation, and may be cached.
* Sequences, that is any class for which ``__match_container__&MATCH_SEQUENCE`` is not zero, are not modified by iteration, subscripting or calls to ``len()``. * Sequences, that is any class for which ``__match_container__ == MATCH_SEQUENCE`` is not zero, are not modified by iteration, subscripting or calls to ``len()``.
Consequently, those operations can be freely substituted for each other where they would be equivalent when applied to an immutable sequence. Consequently, those operations can be freely substituted for each other where they would be equivalent when applied to an immutable sequence.
* Mappings, that is any class for which ``__match_container__&MATCH_MAPPING`` is not zero, will not capture the second argument of the ``get()`` method. * Mappings, that is any class for which ``__match_container__ == MATCH_MAPPING`` is not zero, will not capture the second argument of the ``get()`` method.
So, the ``$sentinel`` value may be freely re-used. So, the ``$sentinel`` value may be freely re-used.
In fact, implementations are encouraged to make these assumptions, as it is likely to result in signficantly better performance. In fact, implementations are encouraged to make these assumptions, as it is likely to result in signficantly better performance.
@ -668,7 +653,7 @@ Rejected Ideas
Using attributes from the instance's dictionary Using attributes from the instance's dictionary
----------------------------------------------- -----------------------------------------------
An earlier version of this PEP only used attributes from the instance's dictionary when matching a class pattern with ``MATCH_ATTRIBUTES``. An earlier version of this PEP only used attributes from the instance's dictionary when matching a class pattern with ``__match_class__`` was the default value.
The intent was to avoid capturing bound-methods and other synthetic attributes. However, this also mean that properties were ignored. The intent was to avoid capturing bound-methods and other synthetic attributes. However, this also mean that properties were ignored.
For the class:: For the class::
@ -683,7 +668,7 @@ For the class::
... ...
Ideally we would match the attributes "a" and "p", but not "m". Ideally we would match the attributes "a" and "p", but not "m".
However, there is no general way to do that, so this PEP now follows the semantics of PEP 634 for ``MATCH_ATTRIBUTES``. However, there is no general way to do that, so this PEP now follows the semantics of PEP 634.
Lookup of ``__match_args__`` on the subject not the pattern Lookup of ``__match_args__`` on the subject not the pattern
----------------------------------------------------------- -----------------------------------------------------------
@ -703,6 +688,7 @@ An earlier version of this PEP combined ``__match_class__`` and ``__match_contai
Using a single value has a small advantage in terms of performance, Using a single value has a small advantage in terms of performance,
but is likely to result in unintended changes to container matching when overriding class matching behavior, and vice versa. but is likely to result in unintended changes to container matching when overriding class matching behavior, and vice versa.
Deferred Ideas Deferred Ideas
============== ==============
@ -722,6 +708,12 @@ but is tricky. With these additional features it can be implemented easily [9]_.
This idea will feature in a future PEP for 3.11. This idea will feature in a future PEP for 3.11.
However, it is too late in the 3.10 development cycle for such a change. However, it is too late in the 3.10 development cycle for such a change.
Having a separate value to reject all class matches
---------------------------------------------------
In an earlier version of this PEP, there was a distinct value for ``__match_class__`` that allowed classes to not match any class
pattern that would have required deconstruction. However, this would become redundant once ``MATCH_POSITIONAL`` is introduced, and
complicates the specification for an extremely rare case.
References References
========== ==========
@ -748,7 +740,7 @@ This::
translates to:: translates to::
$kind = type($value).__match_container__ $kind = type($value).__match_container__
if $kind & MATCH_SEQUENCE == 0: if $kind != MATCH_SEQUENCE:
FAIL FAIL
if len($value) != 2: if len($value) != 2:
FAIL FAIL
@ -765,7 +757,7 @@ This::
translates to:: translates to::
$kind = type($value).__match_container__ $kind = type($value).__match_container__
if $kind & MATCH_SEQUENCE == 0: if $kind != MATCH_SEQUENCE:
FAIL FAIL
if len($value) < 2: if len($value) < 2:
FAIL FAIL
@ -780,7 +772,7 @@ This::
translates to:: translates to::
$kind = type($value).__match_container__ $kind = type($value).__match_container__
if $kind & MATCH_MAPPING == 0: if $kind != MATCH_MAPPING:
FAIL FAIL
$tmp = $value.get("x", $sentinel) $tmp = $value.get("x", $sentinel)
if $tmp is $sentinel: if $tmp is $sentinel:
@ -802,7 +794,7 @@ This::
translates to:: translates to::
$kind = type($value).__match_container__ $kind = type($value).__match_container__
if $kind & MATCH_MAPPING == 0: if $kind != MATCH_MAPPING:
FAIL FAIL
$tmp = dict($value) $tmp = dict($value)
if not $tmp.keys() >= {"x", "y"}: if not $tmp.keys() >= {"x", "y"}:
@ -821,17 +813,13 @@ translates to::
if not isinstance($value, ClsName): if not isinstance($value, ClsName):
FAIL FAIL
$kind = type($value).__match_class__ $attrs = ClsName.__match_args__
if $kind & MATCH_ATTRIBUTES: if len($attr) < 2:
$attrs = ClsName.__match_args__ FAIL
if len($attr) < 2: try:
FAIL x = getattr($value, $attrs[0])
try: y = getattr($value, $attrs[1])
x = getattr($value, $attrs[0]) except AttributeError:
y = getattr($value, $attrs[1])
except AttributeError:
FAIL
else:
FAIL FAIL
.. [7] .. [7]
@ -844,14 +832,10 @@ translates to::
if not isinstance($value, ClsName): if not isinstance($value, ClsName):
FAIL FAIL
$kind = type($value).__match_class__ try:
lif $kind & MATCH_ATTRIBUTES: x = $value.a
try: y = $value.b
x = $value.a except AttributeError:
y = $value.b
except AttributeError:
FAIL
else:
FAIL FAIL
.. [8] .. [8]
@ -865,20 +849,16 @@ translates to::
if not isinstance($value, ClsName): if not isinstance($value, ClsName):
FAIL FAIL
$kind = type($value).__match_class__ $attrs = ClsName.__match_args__
if $kind & MATCH_ATTRIBUTES: if len($attr) < 1:
$attrs = ClsName.__match_args__ raise TypeError(...)
if len($attr) < 1: $positional_names = $attrs[:1]
try:
x = getattr($value, $attrs[0])
if "a" in $positional_names:
raise TypeError(...) raise TypeError(...)
$positional_names = $attrs[:1] y = $value.a
try: except AttributeError:
x = getattr($value, $attrs[0])
if "a" in $positional_names:
raise TypeError(...)
y = $value.a
except AttributeError:
FAIL
else:
FAIL FAIL
.. [9] .. [9]