【问题标题】:Overriding __contains__ method of dict breaks after first key:value pair在第一个键:值对之后覆盖 __contains__ 方法的 dict 中断
【发布时间】:2022-06-26 01:26:42
【问题描述】:

我的用例与示例:

m = Mapping()

m["John Doe"] = "PERSON"
m["Google"] = "ORG"

我希望能够插入“John”、“Mr. John”或“Mr. Doe”等实体,因为映射中存在类似的实体(“John Doe”)。

这是我的 Mapping 类的实现(请检查包含):

from fuzzywuzzy import fuzz

class Mapping(dict):
    def __setitem__(self, key, item):
        self.__dict__[key] = item

    def __getitem__(self, key):
        return self.__dict__[key]

    def __repr__(self):
        return repr(self.__dict__)

    def __len__(self):
        return len(self.__dict__)

    def __delitem__(self, key):
        del self.__dict__[key]

    def clear(self):
        return self.__dict__.clear()

    def copy(self):
        return self.__dict__.copy()

    def has_key(self, k):
        return k in self.__dict__

    def update(self, *args, **kwargs):
        return self.__dict__.update(*args, **kwargs)

    def keys(self):
        return self.__dict__.keys()

    def values(self):
        return self.__dict__.values()

    def items(self):
        return self.__dict__.items()

    def pop(self, *args):
        return self.__dict__.pop(*args)

    def __cmp__(self, dict_):
        return self.__cmp__(self.__dict__, dict_)

    def __contains__(self, item):
        for key in self.__dict__.keys():
# if incoming token is a partial or complete match of an existing token in the mapping
            if fuzz.partial_token_set_ratio(item, key) == 100: 
                return True
            else:
                return False

    def __iter__(self):
        return iter(self.__dict__)

    def __unicode__(self):
        return unicode(repr(self.__dict__))

这打破了我的例子:

"John" in m # returns True as expected
"Goo" in m # returns False ?

fuzz.partial_token_set_ratio("Goo", "Google") # returns 100
fuzz.partial_token_set_ratio("John", "John Doe") # returns 100

为什么在第一个键之后会中断?

【问题讨论】:

  • 扩展 dict 的全部意义在于,您不必复制和粘贴所有方法来让它们工作。
  • __dict__ 不是你想象的那样。看看m.__dict__ 看看我的意思。
  • 你为什么要这样?这没有道理。现在,您的 Mapping 对象带有 两个字典,即对象本身(这是一个 dict),一个 __dict__ 字典,这是它的命名空间。
  • 另外,这不起作用,因为您的 __contains__ 方法总是在第一次迭代时返回。但这一切似乎都被误导了。
  • 我希望能够消除名称的多种变体并合并到一个键中。

标签: python dictionary fuzzywuzzy


【解决方案1】:

让我们从一个更简单的问题开始。假设您希望字典 D 在内部使用像 "John Doe" 这样的键,但您希望它仍然能够让 D["Mr. John Doe"] 返回与 D["John Doe"] 相同的内容。

为此,我将编写一个函数,将任何可接受的名称(如“Mr. John Doe”)转换为其规范版本。

def canonical_version_of(name: str) -> str:
    return name.removeprefix('Mr. ')  # Requires Python 3.9+

然后您需要创建某种自定义字典子类,该子类将在内部使用规范版本的名称。有 4 个主要选项:

  1. 你可以直接继承dict,然后调用super()来访问默认的dict功能。这需要覆盖几乎所有默认的dict.__dunder__ 方法,因为它们中的大多数都是硬连线的,可以在没有先规范化它们的情况下使用键进行操作。原帖试图遵循这个模型,但没有使用super() 来访问字典的内部内容,因此无法按预期工作。
  2. 您可以编写一个带有自己的.data 属性的容器类来存储字典的规范版本,并在其中实现所有相关的__dunder__ 方法。这最终与方法#1 非常相似,除了调用super() 来访问基本的dict 功能,您可以将该功能应用于.data
  3. 您可以将抽象基类MutableMapping 子类化,再次将字典的规范版本存储在.data 属性中。这使您可以定义更少的 __dunder__ 方法,而它会自动推断其余的方法。所以通常这比 1 和 2 更可取。
  4. 您可以使用 import UserDict from collections 并将其子类化。这会自动为所有相关方法定义默认的类似字典的行为,将底层字典存储在 .data 属性中。一般来说,这可能是最简单的方法,最大限度地减少您需要覆盖的__dunder__ 方法的数量,并且访问.data 通常比调用super() 更直观一些。

