Python:如何递归地从 NESTED 数据结构(列表和字典)中删除 None 值?
Posted
技术标签:
【中文标题】Python:如何递归地从 NESTED 数据结构(列表和字典)中删除 None 值?【英文标题】:Python: How RECURSIVELY remove None values from a NESTED data structure (lists and dictionaries)? 【发布时间】:2013-12-31 18:16:13 【问题描述】:这是一些嵌套数据,包括列表、元组和字典:
data1 = ( 501, (None, 999), None, (None), 504 )
data2 = 1:601, 2:None, None:603, 'four':'sixty'
data3 = OrderedDict( [(None, 401), (12, 402), (13, None), (14, data2)] )
data = [ [None, 22, tuple([None]), (None,None), None], ( (None, 202), None:301, 32:302, 33:data1, data3 ) ]
目标:删除任何为 None 的键或值(从“数据”中)。如果一个列表或字典包含一个值,它本身就是一个列表、元组或字典,然后递归,以删除 NESTED Nones。
期望的输出:
[[22, (), ()], ((202,), 32: 302, 33: (501, (999,), 504), OrderedDict([(12, 402), (14, 'four': 'sixty', 1: 601)]))]
或者更易读,这里是格式化输出:
StripNones(data)= list:
. [22, (), ()]
. tuple:
. . (202,)
. . 32: 302, 33: (501, (999,), 504)
. . OrderedDict([(12, 402), (14, 'four': 'sixty', 1: 601)])
我会提出一个可能的答案,因为我还没有找到现有的解决方案。我很欣赏任何替代方案或指向预先存在的解决方案的指针。
编辑 我忘了提到这必须在 Python 2.7 中工作。我目前无法使用 Python 3。
虽然它IS值得为其他人发布 Python 3 解决方案。所以请指出您要回答的是哪个python。
【问题讨论】:
您需要处理哪些类型?没有完全通用的解决方案。如果像(None, None)
这样的东西作为键出现怎么办?
(1) 我现在已经发布了我的答案,这表明了我的想法。 (2) 我决定不弄乱键,除非它们直接是“无”。恕我直言,最好在输入字典时对密钥进行清理,而不是稍后。在将其放入字典之前,key = StripNones(key)
也会这样做,而不是在之后尝试修复它。
【参考方案1】:
如果您可以假设各个子类的__init__
方法与典型的基类具有相同的签名:
def remove_none(obj):
if isinstance(obj, (list, tuple, set)):
return type(obj)(remove_none(x) for x in obj if x is not None)
elif isinstance(obj, dict):
return type(obj)((remove_none(k), remove_none(v))
for k, v in obj.items() if k is not None and v is not None)
else:
return obj
from collections import OrderedDict
data1 = ( 501, (None, 999), None, (None), 504 )
data2 = 1:601, 2:None, None:603, 'four':'sixty'
data3 = OrderedDict( [(None, 401), (12, 402), (13, None), (14, data2)] )
data = [ [None, 22, tuple([None]), (None,None), None], ( (None, 202), None:301, 32:302, 33:data1, data3 ) ]
print remove_none(data)
请注意,这 不会 与 defaultdict
一起工作,例如,因为 defaultdict 接受了 __init__
的附加参数。要使其与defaultdict
一起工作,需要另一个特殊情况elif
(在常规字典之前)。
还请注意,我实际上已经构造了 new 对象。我没有修改旧的。如果您不需要支持修改像tuple
这样的不可变对象,则可以修改旧对象。
【讨论】:
终于得到你的答案了。type(obj)(..)
-- 非常好。 isinstance
具有多种类型,适用于那些可以使用相同理解的人——很高兴知道。
我已经接受了这个答案,因为我最喜欢这个答案。未来的读者还会查看@perreal 的答案,以及 mgilson 和我的 cmets。 perreal 回答中的一些逻辑使用可能有助于涵盖其他情况。
(而且,未来的读者,如果您希望修改现有的大型数据结构,而不是创建新副本,那么我最初尝试中的一些逻辑(远在下面)可能有用。具体来说,收集要删除的密钥和要更新的项目。)【参考方案2】:
def stripNone(data):
if isinstance(data, dict):
return k:stripNone(v) for k, v in data.items() if k is not None and v is not None
elif isinstance(data, list):
return [stripNone(item) for item in data if item is not None]
elif isinstance(data, tuple):
return tuple(stripNone(item) for item in data if item is not None)
elif isinstance(data, set):
return stripNone(item) for item in data if item is not None
else:
return data
样本运行:
print stripNone(data1)
print stripNone(data2)
print stripNone(data3)
print stripNone(data)
(501, (999,), 504)
'four': 'sixty', 1: 601
12: 402, 14: 'four': 'sixty', 1: 601
[[22, (), ()], ((202,), 32: 302, 33: (501, (999,), 504), 12: 402, 14: 'four': 'sixty', 1: 601)]
【讨论】:
这会删除零、空字符串和其他虚假的非无。 将elif data
更改为elif data is not None
@user2357112 已修复 :) 请立即检查。
@inspectorG4dget -- 将elif data:
更改为else
怎么样。 如果 data is None
没关系。无论如何,OP 的函数只会返回 None
。毕竟,它必须返回 something。也许更重要的是,我们期望stripNone(None)
会返回什么? (或者它应该引发异常?)_
如果data is not None
检查失败,你会从函数的末尾掉出并返回None
...即data
,所以你不妨使用else
.【参考方案3】:
这是我最初的尝试,在发布问题之前。 保留它,因为它可能有助于解释目标。
如果想要修改现有的 LARGE 集合,而不是将数据复制到新集合中,它还有一些有用的代码。 (其他答案创建新集合。)
# ---------- StripNones.py Python 2.7 ----------
import collections, copy
# Recursively remove None, from list/tuple elements, and dict key/values.
# NOTE: Changes type of iterable to list, except for strings and tuples.
# NOTE: We don't RECURSE KEYS.
# When "beImmutable=False", may modify "data".
# Result may have different collection types; similar to "filter()".
def StripNones(data, beImmutable=True):
t = type(data)
if issubclass(t, dict):
return _StripNones_FromDict(data, beImmutable)
elif issubclass(t, collections.Iterable):
if issubclass(t, basestring):
# Don't need to search a string for None.
return data
# NOTE: Changes type of iterable to list.
data = [StripNones(x, beImmutable) for x in data if x is not None]
if issubclass(t, tuple):
return tuple(data)
return data
# Modifies dict, removing items whose keys are in keysToRemove.
def RemoveKeys(dict, keysToRemove):
for key in keysToRemove:
dict.pop(key, None)
# Recursively remove None, from dict key/values.
# NOTE: We DON'T RECURSE KEYS.
# When "beImmutable=False", may modify "data".
def _StripNones_FromDict(data, beImmutable):
keysToRemove = []
newItems = []
for item in data.iteritems():
key = item[0]
if None in item:
# Either key or value is None.
keysToRemove.append( key )
else:
# The value might change when stripped.
oldValue = item[1]
newValue = StripNones(oldValue, beImmutable)
if newValue is not oldValue:
newItems.append( (key, newValue) )
somethingChanged = (len(keysToRemove) > 0) or (len(newItems) > 0)
if beImmutable and somethingChanged:
# Avoid modifying the original.
data = copy.copy(data)
if len(keysToRemove) > 0:
# if not beImmutable, MODIFYING ORIGINAL "data".
RemoveKeys(data, keysToRemove)
if len(newItems) > 0:
# if not beImmutable, MODIFYING ORIGINAL "data".
data.update( newItems )
return data
# ---------- TESTING ----------
# When run this file as a script (instead of importing it):
if (__name__ == "__main__"):
from collections import OrderedDict
maxWidth = 100
indentStr = '. '
def NewLineAndIndent(indent):
return '\n' + indentStr*indent
#print NewLineAndIndent(3)
# Returns list of strings.
def HeaderAndItems(value, indent=0):
if isinstance(value, basestring):
L = repr(value)
else:
if isinstance(value, dict):
L = [ repr(key) + ': ' + Repr(value[key], indent+1) for key in value ]
else:
L = [ Repr(x, indent+1) for x in value ]
header = type(value).__name__ + ':'
L.insert(0, header)
#print L
return L
def Repr(value, indent=0):
result = repr(value)
if (len(result) > maxWidth) and \
isinstance(value, collections.Iterable) and \
not isinstance(value, basestring):
L = HeaderAndItems(value, indent)
return NewLineAndIndent(indent + 1).join(L)
return result
#print Repr( [11, [221, 222], '331':331, '332': '3331':3331 , 44] )
def printV(name, value):
print( str(name) + "= " + Repr(value) )
print '\n\n\n'
data1 = ( 501, (None, 999), None, (None), 504 )
data2 = 1:601, 2:None, None:603, 'four':'sixty'
data3 = OrderedDict( [(None, 401), (12, 402), (13, None), (14, data2)] )
data = [ [None, 22, tuple([None]), (None,None), None], ( (None, 202), None:301, 32:302, 33:data1, data3 ) ]
printV( 'ORIGINAL data', data )
printV( 'StripNones(data)', StripNones(data) )
print '----- beImmutable = True -----'
#printV( 'data', data )
printV( 'data2', data2 )
#printV( 'data3', data3 )
print '----- beImmutable = False -----'
StripNones(data, False)
#printV( 'data', data )
printV( 'data2', data2 )
#printV( 'data3', data3 )
print
输出:
ORIGINAL data= list:
. [None, 22, (None,), (None, None), None]
. tuple:
. . (None, 202)
. . 32: 302, 33: (501, (None, 999), None, None, 504), None: 301
. . OrderedDict:
. . . None: 401
. . . 12: 402
. . . 13: None
. . . 14: 'four': 'sixty', 1: 601, 2: None, None: 603
StripNones(data)= list:
. [22, (), ()]
. tuple:
. . (202,)
. . 32: 302, 33: (501, (999,), 504)
. . OrderedDict([(12, 402), (14, 'four': 'sixty', 1: 601)])
----- beImmutable = True -----
data2= 'four': 'sixty', 1: 601, 2: None, None: 603
----- beImmutable = False -----
data2= 'four': 'sixty', 1: 601
要点:
if issubclass(t, basestring):
避免在字符串内部进行搜索,因为这没有任何意义,AFAIK。
if issubclass(t, tuple):
将结果转换回元组。
对于字典,使用copy.copy(data)
,返回与原始字典相同类型的对象。
限制:不尝试为以下类型保留集合/迭代器类型:list、tuple、dict(及其子类)。
如果需要更改,默认用法会复制数据结构。为False
传递beImmutable
可以在大量数据时产生更高的性能,但会改变原始数据,包括改变嵌套的数据片段——这可能会被代码中其他地方的变量引用。
【讨论】:
哇,这比其他人写的要长得多。使用issubclass
而不是isinstance
似乎有点奇怪。看看这个,有几个地方看起来可能不起作用。我会看看我是否可以构建一个失败的测试用例。
如果你想从字典中删除一个键,只需使用del d[k]
。 pop
用于您想要的值。
@user2357112 -- 有时我会使用pop(k, None)
,即使我不想要这个值,这样我就不需要自己处理KeyError
...
@user2357112 就像 mgilson 说的:我使用 pop
的原因是吞下任何不正确的键。这是好事还是坏事取决于情况。事实上,在这种情况下,只有有效的密钥被传入,所以我可以使用del
。也许这会更 Pythonic,允许 KeyError 传播给调用者,以防编码错误。
@ToolmakerSteve:那是因为您的测试用例不会触发newItems
更新。在Counter
的大多数用法中,您不会有包含None
的值,但这是该类旨在允许的用例。试试StripNones(collections.Counter(1: (None, None)))
。【参考方案4】:
def purify(o):
if hasattr(o, 'items'):
oo = type(o)()
for k in o:
if k != None and o[k] != None:
oo[k] = purify(o[k])
elif hasattr(o, '__iter__'):
oo = [ ]
for it in o:
if it != None:
oo.append(purify(it))
else: return o
return type(o)(oo)
print purify(data)
给予:
[[22, (), ()], ((202,), 32: 302, 33: (501, (999,), 504), OrderedDict([(12, 402), (14, 'four': 'sixty', 1: 601)]))]
【讨论】:
似乎你的__iter__
子句可以简化一点:oo = (purify(it) for it in o if it is not None)
(但我没有测试它,所以不能保证)。
另外,for it in iter(o)
在其中对iter
进行了完全无用的调用:P
@mgilson 我喜欢 perreal 如何使用 hasattr,而不是依赖显式命名类型。与您所做的相比,有什么缺点吗?总的来说,我更喜欢你(mgilson 的)解决方案的紧凑程度,但也许这也可以在这里完成?
而且,虽然这适用于 defaultdict
而我的不适用,但您不会保留 defaultdict 的 default_factory
属性,因此您不会真正保留原始...跨度>
@ToolmakerSteve -- 有一些我喜欢它的地方,也有一些我不喜欢的地方。它将适用于更多对象,但不能保证具有.items
属性的对象与字典类似。因此,它可能不适用于其他一些答案可以使用的某些对象。正如另一位评论者所说,对于完全任意的对象,这或多或少是不可能解决的问题。你需要画一条线,说你只会接受符合某些 API 的对象。【参考方案5】:
如果您想要一种功能齐全但简洁的方法来处理此类现实世界的嵌套数据结构,甚至处理循环,我建议您查看the remap utility from the boltons utility package。
在pip install boltons
或将iterutils.py 复制到您的项目之后,只需执行以下操作:
from collections import OrderedDict
from boltons.iterutils import remap
data1 = ( 501, (None, 999), None, (None), 504 )
data2 = 1:601, 2:None, None:603, 'four':'sixty'
data3 = OrderedDict( [(None, 401), (12, 402), (13, None), (14, data2)] )
data = [ [None, 22, tuple([None]), (None,None), None], ( (None, 202), None:301, 32:302, 33:data1, data3 ) ]
drop_none = lambda path, key, value: key is not None and value is not None
cleaned = remap(data, visit=drop_none)
print(cleaned)
# got:
[[22, (), ()], ((202,), 32: 302, 33: (501, (999,), 504), OrderedDict([(12, 402), (14, 'four': 'sixty', 1: 601)]))]
This page has many more examples,包括处理更大对象的对象(来自 Github 的 API)。
它是纯 Python,因此可以在任何地方使用,并且在 Python 2.7 和 3.3+ 中经过全面测试。最重要的是,我是专门为这样的情况编写的,所以如果你发现它无法处理的情况,你可以让我修复它right here。
【讨论】:
以上是关于Python:如何递归地从 NESTED 数据结构(列表和字典)中删除 None 值?的主要内容,如果未能解决你的问题,请参考以下文章