在 Python3 中嵌套默认词典

Nesting Defaultdictionaries in Python3

我正在编写一段代码,这是我第一次从 collections 实现 defaultdict。 目前,我有一段代码可以像 defaultdict 一样正常工作,但我真的很想嵌套我的字典。

这是我目前拥有的代码:

from collections import defaultdict, Counter
from re import findall

class Class: 
    def __init__(self, n, file):
        self.counts = defaultdict(lambda: defaultdict(int))
        self.__n = n
        self.__file = file

    def function(self, starting_text, charas):
        self.__starting_text = starting_text
        self.__charas = charas

        with open(self.__file) as file:
            text = file.read().lower().replace('\n', ' ')

        ngrams = [text[i : i + self.__n] for i in range(len(text))]

        out = self.counts
        for item in ngrams:
            data = []
            for word in findall( item+".", text):
                data.append(word[-1])
            self.counts = { item : data.count(item) for item in data }
            out[item] = self.counts
        self.counts = out

代码中的一些东西还没有实现,因为我有点停滞不前,所以请忽略任何不适用于这个特定问题的东西!

如果我 运行 print(self.counts) 最后,我的程序 运行 看起来像这样:

defaultdict(<function Class.__init__.<locals>.<lambda> at 0x7f8c4a94bea0>, {'t': {'h': 1, ' ': 1, 'e': 1}, 'h': {'i': 1, 'o': 1}, 'i': {'s': 2}, 's': {' ': 2, 'h': 1, 'e': 1}, ' ': {'i': 1, 'a': 1, 's': 2}, 'a': {' ': 1}, 'o': {'r': 1}, 'r': {'t': 1}, 'e': {'n': 2, ' ': 1}, 'n': {'t': 1, 'c': 1}, 'c': {'e': 1}})

太棒了!但我真的很想让那些 inner 词典也成为 defaultdicts。特别是,如果我 运行 self.counts['t']['h'],我会如预期的那样得到 1。 但是,defaultdict 的一个好处是如果密钥不可用,它会为您提供 0。目前,如果我 运行 self.counts['t']['x'] 我得到一个 keyerror,但我会通过让每个内部列表也成为 defaultdict 而得到 0

我假设这可以在以 out=self.counts 开头的代码块中的某处完成,但我有点不确定如何实现。

你应该改变这两行:

self.counts = { item : data.count(item) for item in data }
out[item] = self.counts

您不需要 out 变量。问题是您正在创建一个 "normal" 字典并将其分配给 self.count。只需使用:

self.counts[item].update({ item : data.count(item) for item in data })
  1. 使数据成为 collections.Counterdefaultdict 而不是列表(因为您完全不关心位的顺序,只关心它们的出现次数)
  2. 然后 update 你的 self.counts[item] 使用计数器而不是分配字典
  3. 您甚至可以直接进行更新:
    for item in ngrams:
        data = self.counts[item]
        for word in findall( item+".", text):
            data[word[-1]] += 1
    
    仅此而已,这会将相关计数直接更新到最初定义的 defaultdict

撇开大部分代码……不理想,或者很奇怪

似乎不​​必要地复杂

您正在获取 ngram 之后的每个代码点,为什么不直接提取 n+1 的伪 ngram 并将其拆分?类似于(未经测试,可能略有偏差):

for i in range(0, len(text)-n):
    ngram, follower = text[i:i+n], text[i+n]
    self.counts[ngram][follower] += 1

这也至少避免了代码的二次复杂性(以及各种常量复杂性),这是一个很好的副作用,但请注意,原始代码会隐式跳过 \n 的追随者(换行符) / 换行符)与 re.DOTALL. "matches any character except a newline" 一样。所以如果你想保持这种行为,你必须专门测试并跳过 follower == '\n'

重用成员变量作为局部变量?

出于某种奇怪的原因,您将 self.counts 重新用作局部变量,将其保存到 out,将其设置为奇怪的东西,然后在自行设置后重新加载它,为什么out 不是 内部 变量吗?

        for item in ngrams:
            data = []
            for word in findall( item+".", text):
                data.append(word[-1])
            out = { item : data.count(item) for item in data }
            self.counts[item] = out

这不是很有用(可能除了 printf 调试实用程序之外),您可以直接分配给 self.counts[item]

我也不知道 __starting_text__charas 有什么实用程序

双下划线前缀

不要。不管你用它们做什么,我有理由相信你错了(因为我很少遇到知道这些是做什么的人),你应该停止它。

如果您想向调用者暗示某物是对象的内部细节,请使用单个下划线前缀。尽管您可能也不需要这样做。

始终将编码传递给文本模式 open

说真的。 open(path) 在文本模式下工作(自动将原始磁盘数据解码为 str),但它选择的编码是 getdefaultencoding() returns成为垃圾。您不想用它来读取用户的文件,而且您绝对不会想用它来读取您自己的文件。明确提供 encoding='utf-8',这将避免很多麻烦。如果你需要推断编码也许使用chardet。

您的代码没有使用 defaultdict 的任何功能。如果您使用普通的 dict 代替,它的工作原理是相同的。这也非常令人困惑,因为您在代码的不同部分为不同的事物使用了多个变量名称(如 itemself.__counts)。

但是你可以很简单地改变它来使用 defaultdict 而不是用困难的方式做事。以下是我的修复方法:

def function(self, starting_text, charas):
    self.__starting_text = starting_text
    self.__charas = charas

    with open(self.__file) as file:
        text = file.read().lower().replace('\n', ' ')

    ngrams = set(text[i : i + self.__n] for i in range(len(text)))

    for item in ngrams:
        for word in findall( item+".", text):
            self.counts[item][word[-1]] += 1

这与您以前的代码不完全相同,因为它不是幂等的(如果您重复调用它,您将不断增加计数,而不是用新计数替换旧计数)。您可能可以通过首先将所有字符放入列表(如您的 data),然后仅在最后设置 self.__counts 中的值来恢复大部分旧行为。但是我可能更喜欢使用 collections.Counter 而不是手动完成(我可能会在循环中构建 Counter,而不是让 defaultdict 为我做) .

关于一个不相关的说明:您的代码在您的几个属性上使用了双前导下划线。 Python 代码通常不鼓励这样做。它启用名称修改,将 self.__n 之类的名称转换为 self._Class__n(如果 Class 是 class 的名称,则使用 __n 的代码被写入,无论self 是什么类型)。它通常不应该用于将某些东西标记为 "private",而是用于在您无法事先知道可能将其他名称放入同一对象名称空间时避免意外的名称冲突。例如,代理或混合器 class 可能需要允许用户访问任何类型的属性名称,而 class 的设计者不知道这些是什么(以及将被代理的对象或将与之混合的 classes 可能不知道 proxy/mixin class 的存在)。如果您只想将您的属性标记为 "private",请使用一个前导下划线而不是两个。 Python 中没有强制执行数据隐私,因此尝试使用名称修改只会误导您(想要访问您的属性的外部代码仍然能够这样做)。名称修改使调试更加困难。 Python 的哲学是它的程序员都是 "consenting adults",因此应该相信他们知道不会对其他代码的内部行为不当(或者如果他们这样做会处理后果)。