无论您选择上述哪种方法,您都需要编写一些__dunder__ 方法(至少__getitem____setitem____contains__)在读取或写入之前将每个给定密钥转换为其规范版本到字典。使用方法 #4,这里是一个部分示例,尽管我还没有查看完整的 __dunder__ 方法列表来检查您是否需要规范化。

from collections import UserDict

class CanonicalDict(UserDict):
    """Like an ordinary dict, except canonical_version_of(key) is always
       used in place of ordinary keys."""

    def __getitem__(self, key): 
        return self.data[canonical_version_of(key)]

    def __setitem__(self, key, value): 
        self.data[canonical_version_of(key)] = value
    
    def __contains__(self, key): 
        return canonical_version_of(key) in self.data

这种方法可以很容易地推广到更改 canonical_version_of 以以其他方式规范化名称,例如处理“Doe,John”或“JOHN DOE”或“Doe,John Doe”。这将为您提供大部分您在原始帖子中所要求的内容。

但是,这种方法确实要求任何给定密钥的信息足够丰富以生成名称的规范版本,例如,如果内部 .data 将在 John Doe 下存储 Mr. Doe 的条目,那么密钥将需要以某种方式在其中包含“John”和“Doe”。这会妨碍原始帖子希望能够使用“先生”。 Doe' 或 'John' 作为访问字典的键。

要获得这样的结果,您可以重写 canonical_version_of 以首先在字典的现有键中查找足够接近的匹配项,如果找不到,则返回给定的键。由于这个版本的canonical_version_of 需要字典才能查看,我会将其设为您自定义的CanonicalDict 类的方法,然后让__getitem__ 等其他方法将其称为self.canonical_version_of(key)

from fuzzywuzzy import process

class CanonicalDict(UserDict):
    def canonical_version_of(self, name:str) -> str:
        """Returns the best match for name among this CanonicalDict's keys if 
           that match scores at least 90%.  Otherwise returns name."""
        if not self.data: return name  # handle case where this dict is empty
        candidate, score = process.ExtractOne(name, self.data.keys())
        if score >= 90: return candidate
        return name

    def __getitem__(self, key):
        return self.data[self.canonical_version_of(key)]

    # ... and similarly for other __dunder__ methods...

这种方法基本上采用最初用于个人的任何名称作为该个人的“规范名称”,然后在内部继续将该名称用于任何其他足够接近的名称,仅当它们不同时才允许创造新的规范名称 -所有现有的都足够了。

只有在我确定字典中只存储几个键(理想情况下,我可以用“好”规范名称预先播种的键)并且这些键是的情况下,我才会使用这样的方案都相距甚远。例如,这可能是一种很好的方法,可以用来帮助人们使用您创建的密钥在游戏百科全书中查找条目。但是,如果您的字典可能包含名称相似或重叠的键,那么这种模糊匹配方法的结果可能非常不可预测,并且高度依赖于键存储的顺序。例如,如果您将 John Doe 的初始条目存储在“John Doe”下,那么(取决于您要求匹配的接近程度)所有未来对“John”或“Doe”的引用可能会重定向到这个现有条目。但是,如果您将他的初始条目存储在“Mr. Doe”下,然后再尝试查找“John”,它可能不会将这些条目识别为足够接近以匹配。因此请注意,这种方法可能非常挑剔,不一定是个好主意!但是你问...

【讨论】:

    猜你喜欢
    • 2012-05-13
    • 1970-01-01
    • 1970-01-01
    • 2021-04-01
    • 1970-01-01
    • 2017-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多