【问题标题】:How to glob for iterable element如何为可迭代元素全局化
【发布时间】:2010-08-30 14:33:12
【问题描述】:

我有一个包含可迭代对象的 python 字典,其中一些是列表,但大多数是其他字典。我想做类似于以下的全局样式分配:

myiter['*']['*.txt']['name'] = 'Woot'

也就是说,对于 myiter 中的每个元素,查找所有键以 '.txt' 结尾的元素,然后将它们的 'name' 项设置为 'Woot'。

我考虑过对 dict 进行子类化并使用 fnmatch 模块。但是,我不清楚实现此目的的最佳方法是什么。

【问题讨论】:

    标签: python


    【解决方案1】:

    我认为最好的方法是这样做 -- '*' 是字典中完全有效的键,因此 myiter['*'] 具有完全明确定义的含义和有用性,和颠覆肯定会导致问题。如何对 字符串的键(包括列表而非映射的元素中的唯一整数“键”(索引))进行“glob”也是一个相当大的设计问题。

    如果您仍然必须这样做,我建议通过子类化abstract base class collections.MutableMapping 来完全控制,并实现所需的方法(__len____iter____getitem____setitem____delitem__,并且,为了获得更好的性能,还覆盖了诸如 __contains__ 之类的其他函数,ABC 确实在其他函数的基础上实施,但速度较慢)包含dict。相反,根据其他建议,子类化 dict 将需要您覆盖大量方法,以避免在您覆盖的方法和不覆盖的方法中使用“包含通配符的键”之间的不一致行为。

    无论你是collections.MutableMapping 还是dict 的子类,要创建你的Globbable 类,你都必须做出一个核心设计决策:yourthing[somekey] 是什么returnyourthingGlobbable?

    somekey 是一个包含通配符的字符串时,它可能必须返回不同的类型,而不是其他任何类型。在后一种情况下,人们会想象,那个入口处的实际情况是什么;但在前者中,它不能只返回另一个Globbable——否则,yourthing[somekey] = 'bah' 在一般情况下会做什么?对于您的单个“流畅语法”示例,您希望它在 yourthing 的每个 items 中设置一个 somekey 条目(与行为的 HUGE 语义中断宇宙中所有其他映射;-) -- 但是,您如何在yourthing 本身中设置条目?!

    让我们看看 Python 的禅宗对这种你渴望的“精巧语法”有什么要说的……:

    >>> import this
        ...
    If the implementation is hard to explain, it's a bad idea.
    If the implementation is easy to explain, it may be a good idea.
    

    考虑一下放弃“流畅语法”(以及它必然暗示的所有 巨大 语义头痛)的替代方案,以支持清晰和简单(此处使用 Python 2.7 及更好的语法) ,仅用于 dict 理解——如果您坚持使用 2.6 或更早版本,请使用显式的 dict(...) 调用),例如:

    def match(s, pat):
        try: return fnmatch.fnmatch(s, pat)
        except TypeError: return False
    
    def sel(ds, pat):
        return [d[k] for d in ds for k in d if match(k, pat)]
    
    def set(ds, k, v):
        for d in ds: d[k] = v
    

    所以你的任务可能会变成

    set(sel(sel([myiter], '*')), '*.txt'), 'name', 'Woot')
    

    (如果全部选择 '*' 是多余的,我只是省略了它)。这是否如此可怕到值得我上面提到的问题的泥潭来代替使用

    myiter['*']['*.txt']['name'] = 'Woot'
    

    ...?到目前为止,最清晰、性能最好的方法当然仍然是更简单的方法

    def match(k, v, pat):
        try:
          if fnmatch.fnmatch(k, pat):
            return isinstance(v, dict)
        except TypeError:
            return False
    
    for k, v in myiter.items():
      if match(k, v, '*'):
        for sk, sv in v.items():
          if match(sk, sv, '*.txt'):
            sv['name'] = 'Woot'
    

    但如果你绝对渴望简洁和紧凑,鄙视 Python 公案“稀疏胜于密集”的禅宗,你可以至少在没有我提到的各种噩梦的情况下获得它们,以实现你的理想的“语法糖”。

    【讨论】:

      【解决方案2】:

      最好的方法是继承 dict 并使用 fnmatch 模块。

      • subclass dict:以面向对象的方式添加您想要的功能。
      • fnmatch 模块:重用现有功能。

      【讨论】:

        【解决方案3】:

        您可以使用fnmatch 来匹配字典键的功能,尽管您必须稍微妥协语法,特别是如果您想在嵌套字典上执行此操作。也许具有返回通配符匹配的搜索方法的自定义类字典类会很好用。

        这是一个非常基本的示例,带有一个警告,即这不是递归的,并且不会处理嵌套字典:

        from fnmatch import fnmatch
        
        class GlobDict(dict):
            def glob(self, match):
                """@match should be a glob style pattern match (e.g. '*.txt')"""
                return dict([(k,v) for k,v  in self.items() if fnmatch(k, match)])
        
        # Start with a basic dict
        basic_dict = {'file1.jpg':'image', 'file2.txt':'text', 'file3.mpg':'movie',
                      'file4.txt':'text'}
        
        # Create a GlobDict from it
        glob_dict = GlobDict( **basic_dict )
        
        # Then get glob-styl results!
        globbed_results = glob_dict.glob('*.txt')
        # => {'file4.txt': 'text', 'file2.txt': 'text'}
        

        至于什么方式是最好的?最好的方法是行之有效的方法。甚至在创建解决方案之前,不要尝试对其进行优化!

        【讨论】:

          【解决方案4】:

          遵循最小魔法原则,或许只是定义一个递归函数,而不是继承dict

          import fnmatch
          
          def set_dict_with_pat(it,key_patterns,value):
              if len(key_patterns)>1:
                  for key in it:
                      if fnmatch.fnmatch(key,key_patterns[0]):
                          set_dict_with_pat(it[key],key_patterns[1:],value)
              else:
                  for key in it:
                      if fnmatch.fnmatch(key,key_patterns[0]):
                          it[key]=value
          

          可以这样使用:

          myiter=({'dir1':{'a.txt':{'name':'Roger'},'b.notxt':{'name':'Carl'}},'dir2':{'b.txt':{'name':'Sally'}}})
          set_dict_with_pat(myiter,['*','*.txt','name'],'Woot')
          print(myiter)
          # {'dir2': {'b.txt': {'name': 'Woot'}}, 'dir1': {'b.notxt': {'name': 'Carl'}, 'a.txt': {'name': 'Woot'}}}
          

          【讨论】:

          • 用 from fnmatch import fnmatch 替换 import fnmatch 将摆脱那些不方便的 fnmatch.fnmatches。
          • @Tony:感谢您的建议。然而,这个答案 (stackoverflow.com/questions/1744258/…) 让我相信import module 是比from module import function 更好的编码风格。
          猜你喜欢
          • 2021-11-14
          • 1970-01-01
          • 1970-01-01
          • 2018-07-13
          • 1970-01-01
          • 2021-11-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多