【问题标题】:Is there a clever way to pass the key to defaultdict's default_factory?有没有巧妙的方法将密钥传递给 defaultdict 的 default_factory?
【发布时间】:2011-02-24 03:06:26
【问题描述】:

一个类有一个带有一个参数的构造函数:

class C(object):
    def __init__(self, v):
        self.v = v
        ...

在代码中的某处,dict 中的值知道它们的键很有用。
我想使用带有传递给新生儿默认值的键的 defaultdict:

d = defaultdict(lambda : C(here_i_wish_the_key_to_be))

有什么建议吗?

【问题讨论】:

    标签: python dictionary defaultdict


    【解决方案1】:

    另一种可能实现所需功能的方法是使用装饰器

    def initializer(cls: type):
        def argument_wrapper(
            *args: Tuple[Any], **kwargs: Dict[str, Any]
        ) -> Callable[[], 'X']:
            def wrapper():
                return cls(*args, **kwargs)
    
            return wrapper
    
        return argument_wrapper
    
    
    @initializer
    class X:
        def __init__(self, *, some_key: int, foo: int = 10, bar: int = 20) -> None:
            self._some_key = some_key
            self._foo = foo
            self._bar = bar
    
        @property
        def key(self) -> int:
            return self._some_key
    
        @property
        def foo(self) -> int:
            return self._foo
    
        @property
        def bar(self) -> int:
            return self._bar
    
        def __str__(self) -> str:
            return f'[Key: {self.key}, Foo: {self.foo}, Bar: {self.bar}]'
    

    然后你可以像这样创建一个defaultdict

    >>> d = defaultdict(X(some_key=10, foo=15, bar=20))
    >>> d['baz']
    [Key: 10, Foo: 15, Bar: 20]
    >>> d['qux']
    [Key: 10, Foo: 15, Bar: 20]
    

    default_factory 将使用指定的 X 创建新实例 论据。

    当然,这只有在您知道该类将用于default_factory 时才有用。否则,为了实例化一个单独的类,您需要执行以下操作:

    x = X(some_key=10, foo=15)()
    

    这有点难看...但是,如果您想避免这种情况并引入一定程度的复杂性,您还可以在argument_wrapper 中添加一个关键字参数,如factory,这将允许通用行为:

    def initializer(cls: type):
        def argument_wrapper(
            *args: Tuple[Any], factory: bool = False, **kwargs: Dict[str, Any]
        ) -> Callable[[], 'X']:
            def wrapper():
                return cls(*args, **kwargs)
    
            if factory:
                return wrapper
            return cls(*args, **kwargs)
    
        return argument_wrapper
    

    然后您可以在哪里使用该类:

    >>> X(some_key=10, foo=15)
    [Key: 10, Foo: 15, Bar: 20]
    >>> d = defaultdict(X(some_key=15, foo=15, bar=25, factory=True))
    >>> d['baz']
    [Key: 15, Foo: 15, Bar: 25]
    

    【讨论】:

      【解决方案2】:

      这是一个自动添加值的字典的工作示例。在 /usr/include 中查找重复文件的演示任务。注意自定义字典PathDict只需要四行:

      class FullPaths:
      
          def __init__(self,filename):
              self.filename = filename
              self.paths = set()
      
          def record_path(self,path):
              self.paths.add(path)
      
      class PathDict(dict):
      
          def __missing__(self, key):
              ret = self[key] = FullPaths(key)
              return ret
      
      if __name__ == "__main__":
          pathdict = PathDict()
          for root, _, files in os.walk('/usr/include'):
              for f in files:
                  path = os.path.join(root,f)
                  pathdict[f].record_path(path)
          for fullpath in pathdict.values():
              if len(fullpath.paths) > 1:
                  print("{} located in {}".format(fullpath.filename,','.join(fullpath.paths)))
      

      【讨论】:

        【解决方案3】:

        不,没有。

        defaultdict 实现无法配置为将缺少的key 传递给开箱即用的default_factory。你唯一的选择是实现你自己的defaultdict 子类,正如上面@JochenRitzel 所建议的那样。

        但这并不像标准库解决方案那样“聪明”或几乎干净(如果存在的话)。 因此,您简洁的是/否问题的答案显然是“否”。

        标准库缺少如此常用的工具太可惜了。

        【讨论】:

        • 是的,让工厂拿钥匙(一元功能而不是零元)会是一个更好的设计选择。当我们想要返回一个常量时,很容易丢弃一个参数。
        【解决方案4】:

        它几乎不能称得上是 聪明 - 但是子类化是你的朋友:

        class keydefaultdict(defaultdict):
            def __missing__(self, key):
                if self.default_factory is None:
                    raise KeyError( key )
                else:
                    ret = self[key] = self.default_factory(key)
                    return ret
        
        d = keydefaultdict(C)
        d[x] # returns C(x)
        

        【讨论】:

        • 这正是我试图避免的丑陋......即使使用简单的 dict 并检查密钥是否存在也更加简洁。
        • @Paul:但这就是你的答案。丑陋?加油!
        • 我想我只是将那段代码放入我的个性化通用实用程序模块中,以便我可以随时使用它。那样也不会太丑……
        • +1 直接解决了 OP 的问题,对我来说并不“丑陋”。这也是一个很好的答案,因为许多人似乎没有意识到 defaultdict__missing__() 方法可以被覆盖(因为它可以在自 2.5 版以来内置 dict 类的任何子类中)。
        • +1 __missing__ 的全部目的是自定义丢失键的行为。 @silentghost 提到的 dict.setdefault() 方法也可以工作(从好的方面来说, setdefault() 很短并且已经存在;在消极方面,它存在效率问题,没有人真正喜欢“setdefault”这个名字) .
        【解决方案5】:

        我认为你根本不需要defaultdict。为什么不直接使用dict.setdefault 方法?

        >>> d = {}
        >>> d.setdefault('p', C('p')).v
        'p'
        

        这当然会创建许多C 的实例。如果这是一个问题,我认为更简单的方法可以:

        >>> d = {}
        >>> if 'e' not in d: d['e'] = C('e')
        

        据我所知,它会比defaultdict 或任何其他替代方案更快。

        ETA关于in 测试与使用 try-except 子句的速度:

        >>> def g():
            d = {}
            if 'a' in d:
                return d['a']
        
        
        >>> timeit.timeit(g)
        0.19638929363557622
        >>> def f():
            d = {}
            try:
                return d['a']
            except KeyError:
                return
        
        
        >>> timeit.timeit(f)
        0.6167065411074759
        >>> def k():
            d = {'a': 2}
            if 'a' in d:
                return d['a']
        
        
        >>> timeit.timeit(k)
        0.30074866358404506
        >>> def p():
            d = {'a': 2}
            try:
                return d['a']
            except KeyError:
                return
        
        
        >>> timeit.timeit(p)
        0.28588609450770264
        

        【讨论】:

        • 这在 d 被多次访问并且很少丢失键的情况下是非常浪费的:因此 C(key) 将创建大量不需要的对象供 GC 收集。此外,在我的情况下还有一个额外的痛苦,因为创建新的 C 对象很慢。
        • @Paul:没错。我会建议更简单的方法,请参阅我的编辑。
        • 我不确定它是否比 defaultdict 快,但这是我通常会做的事情(请参阅我对 THC4k 答案的评论)。我希望有一种简单的方法来破解 default_factory 不接受 args 的事实,以保持代码更优雅。
        • @SilentGhost:我不明白 - 这如何解决 OP 的问题?我认为 OP 希望任何尝试阅读 d[key] 以返回 d[key] = C(key) 如果 key not in d。但是你的解决方案需要他实际去提前预设d[key]?他怎么知道他需要哪个key
        • 因为 setdefault 丑得要命,集合中的 defaultdict 应该支持接收密钥的工厂函数。 Python 设计师浪费了多少机会!
        猜你喜欢
        • 1970-01-01
        • 2020-12-10
        • 1970-01-01
        • 1970-01-01
        • 2019-12-21
        • 1970-01-01
        • 1970-01-01
        • 2021-10-18
        • 2018-02-28
        相关资源
        最近更新 更多