-
Notifications
You must be signed in to change notification settings - Fork 65
'Mapping' destructuring #10
Comments
Also, you could have:
|
Oh, neat! I guess the implementation could be
This sure beats the work of implementing support for
|
(...and explaining why if you leave out |
In my notes I require I also proposed to allow name patterns in key positions, but this is mostly to allow fixed length match. But this can easily be checked with a guard, so I also withdraw this idea. |
I think there's enough agreement here to mark it as 'accepted'. (The minor issue of whether to support |
Whoa, I take it back. Ivan's PEP currently doesn't support |
Okay, in the end we opted for |
I've begun implementing this, and have come across a couple of questions that the PEP does not explicitly address. The answers seem clear to me, but they should probably be included in the spec:
Finally, it's not clear to me how match {"x": False, "y": True}:
case {"x" | "y": True}: # Does this match?
... |
Maybe skip the OR in keys for now?
|
Yeah, I think that's a good idea. It's also worth considering omitting them from the proposal, since it seems they have few real-world use cases and could easily be added later. |
If we were to do it, some kind of distributive law should apply, like {X|Y} should be like {X}|{Y}. But fine to move to Rejected/Postponed for now. |
Flagging for revision since we're dropping |
I've come across a couple of questions that I'd like input on. Consider the following example: from collections import defaultdict
match defaultdict(int):
case {"XXX": _}:
... Does this case match? The current implementation copies the target into a But I'm starting to wonder if it should. It would be pretty straightforward to turn the key pops into regular lookups on the target itself. There would just be more bookkeeping involved. Which leads me to my second question. How should we handle duplicate keys? xxx = "XXX"
match d:
case {"XXX": _, "XXX": _}:
pass
case {"XXX": _, .xxx: _}:
pass Currently, neither of these cases match. That seems fine to me, but perhaps it makes more sense to raise... I'm not sure. Again, we would just require more bookkeeping to catch these. Thoughts? |
|
I think I agree. We do currently allow |
In particular the PEP still shows |
In my code review you pointed out that some cases don't catch duplicate keys. It turns out that we do in fact check for duplicate keys, but many cases short-circuit and fail before we can get that far. Some cases are when:
>>> match None:
... case {"x": _, "x": _}:
... ...
...
>>>
>>> match {}:
... case {"x": _, "x": _}:
... ...
...
>>>
>>> match {"y": None, "z": None}:
... case {"x": _, "x": _}:
... ...
...
>>> It's only when all of these steps succeed that we actually get to the duplicate and raise: >>> match {"x": None, "y": None}:
... case {"x": _, "x": _}:
... ...
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: mapping pattern checks duplicate key ('x')
>>> I think that the first three cases are best caught by linters, since they have no effect on the match at runtime. If a particular match gets far enough that we actually hit the second lookup (like the last example), then sure, we should absolutely raise. But a lot of the design choices in the implementation have favored doing as little extra work as possible when failing a match, so it seems wasteful to try to check the keys of a pattern that isn't going to match anyways. This could come with a significant runtime penalty for many heavily nested cases... even the ones with no duplicates! Further, checking this in the compiler feels wrong. It would only work when literals (not name lookups) conflict. We also don't do this anywhere else: the statement So I vote we leave the current behavior as-is. It's performant, and still catches the cases that matter. |
Hm, I still want to push back a little here. Outside patterns, duplicate dict keys are not errors -- we use "last one wins". In patterns, we reject duplicate keys when we get to matching. So I think we have decided that mappin patterns with duplicate keys are statically invalid, and for cases where we can detect this just by parsing the thing (i.e. when both keys are literals) making this a compilation error seems right. (Just like we do for duplicate keyword args in calls. Which match also should reject statically.) Obviously we'd still need the runtime check to catch cases like
|
Hm. Would the compiler raise a I'm not aware of any cases where the compiler raises anything besides |
If the compiler detects it, it should raise But frankly I could also live with only the runtime detection -- it's unlikely that people write the same literal key twice, and it will still be caught when actually matching. (That's why I wrote "push back a little.") |
I see in the pep that only constant value patterns or literals are allowed as keys, but I don't see a justification their or here on why (I can make several guesses, and it seems reasonable). Is there any reason that tuples of these are also disallowed? They should be unchangeable, unnamed, and, at least in principal, possible to check for uniqueness. As an additional feedback on the wording in the pep, as noted above, it states that constant value patterns or literals can be used. This would send the reader to the section on Constant value patterns. That section mentions that this is used for comparing actual constants and Enum values. It may not be obvious to readers that it is possible to check for the existence of a key using this pattern that is not a constant or an Enum. For instance: class Foo:
'''Fixed hash otherwise non constant'''
def __hash__(self):
return 12
f = Foo()
tup_key = (1,2)
mapping = {f: 'hello', tup_key: "world"}
match mapping:
case {.f: first, .tup_key: second}:
print(first, second)
case _:
print('default') |
Yeah, this went through a few revisions and the text is not entirely consistent between sections. We hadn't thought of using (constant) tuples for keys, maybe we could add that in the future (or if there's a groundswell of demand during the review phase :-). PS. In order to make the example valid you'd have to add an |
Oh good point on equals, I was playing too fast when typing this up. It happened to work in my interpreter, but I guess that's just because I didn't hit any collisions in the hash table. I personally am a +1 on constant tuples, as I use them all the time. However, I am also a +1 in that it probably is not a reason to delay, as there is a way to get the behavior with no changes. |
It's because the >>> class Foo:
... '''Fixed hash otherwise non constant'''
... def __hash__(self):
... return 12
...
>>> f1 = Foo()
>>> f2 = Foo()
>>> tup_key = (1,2)
>>> mapping = {f1: 'hello', tup_key: "world"}
>>>
>>> match mapping:
... case {.f2: first, .tup_key: second}:
... print("f2")
... case {.f1: first, .tup_key: second}:
... print("f1")
...
f1 |
Yeah, but if you only use one object you don't need to define |
Yeah, I must admit “constant” never really sat right with me. “Value” captures the meaning well. |
Yeah the hash was not really necessary to convey the idea of "constness", I'm sorry for the extra, I was playing in interactive mode and just copied what I had. (it was not even working how I was thinking anyway as you two were kind to point out, it was falling back to id) I will try to be clearer and slow down a bit in any future posts. "value pattern" does seem an easier term to teach. +1 |
See #40 (comment) about the terminology. |
Just like
case [a, b]
is a Sequence destructuring, we could definecase {key: value, ...}
as a Mapping destructuring. Semantics:{"a": 1, "b": 2}
is a match for the pattern{"a": a}
.collections.abc.Mapping
.case dict(key1=a, key2=b)
, (implemented bydict.__match__()
) but this would probably only match actualdict
instances (ordict
subclass instances).Questions:
case {k: v}
would match.case {"a"|"A": a}
?case {"a": a, **rest}
?The text was updated successfully, but these errors were encountered: