【问题标题】:Nesting Defaultdictionaries in Python3在 Python3 中嵌套默认字典
【发布时间】:2020-02-18 06:45:06
【问题描述】:

我正在编写一段代码,这是我第一次从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 字典也成为默认字典。特别是,如果我运行self.counts['t']['h'],我得到1,正如预期的那样。 但是,defaultdict 的一个好处是如果密钥不可用,它会为您提供0。目前,如果我运行self.counts['t']['x'],我会得到一个keyerror,但我会得到0,而不是让每个内部列表也成为一个defaultdict。

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

【问题讨论】:

  • 您应该提供一个最小的可重现示例,这不是:stackoverflow.com/help/minimal-reproducible-example
  • 我不明白你在做什么。在您的 Class 构造函数中 - 请想一个更好的名称 - 您将 self.counts 初始化为默认字典,其每个值也将是默认字典。这就是你说你想做的(内部字典是默认字典),而且它似乎已经完成了。但是在功能上——请想一个更好的名字——你将其他一些对象分配给相同的 self.counts。这些分配之一实际上是在一个循环内。这几乎肯定不是你想要的。

标签: python python-3.x dictionary defaultdict


【解决方案1】:
  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.“匹配除换行符以外的任何字符”。因此,如果您想保持这种行为,您必须专门测试并跳过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-debugging 实用程序),您可以直接分配给self.counts[item]

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

双下划线前缀

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

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

总是将编码传递给文本模式open

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

【讨论】:

    【解决方案2】:

    你应该改变这两行:

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

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

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

    【讨论】:

    • 您可能甚至不需要将项目具体化为 dict 并且可以使用元组生成器,尽管我猜 count 的开销无论如何都会使这种次要效率相形见绌。
    【解决方案3】:

    您的代码没有使用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 是写入使用__n 的代码的类的名称,无论self 是什么类型)。它通常不应该用于将某些东西标记为“私有”,而是为了避免在您无法提前知道其他名称可能放入同一对象命名空间时发生意外的名称冲突。例如,代理或 mixin 类可能需要允许用户访问任何类型的属性名称,而类的设计者不知道这些将是什么(以及将被代理的对象或类的设计者将与可能不知道代理/mixin 类的存在混合在一起)。如果您只想将您的属性标记为“私有”,请使用单个前导下划线而不是两个。 Python 中没有强制执行数据隐私,并且尝试使用名称修改只会误导您(想要访问您的属性的外部代码仍然可以这样做)。名称修改使调试变得更加困难。 Python 的哲学是它的程序员都是“同意的成年人”,因此应该相信他们知道不会与其他代码的内部行为不当(或者如果他们这样做的话,他们会处理后果)。

    【讨论】:

      猜你喜欢
      • 2018-09-12
      • 2018-08-02
      • 1970-01-01
      • 1970-01-01
      • 2016-01-30
      • 2017-02-16
      • 2020-03-13
      • 1970-01-01
      • 2011-06-29
      相关资源
      最近更新 更多