web-dev-qa-db-fra.com

Quelle est la méthode Pythonic pour effectuer la transformation suivante sur une liste de dict?

J'ai une liste de dicts comme ceci:

l = [{'name': 'foo', 'values': [1,2,3,4]}, {'name': 'bar', 'values': [5,6,7,8]}]

et je voudrais obtenir une sortie de cette forme:

>>> [('foo', 'bar'), ([1,2,3,4], [5,6,7,8])]

Mais à part de for- looping et appending je ne vois pas de solution. Y a-t-il un moyen plus intelligent que de faire cela?

names = []
values = []
for d in l:
    names.append(d['name'])
    values.append(d['values'])
28
oarfish

Utiliser l'expression du générateur:

l = [{'name': 'foo', 'values': [1,2,3,4]}, {'name': 'bar', 'values': [5,6,7,8]}]
v = [Tuple(k["name"] for k in l), Tuple(k["values"] for k in l)]
print(v)

Sortie:

[('foo', 'bar'), ([1, 2, 3, 4], [5, 6, 7, 8])]
29
eyllanesc

J'utiliserais une liste de compréhension (un peu comme celle de eyllanesc) si j'écrivais ce code pour la consommation publique. Mais juste pour le plaisir, voici un one-liner qui n’utilise pas de fors.

>>> l = [{'name': 'foo', 'values': [1,2,3,4]}, {'name': 'bar', 'values': [5,6,7,8]}]
>>> list(Zip(*map(dict.values, l)))
[('foo', 'bar'), ([1, 2, 3, 4], [5, 6, 7, 8])]

(Notez que cela ne fonctionne de manière fiable que si les dictionnaires préservent l'ordre d'insertion, ce qui n'est pas le cas dans toutes les versions de Python. CPython 3.6 le fait en tant que détail d'implémentation, mais son comportement n'est garanti qu'à partir de la version 3.7.)

Répartition rapide du processus:

  • dict.values ​​renvoie un objet dict_values, qui est une variable contenant toutes les valeurs de dict.
  • map prend chaque dictionnaire dans l et y appelle dict.values, ce qui renvoie un objet itérable d'objets dict_values.
  • Zip(*thing) est une recette classique de "transposition", qui prend un itérable d’itérables et l’inverse en diagonale. Par exemple. [[a, b], [c, d]] devient [[a, c], [b, d]]. Cela met tous les noms dans un tuple et toutes les valeurs dans un autre.
  • list convertit l'objet Zip en une liste.
24
Kevin

Vous pouvez utiliser operator.itemgetter to garantie classement des valeurs:

from operator import itemgetter

fields = ('name', 'values')
res = list(Zip(*map(itemgetter(*fields), L)))

print(res)

[('foo', 'bar'), ([1, 2, 3, 4], [5, 6, 7, 8])]

Si, en supposant Python 3.6+, vous ne pouvez pas garantir un ordre d'insertion approprié des dictionnaires dans votre liste d'entrées, vous devrez définir explicitement un ordre comme ci-dessus.

Performance

Bien qu'une liste de "compréhensions de tuples" fonctionne, elle devient illisible et inefficace lorsque vous interrogez plusieurs champs:

from operator import itemgetter

n = 10**6
L = [{'name': 'foo', 'values': [1,2,3,4], 'name2': 'Zoo', 'name3': 'xyz',
      'name4': 'def'}, {'name': 'bar', 'values': [5,6,7,8], 'name2': 'bart',
      'name3': 'abc', 'name4': 'ghi'}] * n

%timeit [Tuple(k["name"] for k in L), Tuple(k["values"] for k in L),\
         Tuple(k["name2"] for k in L), Tuple(k["name3"] for k in L),
         Tuple(k["name4"] for k in L)]

%timeit fields = ('name', 'values', 'name2', 'name3' ,'name4');\
        list(Zip(*map(itemgetter(*fields), L)))

1 loop, best of 3: 1.25 s per loop
1 loop, best of 3: 1.04 s per loop
10
jpp

