web-dev-qa-db-fra.com

"décompresser" un dictionnaire passé dans l'espace de nommage de la fonction en Python?

Dans mon travail, j'ai souvent des paramètres que je dois regrouper en sous-ensembles pour des raisons pratiques:

d1 = {'x':1,'y':2}
d2 = {'a':3,'b':4}

Je le fais en passant dans plusieurs dictionnaires. La plupart du temps, j'utilise directement le dictionnaire adopté, à savoir:

def f(d1,d2):
    for k in d1:
        blah( d1[k] )

Dans certaines fonctions, je dois accéder directement aux variables et les choses deviennent encombrantes. Je veux vraiment ces variables dans l'espace de noms local. Je veux être capable de faire quelque chose comme:

def f(d1,d2)
    locals().update(d1)
    blah(x)
    blah(y)    

mais les mises à jour du dictionnaire renvoyées par locals () ne sont pas garanties pour mettre à jour réellement l'espace de noms.

Voici la manière manuelle évidente:

def f(d1,d2):
    x,y,a,b = d1['x'],d1['y'],d2['a'],d2['b']
    blah(x)
    return {'x':x,'y':y}, {'a':a,'b':b}

Il en résulte trois répétitions de la liste de paramètres par fonction. Ceci peut être automatisé avec un décorateur:

def unpack_and_repack(f):
    def f_new(d1, d2):
        x,y,a,b = f(d1['x'],d1['y'],d2['a'],d3['b'])
        return {'x':x,'y':y}, {'a':a,'b':b}
    return f_new
@unpack
def f(x,y,a,b):
    blah(x)
    blah(y)
    return x,y,a,b

Il en résulte trois répétitions pour le décorateur, plus deux par fonction, il est donc préférable que vous ayez beaucoup de fonctions.

Y a-t-il un meilleur moyen? Peut-être que quelque chose utilisant eval? Merci!

27
Andrew Wagner

Vous pouvez toujours passer un dictionnaire en tant qu'argument d'une fonction. Par exemple, 

dict = {'a':1, 'b':2}
def myFunc(a=0, b=0, c=0):
    print(a,b,c)
myFunc(**dict)
28
Bear

Si vous aimez la syntaxe d.variable meilleure que la d['variable'], vous pouvez envelopper le dictionnaire dans un objet "tas" presque trivial comme ceci:

class Bunch:
    def __init__(self, **kw):
        self.__dict__.update(kw)

Il n'apporte pas exactement le contenu du dictionnaire dans l'espace de noms local, mais se rapproche si vous utilisez des noms abrégés pour les objets. 

10
Jouni K. Seppänen

En supposant que toutes les clés de votre dictionnaire soient qualifiées d'identificateurs, vous pouvez le faire simplement:

adict = { 'x' : 'I am x', 'y' : ' I am y' }
for key in  adict.keys():
  exec(key + " = adict['" + key + "']")
blah(x)
blah(y)
7
Thava

Ceci est similaire à votre idée de décorateur, mais elle est un peu plus générale, car elle vous permet de passer un nombre arbitraire de dicts à foo, et le décorateur ne doit rien savoir des clés dans les dicts ou de l'ordre des commandes. arguments lors de l’appel de la fonction foo sous-jacente.

#!/usr/bin/env python
d1 = {'x':1,'y':2}
d2 = {'a':3,'b':4}

def unpack_dicts(f):
    def f_new(*dicts):
        new_dict={}
        for d in dicts:
            new_dict.update(d)
        return f(**new_dict)
    return f_new

@unpack_dicts
def foo(x,y,a,b):
    print x,y,a,b

foo(d1,d2)
# 1 2 3 4
4
unutbu

Voici ce que j'utilise comme solution de contournement locals().update(d1):

def f(d1,d2)
    exec ','.join(d1) + ', = d1.values()'
    blah(x)
    blah(y)
4
Thomas Holder

Je pense que la sagesse courante est de "ne pas utiliser le module inspect dans le code de production", et je suis pour la plupart d'accord avec cela. En tant que tel, je pense que c'est une mauvaise idée de procéder comme suit dans le code de production. Mais si vous travaillez sur un python qui supporte les frames (comme CPython), ceci devrait fonctionne:

>>> def framelocals():
...    return inspect.currentframe(1).f_locals
... 
>>> def foo(ns):
...    framelocals().update(ns)
...    print locals()
... 
>>> foo({'bar': 17})
{'ns': {'bar': 17}, 'bar': 17}

