如何匹配一个空字典?

huangapple go评论60阅读模式
英文:

How to match an empty dictionary?

问题

Python自版本3.10起支持结构化模式匹配。我注意到,与匹配list不同,简单地匹配{}并不能匹配空的dict

根据我的朴素方法,非空的dict也会被匹配(Python 3.10.4):

def match_empty(m):
    match m:
        case []:
            print("空列表")
        case {}:
            print("空字典")
        case _:
            print("非空")
match_empty([])           # 空列表
match_empty([1, 2])       # 非空
match_empty({})           # 空字典
match_empty({'a': 1})     # 空字典

甚至匹配构造函数也会破坏空列表的匹配:

def match_empty(m):
    match m:
        case list():
            print("空列表")
        case dict():
            print("空字典")
        case _:
            print("非空")
match_empty([])           # 空列表
match_empty([1, 2])       # 空列表
match_empty({})           # 空字典
match_empty({'a': 1})     # 空字典

下面是一个可以按照我的预期工作的解决方案:

def match_empty(m):
    match m:
        case []:
            print("空列表")
        case d:
            if isinstance(d, dict) and len(d) == 0:
                print("空字典")
                return
            print("非空")
match_empty([])           # 空列表
match_empty([1, 2])       # 非空
match_empty({})           # 空字典
match_empty({'a': 1})     # 非空

现在我的问题是:

  • 为什么我的前两种方法不像预期那样工作?
  • 是否有一种方法可以使用结构化模式匹配来仅匹配空的dict(而不需要显式检查dict的长度)?
英文:

Python supports Structural Pattern Matching since version 3.10.
I came to notice that matching an empty dict doesn't work by simply matching {} as it does for lists.
According to my naive approach, non-empty dicts are also matched (Python 3.10.4):

def match_empty(m):
    match m:
        case []:
            print("empty list")
        case {}:
            print("empty dict")
        case _:
            print("not empty")
match_empty([])           # empty list
match_empty([1, 2])       # not empty
match_empty({})           # empty dict
match_empty({'a': 1})     # empty dict

Matching the constructors even breaks the empty list matching:

def match_empty(m):
    match m:
        case list():
            print("empty list")
        case dict():
            print("empty dict")
        case _:
            print("not empty")
match_empty([])           # empty list
match_empty([1, 2])       # empty list
match_empty({})           # empty dict
match_empty({'a': 1})     # empty dict

Here is a solution, that works as I expect:

def match_empty(m):
    match m:
        case []:
            print("empty list")
        case d:
            if isinstance(d, dict) and len(d) == 0:
                print("empty dict")
                return
            print("not empty")
match_empty([])           # empty list
match_empty([1, 2])       # not empty
match_empty({})           # empty dict
match_empty({'a': 1})     # not empty

Now my questions are:

  • Why do my first 2 approaches not work (as expected)?

  • Is there a way to use structural pattern matching to match only an empty dict (without checking the dict length explicitly)?

答案1

得分: 4

如指定,映射捕获用于匹配键/值的某些子结构。空字典是任何非空字典的"子字典",因此模式{}可以捕获任何非空字典。

您可以添加guard以指定不存在"额外"项:

>>> def match_empty(m):
...     match m:
...         case []:
...             print("empty list")
...         case {**extra} if not extra:
...             print("empty dict")
...         case _:
...             print("not empty")
... 
>>> match_empty({})
empty dict
>>> match_empty({1:1})
not empty

关于为什么映射模式默认允许额外项的一些理由,请参考PEP 635 – 结构化模式匹配:动机和基本原理

映射模式反映了字典查找的常见用法:它允许用户通过常量/已知键从映射中提取一些值,并使这些值匹配给定的子模式。即使不存在**rest,主题中的额外键也会被忽略。这与序列模式不同,其中额外的项将导致匹配失败。但映射实际上与序列不同:它们具有自然的结构子类型行为,即传递具有额外键的字典可能会正常工作。

最后,这里可能值得指出问题中存在一些轻微的误解:

匹配构造函数甚至破坏了空列表的匹配

这不是"匹配构造函数"。在那里写的list()dict()实际上不会创建任何实际的列表或字典实例。这只是匹配类型的正常方式。它不能只是listdict,因为那将是名称绑定。

英文:

As specified, mapping captures are used to match on some substructure of keys/values. An empty dict is a "subdict" of any non-empty dict, so the pattern {} may capture any non-empty dict.

You can add a guard to specify that there are no "extra" items present:

>>> def match_empty(m):
...     match m:
...         case []:
...             print("empty list")
...         case {**extra} if not extra:
...             print("empty dict")
...         case _:
...             print("not empty")
... 
>>> match_empty({})
empty dict
>>> match_empty({1:1})
not empty

For some justification of why mapping patterns allow extra items by default, refer to PEP 635 – Structural Pattern Matching: Motivation and Rationale:

> The mapping pattern reflects the common usage of dictionary lookup: it allows the user to extract some values from a mapping by means of constant/known keys and have the values match given subpatterns. Extra keys in the subject are ignored even if **rest is not present. This is different from sequence patterns, where extra items will cause a match to fail. But mappings are actually different from sequences: they have natural structural sub-typing behavior, i.e., passing a dictionary with extra keys somewhere will likely just work.

Finally, there was a slight misconception in the question that may be worth pointing out here:

> Matching the constructors even breaks the empty list matching

This is not "matching the constructors". No actual list or dict instance will be created by the list() or dict() written there. It's just the normal way to match on types. It can't be just list or dict because that would be a name binding.

答案2

得分: 1

使用映射(字典)作为匹配模式与使用序列(列表)略有不同。您可以通过键-值对匹配字典的结构,其中键是文字,值可以是捕获模式,因此可以在case中使用。

您可以在映射模式中使用**rest来捕获主题中的附加键。与列表的主要区别是 - "在主题中的额外键将被忽略而不会匹配"

因此,当您将{}用作case时,实际上是在告诉Python"匹配一个字典,没有任何约束",而不是"匹配一个空字典"。因此,可能比您的最后尝试稍微更优雅的一种方法是:

def match_empty(m):
    match m:
        case []:
            print("空列表")
        case {**keys}:
            if keys:
                print("非空字典")
            else:
                print("空字典")
        case _:
            print("非空")

我认为这感觉尴尬并且效果不佳的主要原因是因为此功能不打算与混合类型一起使用。也就是说,您首先将该功能用作类型检查器,然后用作模式匹配。如果您知道m是一个字典(并且希望匹配其"内部"),那么这将效果更好。

英文:

Using a mapping (dict) as the match pattern works a bit differently than using a sequence (list). You can match the dict's structure by key-value pairs where the key is a literal and the value can be a capture pattern so it is used in the case.

You can use **rest within a mapping pattern to capture additional keys in the subject. The main difference with lists is - "extra keys in the subject will be ignored while matching".

So when you use {} as a case, what you're really telling Python is "match a dict, no constraints whatsoever", and not "match an empty dict". So one way that might be slightly more elegant than your last attempt is:

def match_empty(m):
    match m:
        case []:
            print("empty list")
        case {**keys}:
            if keys:
                print("non-empty dict")
            else:
                print("empty dict")
        case _:
            print("not empty")

I think the main reason this feels awkward and doesn't work good is because the feature wasn't intended to work with mixed types like this. i.e. you're using the feature as a type-checker first and then as pattern matching. If you knew that m is a dict (and wanted to match its "insides"), this would work much nicer.

huangapple
  • 本文由 发表于 2023年2月9日 00:57:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/75389166.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定