Ce n'est peut-être pas exactement ce que vous aviez à l'esprit, mais pour des données tabulaires comme celle-ci, je trouve que pandas est généralement la meilleure solution à long terme:

>>> import pandas as pd
>>> l = [{'name': 'foo', 'values': [1,2,3,4]}, {'name': 'bar', 'values': [5,6,7,8]}]
>>> df = pd.DataFrame(l)
  name        values
0  foo  [1, 2, 3, 4]
1  bar  [5, 6, 7, 8]

Généralement, vous utilisez le bloc de données directement pour tout ce que vous devez faire, mais vous pouvez également le convertir en une structure de données basée sur des listes:

>>> df['name'].tolist(), df['values'].tolist()
(['foo', 'bar'], [[1, 2, 3, 4], [5, 6, 7, 8]]) 
5
Kale Kundert

Pas sûr des performances, mais voici une autre prise utilisant Zip() et décompactant:

list(Zip(*[Tuple(i.values()) for i in l]))

# [('foo', 'bar'), ([1, 2, 3, 4], [5, 6, 7, 8])]

Edit: Comme @DeepSpace l’a souligné, il peut être réduit à:

list(Zip(*(i.values() for i in l)))

Voici une réponse plus longue mais plus explicite si vous souhaitez définir vous-même les commandes:

list(Zip(*(Tuple(map(lambda k: i.get(k), ('name', 'values'))) for i in l)))

# [('foo', 'bar'), ([1, 2, 3, 4], [5, 6, 7, 8])]
4
Idlehands

utiliser la carte pour cela

names = Tuple(map(lambda d: d['name'], l))
values = Tuple(map(lambda d: d['values'], l))
result = [names, values]
3
user3142459

Voici une façon récursive de le faire:

def trans(l):
  if l:
    res = trans(l[1:])
    res[0], res[1] = (l[0]['name'],) + res[0], (l[0]['values'],) + res[1]
    return res
  return [(),()]
0
greenBox

Premièrement: votre code est correct, lisible et efficace, ce qui me semble Pythonic ..__ Notez que vous ne voulez probablement pas une liste de n-uplets, cependant. Les tuples sont immuables , vous ne pourriez donc pas ajouter un autre nom à names.

Avec un seul dict

Si names est unique, vous pouvez convertir votre liste de dict en un grand dict:

>>> l = [{'name': 'foo', 'values': [1,2,3,4]}, {'name': 'bar', 'values': [5,6,7,8]}]
>>> data = {d['name']:d['values'] for d in l}
>>> data
{'foo': [1, 2, 3, 4], 'bar': [5, 6, 7, 8]}

Vous pouvez obtenir directement les informations souhaitées:

>>> data.keys()
dict_keys(['foo', 'bar'])
>>> data.values()
dict_values([[1, 2, 3, 4], [5, 6, 7, 8]])

Si vous voulez vraiment une liste de listes:

>>> [list(data.keys()), list(data.values())]
[['foo', 'bar'], [[1, 2, 3, 4], [5, 6, 7, 8]]]

Avec des pandas

Si vous travaillez avec une longue liste de dict, tenez compte de pandas .

Vous pouvez initialiser une DataFrame directement:

>>> import pandas as pd
>>> df = pd.DataFrame([{'name': 'foo', 'values': [1,2,3,4]}, {'name': 'bar', 'values': [5,6,7,8]}])
>>> df
  name        values
0  foo  [1, 2, 3, 4]
1  bar  [5, 6, 7, 8]

Si vous avez besoin des noms en tant qu’itération, vous pouvez obtenir la colonne correspondante:

>>> df['name']
0    foo
1    bar
Name: name, dtype: object

Si vous avez vraiment besoin d'une liste de noms:

>>> list(df['name'])
['foo', 'bar']

Pour obtenir les noms et les valeurs ensemble:

>>> df.values.T
array([['foo', 'bar'],
       [list([1, 2, 3, 4]), list([5, 6, 7, 8])]], dtype=object)
0
Eric Duminil