Il extrait simplement la dict réelle du cadre de l'appelant, qui, lorsqu'elle est appelée à l'intérieur d'un corps de fonction, devrait être l'espace de noms de la fonction. Je ne sais pas s'il y a ou non une situation d'utilisation de CPython lorsque locals() ne le fait pas de toute façon; l'avertissement dans la documentation pourrait être de dire "les effets de la modification de la dict renvoyée par locals() dépendent de l'implémentation python". Ainsi, bien que cela fonctionne pour modifier cette dict dans CPython, cela ne peut pas être dans une autre implémentation. 

UPDATE: Cette méthode ne fonctionne pas réellement. 

>>> def flup(ns):
...    framelocals().update(ns)
...    print locals()
...    print bar
... 
>>> flup({'bar': 17})
{'ns': {'bar': 17}, 'bar': 17}
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in flup
NameError: global name 'bar' is not defined

Comme suggéré par ddaa , la compilation de fonctions contient une magie plus profonde qui prend en compte les variables locales. Ainsi, vous pouvez mettre à jour la dict, mais vous ne pouvez pas voir la mise à jour avec une recherche d'espace de noms local normale.

2
Matt Anderson

Je ne pense pas que vous puissiez obtenir plus de commodité pour décompresser des dict en Python. Donc, voici la réponse obligatoire "si ça fait mal, ne faites pas ça".

L'accès aux éléments de mappage IS est plus fastidieux que l'accès aux attributs en Python. Vous devriez donc peut-être passer des instances de classes définies par l'utilisateur au lieu de dict.

2
ddaa

J'ai écrit un paquet Python appelé var_arguments pour regrouper et dissocier des arguments qui devraient être utiles ici; il est disponible sur github .

Au lieu d'écrire, dites:

def f(d1,d2):
    x,y,a,b = d1['x'],d1['y'],d2['a'],d2['b']
    y=x+a
    return {'x':x,'y':y}, {'a':a,'b':b}

Vous pouvez écrire:

from var_arguments import recon_dict, use_dargs

def f(d1,d2):
  r=f2(dargs=[d1,d2])
  return recon_dict(d1,r), recon_dict(d2,r)

@use_dargs
def f2(x,y,a,b):
  y=x+a
  return locals()

J'ai écrit la solution comme celle-ci pour correspondre à ce que vous semblez aller: les dictionnaires arrivent et partent en groupes et nous minimisons le nombre de fois où nous mentionnons les noms des clés dans les dictionnaires et/ou y accédons manuellement. Plus précisément, il suffit de mentionner x, y, a et b une fois de cette façon.

En gros, cela fonctionne comme suit: @use_dargs modifie f2 pour qu’il accepte un argument facultatif pour le mot-clé dargs, qui, le cas échéant, devrait fournir une liste de dictionnaires (dargs = [d1, d2]). Les paires clé/valeur De ces dictionnaires sont ajoutées aux arguments de mot clé qui sont par ailleurs fournis à l'appel de la fonction, les arguments de mot clé ayant la priorité la plus élevée, d2 en deuxième position et la priorité la plus basse. En conséquence, vous pouvez appeler f2 de différentes manières et obtenir le même résultat:

f2(1,2,3,4)
f2(1,a=3,dargs=[dict(y=2,b=4)])
f2(dargs=[dict(x=1,y=2),dict(a=3,b=4)])

recon_dict s’applique lorsque vous avez un dictionnaire qui contient les anciennes valeurs pour toutes les clés qui vous intéressent, et un autre dictionnaire qui contient les valeurs new pour toutes ces clés (et éventuellement d’autres tu ne veux pas) Par exemple:

old_d=dict(a=8,b=9) # I want these keys, but they're old values
xyab=dict(x=1,y=2,a=3,b=4) # These are the new values, but I don't want all of them
new_d=recon_dict(old_d,xyab)
assert new_d==dict(a=3,b=4)

Voici quelques astuces supplémentaires pour supprimer la redondance de la mention répétée de noms de variables dans un corps de fonction géré par var_arguments . Nous pouvons tout d'abord changer:

{'x':x,'y':y}

dans:

ddict('x,y',locals())

De même, nous pouvons changer:

f(x=x,y=y)

dans:

dcall(f,'x,y',locals())

Plus généralement, si nous avons un dictionnaire xy avec les clés x et y et si nos variables locales incluent a et b, nous pouvons changer:

f(x=xy['x'],y=xy['y'],a=a,b=b)

dans:

ldcall(f,'x,y,a,b',[locals(),xy])
0
Marc Coram

Vous pouvez utiliser la sorcellerie :

from sorcery import unpack_dict

x, y = unpack_dict(d1)
0
Alex Hall