重写dict的__contains__方法在第一个键:值对之后中断



我的用例示例:

m = Mapping()
m["John Doe"] = "PERSON"
m["Google"] = "ORG"

我希望能够NOT插入诸如";John"约翰先生;或";Doe先生;因为在映射中存在类似的实体("John Doe")。

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

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

为什么这个键在第一个键之后就断了?

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

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

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

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

  1. 您可以直接对dict进行子类化,然后使用对super()的调用来访问默认的dict功能。这需要覆盖几乎所有默认的dict.__dunder__方法,因为它们中的大多数都是硬连接的,可以在不首先对它们进行规范化的情况下使用键。最初的帖子试图遵循这个模型,但没有使用super()来访问dict的内部内容,因此无法按预期工作
  2. 您可以编写一个具有自己的.data属性的容器类来存储字典的规范版本,并在其中实现所有相关的__dunder__方法。这与方法#1非常相似,只是您将该功能应用于.data,而不是调用super()来访问基本的dict功能
  3. 您可以将抽象基类MutableMapping的子类化,再次将dict的规范版本存储在.data属性中。这使您可以定义较少的__dunder__方法,同时自动推断其余方法。因此,通常情况下,这比1和2更可取
  4. 您可以使用从collections导入UserDict并对其进行子类化。这会自动为所有相关方法定义默认的类似字典的行为,将底层dict存储在.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下存储Doe先生的条目,则该密钥需要以某种方式同时包含"John"one_answers"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";,然后(取决于你要求的匹配的接近程度)所有未来引用";约翰;或";Doe";可能会重定向到此现有条目。但如果您将他的初始条目存储在";Doe先生;然后稍后尝试查找";约翰;它可能不会意识到这些距离足够近,无法匹配。因此,请注意,这种方法可能相当挑剔,不一定是个好主意!但是你问。。。

最新更新