我有一个基本上相当于嵌套字典的数据结构。让我们说它看起来像这样:
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
现在,保持和创造这个是非常痛苦的;每当我有一个新的州/县/专业时,我必须通过令人讨厌的try / catch块创建下层词典。而且,如果我想要遍历所有值,我必须创建恼人的嵌套迭代器。
我也可以使用元组作为键,例如:
{('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81,
('new jersey', 'middlesex county', 'programmers'): 81,
('new jersey', 'middlesex county', 'salesmen'): 62,
('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
这使得迭代值非常简单和自然,但是在做聚类和查看字典的子集等事情上会更加语法上的痛苦(例如,如果我只想逐个状态)。
基本上,有时我想将嵌套字典视为平面字典,有时我想将其视为复杂的层次结构。我可以把它全部包装在一个类中,但似乎有人可能已经完成了这个。或者,似乎可能有一些非常优雅的语法结构来做到这一点。
我怎么能做得更好?
附录:我知道setdefault()
但它并没有真正实现干净的语法。此外,您创建的每个子词典仍需要手动设置setdefault()
。
答案 0 :(得分:187)
class AutoVivification(dict):
"""Implementation of perl's autovivification feature."""
def __getitem__(self, item):
try:
return dict.__getitem__(self, item)
except KeyError:
value = self[item] = type(self)()
return value
测试:
a = AutoVivification()
a[1][2][3] = 4
a[1][3][3] = 5
a[1][2]['test'] = 6
print a
输出:
{1: {2: {'test': 6, 3: 4}, 3: {3: 5}}}
答案 1 :(得分:154)
在Python中实现嵌套字典的最佳方法是什么?
在__missing__
子类上实现dict
以设置并返回新实例。
自Python 2.5以来,这种方法已经可用(and documented),并且(对我来说特别有价值)它非常像普通字典打印,而不是自动生成的defaultdict的丑陋打印:
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)() # retain local pointer to value
return value # faster to return than dict lookup
(注意self[key]
位于赋值的左侧,因此这里没有递归。)
并说你有一些数据:
data = {('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81,
('new jersey', 'middlesex county', 'programmers'): 81,
('new jersey', 'middlesex county', 'salesmen'): 62,
('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
这是我们的使用代码:
vividict = Vividict()
for (state, county, occupation), number in data.items():
vividict[state][county][occupation] = number
现在:
>>> import pprint
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
对这种容器的批评是,如果用户拼错了密钥,我们的代码可能会无声地失败:
>>> vividict['new york']['queens counyt']
{}
另外,现在我们的数据中有一个拼写错误的县:
>>> pprint.pprint(vividict, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36},
'queens counyt': {}}}
我们只是在访问密钥但缺少密钥时提供另一个类Vividict
的嵌套实例。 (返回值赋值很有用,因为它避免了我们在dict上另外调用getter,不幸的是,我们无法在设置时返回它。)
请注意,这些语义与最受欢迎的答案相同,但代码行的一半 - nosklo的实现:
class AutoVivification(dict): """Implementation of perl's autovivification feature.""" def __getitem__(self, item): try: return dict.__getitem__(self, item) except KeyError: value = self[item] = type(self)() return value
下面只是一个示例,说明如何轻松地使用此dict来动态创建嵌套的dict结构。这可以像您想要的那样快速创建分层树结构。
import pprint
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
d = Vividict()
d['foo']['bar']
d['foo']['baz']
d['fizz']['buzz']
d['primary']['secondary']['tertiary']['quaternary']
pprint.pprint(d)
哪个输出:
{'fizz': {'buzz': {}},
'foo': {'bar': {}, 'baz': {}},
'primary': {'secondary': {'tertiary': {'quaternary': {}}}}}
正如最后一行所示,它非常漂亮,并且可以进行手动检查。但是,如果您想直观地检查数据,实现__missing__
以将其类的新实例设置为键并返回它是一个更好的解决方案。
dict.setdefault
虽然提问者认为这不干净,但我觉得它比Vividict
本人更好。
d = {} # or dict()
for (state, county, occupation), number in data.items():
d.setdefault(state, {}).setdefault(county, {})[occupation] = number
现在:
>>> pprint.pprint(d, width=40)
{'new jersey': {'mercer county': {'plumbers': 3,
'programmers': 81},
'middlesex county': {'programmers': 81,
'salesmen': 62}},
'new york': {'queens county': {'plumbers': 9,
'salesmen': 36}}}
错误拼写会失败,并且不会使我们的数据混乱,而且信息不好:
>>> d['new york']['queens counyt']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'queens counyt'
此外,我认为setdefault在循环中使用时效果很好而你不知道你将获得什么密钥,但重复使用变得相当繁琐,我认为没有人会想要跟上以下内容:
d = dict()
d.setdefault('foo', {}).setdefault('bar', {})
d.setdefault('foo', {}).setdefault('baz', {})
d.setdefault('fizz', {}).setdefault('buzz', {})
d.setdefault('primary', {}).setdefault('secondary', {}).setdefault('tertiary', {}).setdefault('quaternary', {})
另一个批评是setdefault需要一个新的实例,无论它是否被使用。但是,Python(或者至少是CPython)在处理未使用和未引用的新实例方面相当聪明,例如,它重用了内存中的位置:
>>> id({}), id({}), id({})
(523575344, 523575344, 523575344)
这是一个整洁的实现,在您不检查数据的脚本中使用与实现__missing__
一样有用:
from collections import defaultdict
def vivdict():
return defaultdict(vivdict)
但是,如果您需要检查数据,那么以相同方式填充数据的自动生效的defaultdict的结果如下所示:
>>> d = vivdict(); d['foo']['bar']; d['foo']['baz']; d['fizz']['buzz']; d['primary']['secondary']['tertiary']['quaternary']; import pprint;
>>> pprint.pprint(d)
defaultdict(<function vivdict at 0x17B01870>, {'foo': defaultdict(<function vivdict
at 0x17B01870>, {'baz': defaultdict(<function vivdict at 0x17B01870>, {}), 'bar':
defaultdict(<function vivdict at 0x17B01870>, {})}), 'primary': defaultdict(<function
vivdict at 0x17B01870>, {'secondary': defaultdict(<function vivdict at 0x17B01870>,
{'tertiary': defaultdict(<function vivdict at 0x17B01870>, {'quaternary': defaultdict(
<function vivdict at 0x17B01870>, {})})})}), 'fizz': defaultdict(<function vivdict at
0x17B01870>, {'buzz': defaultdict(<function vivdict at 0x17B01870>, {})})})
此输出非常不优雅,结果非常难以理解。通常给出的解决方案是递归地转换回dict以进行手动检查。这个非平凡的解决方案留给读者练习。
最后,让我们来看看性能。我减去了实例化的成本。
>>> import timeit
>>> min(timeit.repeat(lambda: {}.setdefault('foo', {}))) - min(timeit.repeat(lambda: {}))
0.13612580299377441
>>> min(timeit.repeat(lambda: vivdict()['foo'])) - min(timeit.repeat(lambda: vivdict()))
0.2936999797821045
>>> min(timeit.repeat(lambda: Vividict()['foo'])) - min(timeit.repeat(lambda: Vividict()))
0.5354437828063965
>>> min(timeit.repeat(lambda: AutoVivification()['foo'])) - min(timeit.repeat(lambda: AutoVivification()))
2.138362169265747
根据效果,dict.setdefault
效果最佳。在你关心执行速度的情况下,我强烈推荐它用于生产代码。
如果你需要这个用于交互式使用(也许在IPython笔记本中),那么性能并不重要 - 在这种情况下,我会选择Vividict来获取输出的可读性。与AutoVivification对象(使用__getitem__
而不是__missing__
相比,它是为此目的而制作的)它远远优于。
在子类__missing__
上实现dict
以设置和返回新实例比替代方案稍微困难但具有以下好处
并且因为它比修改__getitem__
更简单,更高效,所以应该优先选择该方法。
然而,它有缺点:
因此,我个人更喜欢setdefault
到其他解决方案,并且在我需要这种行为的每种情况下都有。
答案 2 :(得分:29)
仅仅因为我没有看到这么小的一个,这里有一个根据你的喜好嵌套的字典,没有汗水:
# yo dawg, i heard you liked dicts
def yodict():
return defaultdict(yodict)
答案 3 :(得分:22)
您可以使用PyYaml创建YAML文件并将其读取。
第1步:创建一个YAML文件,“employment.yml”:
new jersey:
mercer county:
pumbers: 3
programmers: 81
middlesex county:
salesmen: 62
programmers: 81
new york:
queens county:
plumbers: 9
salesmen: 36
第2步:在Python中阅读
import yaml
file_handle = open("employment.yml")
my_shnazzy_dictionary = yaml.safe_load(file_handle)
file_handle.close()
现在my_shnazzy_dictionary
拥有您的所有价值观。如果您需要动态执行此操作,则可以将YAML创建为字符串并将其提供给yaml.safe_load(...)
。
答案 4 :(得分:17)
由于您有星型模式设计,您可能希望将其更像是一个关系表而不像字典。
import collections
class Jobs( object ):
def __init__( self, state, county, title, count ):
self.state= state
self.count= county
self.title= title
self.count= count
facts = [
Jobs( 'new jersey', 'mercer county', 'plumbers', 3 ),
...
def groupBy( facts, name ):
total= collections.defaultdict( int )
for f in facts:
key= getattr( f, name )
total[key] += f.count
在没有SQL开销的情况下,创建类似数据仓库的设计可以做很多事情。
答案 5 :(得分:13)
如果嵌套级别的数量很少,我会使用collections.defaultdict
:
from collections import defaultdict
def nested_dict_factory():
return defaultdict(int)
def nested_dict_factory2():
return defaultdict(nested_dict_factory)
db = defaultdict(nested_dict_factory2)
db['new jersey']['mercer county']['plumbers'] = 3
db['new jersey']['mercer county']['programmers'] = 81
像这样使用defaultdict
可以避免很多混乱的setdefault()
,get()
等。
答案 6 :(得分:9)
这是一个返回任意深度的嵌套字典的函数:
from collections import defaultdict
def make_dict():
return defaultdict(make_dict)
像这样使用:
d=defaultdict(make_dict)
d["food"]["meat"]="beef"
d["food"]["veggie"]="corn"
d["food"]["sweets"]="ice cream"
d["animal"]["pet"]["dog"]="collie"
d["animal"]["pet"]["cat"]="tabby"
d["animal"]["farm animal"]="chicken"
用这样的东西迭代一切:
def iter_all(d,depth=1):
for k,v in d.iteritems():
print "-"*depth,k
if type(v) is defaultdict:
iter_all(v,depth+1)
else:
print "-"*(depth+1),v
iter_all(d)
打印出来:
- food
-- sweets
--- ice cream
-- meat
--- beef
-- veggie
--- corn
- animal
-- pet
--- dog
---- labrador
--- cat
---- tabby
-- farm animal
--- chicken
您最终可能希望将新项目添加到dict中。将所有defaultdict
递归转换为普通dict
s很容易。
def dictify(d):
for k,v in d.iteritems():
if isinstance(v,defaultdict):
d[k] = dictify(v)
return dict(d)
答案 7 :(得分:7)
我发现setdefault
非常有用;它检查是否存在密钥,如果不存在则将其添加:
d = {}
d.setdefault('new jersey', {}).setdefault('mercer county', {})['plumbers'] = 3
setdefault
始终会返回相关密钥,因此您实际上正在更新“d
”的值。
当谈到迭代时,我确信你可以很容易地编写一个生成器,如果在Python中不存在它:
def iterateStates(d):
# Let's count up the total number of "plumbers" / "dentists" / etc.
# across all counties and states
job_totals = {}
# I guess this is the annoying nested stuff you were talking about?
for (state, counties) in d.iteritems():
for (county, jobs) in counties.iteritems():
for (job, num) in jobs.iteritems():
# If job isn't already in job_totals, default it to zero
job_totals[job] = job_totals.get(job, 0) + num
# Now return an iterator of (job, number) tuples
return job_totals.iteritems()
# Display all jobs
for (job, num) in iterateStates(d):
print "There are %d %s in total" % (job, num)
答案 8 :(得分:6)
正如其他人所说,关系数据库可能对您更有用。您可以使用内存中的sqlite3数据库作为数据结构来创建表,然后查询它们。
import sqlite3
c = sqlite3.Connection(':memory:')
c.execute('CREATE TABLE jobs (state, county, title, count)')
c.executemany('insert into jobs values (?, ?, ?, ?)', [
('New Jersey', 'Mercer County', 'Programmers', 81),
('New Jersey', 'Mercer County', 'Plumbers', 3),
('New Jersey', 'Middlesex County', 'Programmers', 81),
('New Jersey', 'Middlesex County', 'Salesmen', 62),
('New York', 'Queens County', 'Salesmen', 36),
('New York', 'Queens County', 'Plumbers', 9),
])
# some example queries
print list(c.execute('SELECT * FROM jobs WHERE county = "Queens County"'))
print list(c.execute('SELECT SUM(count) FROM jobs WHERE title = "Programmers"'))
这只是一个简单的例子。您可以为州,县和职称定义单独的表。
答案 9 :(得分:5)
defaultdict()
是你的朋友!
对于二维字典,您可以这样做:
d = defaultdict(defaultdict)
d[1][2] = 3
如需更多尺寸,您可以:
d = defaultdict(lambda :defaultdict(defaultdict))
d[1][2][3] = 4
答案 10 :(得分:5)
collections.defaultdict
可以进行细分,以制作嵌套字典。然后将任何有用的迭代方法添加到该类中。
>>> from collections import defaultdict
>>> class nesteddict(defaultdict):
def __init__(self):
defaultdict.__init__(self, nesteddict)
def walk(self):
for key, value in self.iteritems():
if isinstance(value, nesteddict):
for tup in value.walk():
yield (key,) + tup
else:
yield key, value
>>> nd = nesteddict()
>>> nd['new jersey']['mercer county']['plumbers'] = 3
>>> nd['new jersey']['mercer county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['programmers'] = 81
>>> nd['new jersey']['middlesex county']['salesmen'] = 62
>>> nd['new york']['queens county']['plumbers'] = 9
>>> nd['new york']['queens county']['salesmen'] = 36
>>> for tup in nd.walk():
print tup
('new jersey', 'mercer county', 'programmers', 81)
('new jersey', 'mercer county', 'plumbers', 3)
('new jersey', 'middlesex county', 'programmers', 81)
('new jersey', 'middlesex county', 'salesmen', 62)
('new york', 'queens county', 'salesmen', 36)
('new york', 'queens county', 'plumbers', 9)
答案 11 :(得分:4)
至于“讨厌的尝试/捕获块”:
d = {}
d.setdefault('key',{}).setdefault('inner key',{})['inner inner key'] = 'value'
print d
产量
{'key': {'inner key': {'inner inner key': 'value'}}}
您可以使用此功能将平面字典格式转换为结构化格式:
fd = {('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81,
('new jersey', 'middlesex county', 'programmers'): 81,
('new jersey', 'middlesex county', 'salesmen'): 62,
('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
for (k1,k2,k3), v in fd.iteritems():
d.setdefault(k1, {}).setdefault(k2, {})[k3] = v
答案 12 :(得分:3)
为了便于迭代嵌套字典,为什么不写一个简单的生成器呢?
def each_job(my_dict):
for state, a in my_dict.items():
for county, b in a.items():
for job, value in b.items():
yield {
'state' : state,
'county' : county,
'job' : job,
'value' : value
}
那么,如果你有一个compilicated嵌套字典,迭代它变得简单:
for r in each_job(my_dict):
print "There are %d %s in %s, %s" % (r['value'], r['job'], r['county'], r['state'])
显然,您的生成器可以产生对您有用的任何格式的数据。
为什么使用try catch块来读取树?在尝试检索密钥之前,查询密钥是否存在是否容易(并且可能更安全)。使用guard子句的函数可能如下所示:
if not my_dict.has_key('new jersey'):
return False
nj_dict = my_dict['new jersey']
...
或者,一种可能有点冗长的方法是使用get方法:
value = my_dict.get('new jersey', {}).get('middlesex county', {}).get('salesmen', 0)
但是对于一种更为简洁的方式,您可能希望使用collections.defaultdict,它是自python 2.5以来标准库的一部分。
import collections
def state_struct(): return collections.defaultdict(county_struct)
def county_struct(): return collections.defaultdict(job_struct)
def job_struct(): return 0
my_dict = collections.defaultdict(state_struct)
print my_dict['new jersey']['middlesex county']['salesmen']
我在这里假设您的数据结构的含义,但应该很容易调整您真正想要做的事情。
答案 13 :(得分:3)
您可以使用Addict:https://github.com/mewwts/addict
>>> from addict import Dict
>>> my_new_shiny_dict = Dict()
>>> my_new_shiny_dict.a.b.c.d.e = 2
>>> my_new_shiny_dict
{'a': {'b': {'c': {'d': {'e': 2}}}}}
答案 14 :(得分:2)
我喜欢将它包装在一个类中并实现__getitem__
和__setitem__
以便它们实现一个简单的查询语言的想法:
>>> d['new jersey/mercer county/plumbers'] = 3
>>> d['new jersey/mercer county/programmers'] = 81
>>> d['new jersey/mercer county/programmers']
81
>>> d['new jersey/mercer country']
<view which implicitly adds 'new jersey/mercer county' to queries/mutations>
如果你想获得幻想,你也可以实现类似的东西:
>>> d['*/*/programmers']
<view which would contain 'programmers' entries>
但大多数情况下我认为这样的事情真的很有趣:D
答案 15 :(得分:1)
除非您的数据集保持相当小,否则您可能需要考虑使用关系数据库。它将完全符合您的要求:可以轻松添加计数,选择计数子集,甚至按州,县,职业或这些的任意组合计算总计数。
答案 16 :(得分:1)
class JobDb(object):
def __init__(self):
self.data = []
self.all = set()
self.free = []
self.index1 = {}
self.index2 = {}
self.index3 = {}
def _indices(self,(key1,key2,key3)):
indices = self.all.copy()
wild = False
for index,key in ((self.index1,key1),(self.index2,key2),
(self.index3,key3)):
if key is not None:
indices &= index.setdefault(key,set())
else:
wild = True
return indices, wild
def __getitem__(self,key):
indices, wild = self._indices(key)
if wild:
return dict(self.data[i] for i in indices)
else:
values = [self.data[i][-1] for i in indices]
if values:
return values[0]
def __setitem__(self,key,value):
indices, wild = self._indices(key)
if indices:
for i in indices:
self.data[i] = key,value
elif wild:
raise KeyError(k)
else:
if self.free:
index = self.free.pop(0)
self.data[index] = key,value
else:
index = len(self.data)
self.data.append((key,value))
self.all.add(index)
self.index1.setdefault(key[0],set()).add(index)
self.index2.setdefault(key[1],set()).add(index)
self.index3.setdefault(key[2],set()).add(index)
def __delitem__(self,key):
indices,wild = self._indices(key)
if not indices:
raise KeyError
self.index1[key[0]] -= indices
self.index2[key[1]] -= indices
self.index3[key[2]] -= indices
self.all -= indices
for i in indices:
self.data[i] = None
self.free.extend(indices)
def __len__(self):
return len(self.all)
def __iter__(self):
for key,value in self.data:
yield key
示例:
>>> db = JobDb()
>>> db['new jersey', 'mercer county', 'plumbers'] = 3
>>> db['new jersey', 'mercer county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'programmers'] = 81
>>> db['new jersey', 'middlesex county', 'salesmen'] = 62
>>> db['new york', 'queens county', 'plumbers'] = 9
>>> db['new york', 'queens county', 'salesmen'] = 36
>>> db['new york', None, None]
{('new york', 'queens county', 'plumbers'): 9,
('new york', 'queens county', 'salesmen'): 36}
>>> db[None, None, 'plumbers']
{('new jersey', 'mercer county', 'plumbers'): 3,
('new york', 'queens county', 'plumbers'): 9}
>>> db['new jersey', 'mercer county', None]
{('new jersey', 'mercer county', 'plumbers'): 3,
('new jersey', 'mercer county', 'programmers'): 81}
>>> db['new jersey', 'middlesex county', 'programmers']
81
>>>
编辑现在使用通配符(None
)查询时返回词典,否则返回单个值。
答案 17 :(得分:1)
您可以在lambdas和defaultdict中使用递归,无需定义名称:
a = defaultdict((lambda f: f(f))(lambda g: lambda:defaultdict(g(g))))
以下是一个例子:
>>> a['new jersey']['mercer county']['plumbers']=3
>>> a['new jersey']['middlesex county']['programmers']=81
>>> a['new jersey']['mercer county']['programmers']=81
>>> a['new jersey']['middlesex county']['salesmen']=62
>>> a
defaultdict(<function __main__.<lambda>>,
{'new jersey': defaultdict(<function __main__.<lambda>>,
{'mercer county': defaultdict(<function __main__.<lambda>>,
{'plumbers': 3, 'programmers': 81}),
'middlesex county': defaultdict(<function __main__.<lambda>>,
{'programmers': 81, 'salesmen': 62})})})
答案 18 :(得分:1)
我曾经使用过这个功能。它安全,快速,易于维护。
def deep_get(dictionary, keys, default=None):
return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
示例:
>>> from functools import reduce
>>> def deep_get(dictionary, keys, default=None):
... return reduce(lambda d, key: d.get(key, default) if isinstance(d, dict) else default, keys.split("."), dictionary)
...
>>> person = {'person':{'name':{'first':'John'}}}
>>> print (deep_get(person, "person.name.first"))
John
>>> print (deep_get(person, "person.name.lastname"))
None
>>> print (deep_get(person, "person.name.lastname", default="No lastname"))
No lastname
>>>
答案 19 :(得分:0)
我有类似的事情要发生。我有很多案例可以做:
thedict = {}
for item in ('foo', 'bar', 'baz'):
mydict = thedict.get(item, {})
mydict = get_value_for(item)
thedict[item] = mydict
但是深入了解多层次。它是“.get(item,{})”这是关键,因为如果没有一个字典,它将创建另一个字典。与此同时,我一直在考虑如何应对 这个更好。现在,有很多
value = mydict.get('foo', {}).get('bar', {}).get('baz', 0)
相反,我做了:
def dictgetter(thedict, default, *args):
totalargs = len(args)
for i,arg in enumerate(args):
if i+1 == totalargs:
thedict = thedict.get(arg, default)
else:
thedict = thedict.get(arg, {})
return thedict
如果你这样做会产生同样的效果:
value = dictgetter(mydict, 0, 'foo', 'bar', 'baz')
更好?我想是的。
答案 20 :(得分:0)
对于以下内容(从上面复制),有一种方法可以实现append函数。我正在尝试使用嵌套字典将值存储为数组。
class Vividict(dict):
def __missing__(self, key):
value = self[key] = type(self)() # retain local pointer to value
return value
我当前的实现如下:
totalGeneHash=Vividict()
for keys in GenHash:
for second in GenHash[keys]:
if keys in sampleHash:
total_val = GenHash[keys][second]
totalGeneHash[gene][keys].append(total_val)
This is the error I get: AttributeError: 'Vividict' object has no attribute 'append'