如何对可迭代元素使用glob
我有一个Python字典,这个字典里面包含了一些可迭代的东西,有的是列表,但大部分是其他字典。我想做一个类似于下面这种的操作:
myiter['*']['*.txt']['name'] = 'Woot'
也就是说,对于我这个可迭代对象中的每一个元素,查找所有键名以'.txt'结尾的元素,然后把它们的'name'项设置为'Woot'。
我考虑过创建一个字典的子类,并使用fnmatch模块。但是,我不太清楚实现这个目标的最佳方法是什么。
4 个回答
你可以使用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'}
至于哪种方法是最好的?最好的方法就是能解决问题的方法。在你还没找到解决方案之前,不要急着去优化它!
最好的方法是创建一个字典的子类,并使用fnmatch模块。
- 创建字典的子类:以面向对象的方式添加你想要的功能。
- fnmatch模块:可以重复使用已有的功能。
我认为最好的办法就是不要这样做——'*'
在字典中是一个完全有效的键,所以myiter['*']
是有明确意义和用处的,改变这个用法肯定会引发问题。而且,如何在键不是字符串的情况下“通配”键,包括那些只用整数作为“键”(索引)的列表元素,这也是一个设计上的难题。
如果你非得这样做,我建议你通过子类化抽象基类 collections.MutableMapping
来完全控制,并实现所需的方法(__len__
, __iter__
, __getitem__
, __setitem__
, __delitem__
,为了更好的性能,还可以重写其他方法,比如__contains__
,因为这个基类是基于其他方法实现的,但速度较慢),这些都可以基于一个包含的dict
来实现。相较于其他建议,直接子类化dict
会需要你重写很多方法,以避免在你重写的方法和未重写的方法中使用“包含通配符的键”时出现不一致的行为。
无论你是子类化collections.MutableMapping
还是dict
来创建你的Globbable
类,你都必须做一个核心设计决定:当yourthing
是一个Globbable
时,yourthing[somekey]
会返回什么?
显然,当somekey
是一个包含通配符的字符串时,它必须返回不同的类型,而不是其他任何东西。在后者的情况下,可以想象返回的就是那个条目的实际内容;但在前者的情况下,它不能仅仅返回另一个Globbable
——否则,yourthing[somekey] = 'bah'
在一般情况下会做什么呢?对于你单个的“简洁语法”示例,你希望它在yourthing
的每个项中设置一个somekey
条目(这与宇宙中其他所有映射的行为有着巨大的语义差异;-)——但那样的话,你又怎么能在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及更高版本的语法,仅仅是为了字典推导——如果你使用的是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哲学中的“稀疏优于密集”这一说法,你可以至少在不需要经历我提到的各种噩梦的情况下,获得它们,以实现你理想中的“语法糖”。