在字典和列表相互嵌套的结构体中搜索指定元素
一、问题概述
在一个python列表中,要找到一个指定的元素非常容易,用index函数即可;在一个字典中找到元素也很容易,遍历即可。以上这些任务之所以容易,一个很重要的原因是这些结构都很简单,而且是固定的,因此只需以一个固定的公式遍历各元素即可。例如,从一个有10个元素的列表x中寻找某个元素,只要用一个for循环,遍历x[0],...,x[9],一个一个和要查找的元素比较。
然而,现实中,元素的结构未必简单,可能会层层嵌套,列里嵌套字典,字典嵌套列,且嵌套的层数还不一定一样。此时寻找的过程会比较困难。例如,现在需要从以下结构体中寻找元素"fee"。
dd = {"er":{"qq":"fee"}, "wq":'fee', "gg":[], "ge":['fee'], "no":{}}
有的"fee"是字典的某个键对应的值,有的"fee"是字典里的某个键对应的列表里的一个元素,有的甚至是字典里的字典里的值。显然,这不是一个简单、固定的结构。
二、解决方式
(一)递归的概念
其实对于这样的结构,遍历也未必不可能,只是不能用常规的方式。这种嵌套式结构,最好的遍历方式是递归(recursion)。思考一下,要寻找一个字典里的元素,可以遍历字典里的所有键值;要寻找一个列表里的元素,可以遍历列表所有的索引(0 ~ 列表长度-1)。那么,如果一个元素还是字典或列表,而不是我们要查找的元素类型(如字符串),怎么办呢?其实也有办法,就是在这个非基本元素上,再运行一次遍历程序。遍历程序里调用遍历程序,函数调用自身,这就叫递归(recursion)。
遍历一个字典,遍历一个列表,或直接处理一个基本元素(字符串)的程序都应写入遍历函数,使用哪一个,用if语句根据输入的对象自动选择。对于基本元素,运行遍历函数后,函数内部可以直接给结果,不需要再递归调用;但如果是字典或列表,则应对每一个元素调用遍历函数(这些元素可能是可遍历的,也可能不可遍历,函数对这两种情况都能处理)。
那么这个函数大致就这么写。
def get_all_elements_containing(obj, element):if type(obj) == dict:for k in obj.keys():get_all_elements_containing(obj[k], element)elif type(obj) == list:for ii in range(len(obj)):get_all_elements_containing(obj[ii], element)elif type(obj) == str:if element == obj:print("found")
运行这个函数,每次找到指定元素,都会输出一次“found”。对于我给的例子,会出现三次“found”。但仅仅输出找到的次数,并不能满足大部分用户的需要。很多时候,我们希望直到这些元素在该结构体中的位置。以文章一开始出现的结构体dd为例,我们希望能输出类似
dd['er']['qq']
这样的结果,告诉用户用这个表达式可以得到“fee”。
(二)叠加的过程
显然,我们要先找到dd['er'],然后从这里递归时再找到['qq']。也就是说,这两部分是通过上一层的递归和下一层的递归叠加而成的。要实现这个效果,应该把上层递归找到的结果通过传递到下层,最终找到时,才能将其叠加。注意:每一层传到下层的内容,应该是上层传来的内容和本层产生的内容的叠加。
所以可以这样写:
def get_all_elements_containing(obj, element, route_current):if type(obj) == dict:for k in obj.keys():get_all_elements_containing(obj[k], element, route_current + '[\'%s\']'%k)elif type(obj) == list:for ii in range(len(obj)):get_all_elements_containing(obj[ii], element, route_current + '[%d]'%ii)elif type(obj) == str:if element == obj:print(route_current)
这个函数可以这样调用:
dd = {"er":{"qq":"fee"}, "wq":'fee', "gg":[], "ge":['fee'], "no":{}}
get_all_elements_containing(dd, "fee", 'dd')
结果:
dd['er']['qq']
dd['wq']
dd['ge'][0]
把所有的能找到“fee”的表达式都找出来了。
(三)结果的统计
刚才已经找到了所有的表达式。但我们一般都希望这个结果不止显示在屏幕上,而是存在内存里供接下来的程序使用。换句话说,我们希望程序能统计递归过程中总共找到了哪几个元素是“fee”。关于这一点,可以每次找到元素时,将其存进函数以外的变量里。这个可以用“引用传递”实现。在python中,通常函数的参数输入是列表或字典时,会进行引用传递,也就是说函数内部运行时,可以直接影响调用的参数对象。
def get_all_elements_containing(obj, element, route_current, list_to_add):if type(obj) == dict:for k in obj.keys():get_all_elements_containing(obj[k], element, route_current + '[\'%s\']'%k, list_to_add)elif type(obj) == list:for ii in range(len(obj)):get_all_elements_containing(obj[ii], element, route_current + '[%d]'%ii, list_to_add)elif type(obj) == str:if element == obj:list_to_add.append(route_current)dd = {"er":{"qq":"fee"}, "wq":'fee', "gg":[], "ge":['fee'], "no":{}}
listele = []
get_all_elements_containing(dd, "fee", 'dd', listele)
print(listele)
在这个函数中,listele作为一个外部变量,被函数get_all_elements_containing调用时,这个外部变量里的内容也发生了变化(在函数外,全局环境里)。所以,最终显示的listele包含了所有的能找到“fee”的表达式。
["dd['er']['qq']", "dd['wq']", "dd['ge'][0]"]