Pourquoi un python dict.update () ne renvoie-t-il pas l'objet?

139

J'essaye de faire:

award_dict = {
    "url" : "http://facebook.com",
    "imageurl" : "http://farm4.static.flickr.com/3431/3939267074_feb9eb19b1_o.png",
    "count" : 1,
}

def award(name, count, points, desc_string, my_size, parent) :
    if my_size > count :
        a = {
            "name" : name,
            "description" : desc_string % count,
            "points" : points,
            "parent_award" : parent,
        }
        a.update(award_dict)
        return self.add_award(a, siteAlias, alias).award

Mais si je me sentais vraiment encombrant dans la fonction, et j'aurais préféré le faire:

        return self.add_award({
            "name" : name,
            "description" : desc_string % count,
            "points" : points,
            "parent_award" : parent,
        }.update(award_dict), siteAlias, alias).award

Pourquoi la mise à jour ne renvoie-t-elle pas l'objet pour que vous puissiez enchaîner?

JQuery fait cela pour faire du chaînage. Pourquoi n'est-ce pas acceptable en python?

Paul Tarjan
la source
14
* TL; DRnewdict = dict(dict001, **dict002)
dreftymac
2
@dreftymac, cela ne fonctionne pas dans la compréhension cependant.
alancalvitti
@alancalvitti Oui, c'est en effet une mise en garde valable à souligner.
dreftymac

Réponses:

219

Python implémente principalement une saveur pragmatiquement teintée de séparation commande-requête : les mutateurs retournent None(avec des exceptions induites de manière pragmatique telles que pop;-) donc ils ne peuvent pas être confondus avec les accesseurs (et dans le même esprit, l'affectation n'est pas une expression, l'instruction -expression la séparation est là, et ainsi de suite).

Cela ne veut pas dire qu'il n'y a pas beaucoup de façons de fusionner les choses quand vous le voulez vraiment, par exemple, faire dict(a, **award_dict)un nouveau dict un peu comme celui que vous semblez souhaiter .updateretourner - alors pourquoi ne pas utiliser CELA si vous sentez vraiment que c'est important ?

Edit : au fait, pas besoin, dans votre cas particulier, de créer en acours de route, non plus:

dict(name=name, description=desc % count, points=points, parent_award=parent,
     **award_dict)

crée un dict unique avec exactement la même sémantique que le vôtre a.update(award_dict)(y compris, en cas de conflit, le fait que les entrées award_dictécrasent celles que vous donnez explicitement; pour obtenir l'autre sémantique, c'est-à-dire avoir des entrées explicites "gagnantes" de tels conflits, passer award_dictcomme seul argument de position , avant les mots-clés, et dépourvu de la **forme - dict(award_dict, name=nameetc etc).

Alex Martelli
la source
Eh bien, cela créera un autre dictionnaire après avoir dû créer un fichier. Je voulais créer un dict, puis ajouter un tas d'autres valeurs, puis le donner à une fonction.
Paul Tarjan
@Paul, et c'est exactement ce que vous faites - avec deux déclarations (beaucoup plus lisibles que la manière imbriquée que vous vouliez) qui pour vous "vous sentait vraiment encombrante". Éditer ma réponse pour montrer comment éviter de créer acomplètement, btw,
Alex Martelli
1
La solution originale n'est pas robuste. Si award_dict contient des clés déjà spécifiées, une SyntaxError sera lancée pour un argument de mot-clé répété. La solution dict de jamylak (itertools.chain (d1.iteritems (), .. d <n> .iteritems ())) fonctionne non seulement dans le cas où les dictionnaires ont des clés dupliquées, mais vous permet également de fusionner facilement plusieurs dictionnaires avec des dictionnaires plus tard dans la chaîne ayant préséance sur la valeur finale.
Matt
2
De plus, si les clés de award_dict ne sont pas des chaînes, l'interpréteur lancera unTypeError
kunl
3
dict(old_dict, old_key=new_value)ne lancera pas plusieurs valeurs pour le mot-clé et ne retournera pas de nouveau dict.
Charmy
35

L'API de Python, par convention, fait la distinction entre les procédures et les fonctions. Les fonctions calculent de nouvelles valeurs à partir de leurs paramètres (y compris tout objet cible); les procédures modifient les objets et ne renvoient rien (c'est-à-dire qu'elles renvoient None). Les procédures ont donc des effets secondaires, les fonctions n'en ont pas. update est une procédure, donc elle ne renvoie pas de valeur.

La motivation pour le faire de cette façon est que sinon, vous pourriez avoir des effets secondaires indésirables. Considérer

bar = foo.reverse()

Si reverse (qui inverse la liste sur place) renvoie également la liste, les utilisateurs peuvent penser que reverse renvoie une nouvelle liste qui est assignée à bar, et ne remarque jamais que foo est également modifiée. En rendant le retour inverse Aucun, ils reconnaissent immédiatement que la barre n'est pas le résultat du renversement, et regarderont de plus près quel est l'effet du retour.

Martin c.Löwis
la source
1
Je vous remercie. Pourquoi l'inverse ne donnerait-il pas également la possibilité de ne pas le faire sur place? Performance? faire reverse(foo)est bizarre.
Paul Tarjan
L'ajout d'une option serait inapproprié: cela changerait la nature de la méthode en fonction d'un paramètre. Cependant, les méthodes devraient vraiment avoir des types de retour fixes (il y a, malheureusement, des cas où cette règle est enfreinte). Il est facile de créer une copie inversée: il suffit de faire une copie (en utilisant bar=foo[:]), puis de rétablir la copie.
Martin v.Löwis
3
Je pense que la raison est explicite. Dans bar = foo.reverse(), vous pourriez penser que ce foon'est pas modifié. Pour éviter toute confusion, vous avez à la fois foo.reverse()et bar = reversed(foo).
Roberto Bonvallet
Quel est le problème avec la modification de la nature d'un paramètre en fonction d'un paramètre?
Julien
22

C'est simple car:

(lambda d: d.update(dict2) or d)(d1)
Kostya Golovechtko
la source
15
>>> dict_merge = lambda a,b: a.update(b) or a
>>> dict_merge({'a':1, 'b':3},{'c':5})
{'a': 1, 'c': 5, 'b': 3}

Notez qu'en plus de renvoyer le dict fusionné, il modifie le premier paramètre sur place. Donc dict_merge (a, b) modifiera a.

Ou, bien sûr, vous pouvez tout faire en ligne:

>>> (lambda a,b: a.update(b) or a)({'a':1, 'b':3},{'c':5})
{'a': 1, 'c': 5, 'b': 3}
Crispin Wellington
la source
10
-1 lambdane doit pas être utilisé comme ça, au lieu d' utiliser la fonction classique à la defplace
jamylak
8
Vous n'avez même pas besoin d'un lambda, utilisez simplementa.update(b) or a
Pycz
10

pas assez de réputation pour le commentaire laissé sur la première réponse

@beardc cela ne semble pas être une chose CPython. PyPy me donne "TypeError: les mots clés doivent être des chaînes"

La solution **kwargsne fonctionne que car le dictionnaire à fusionner ne contient que des clés de type string .

c'est à dire

>>> dict({1:2}, **{3:4})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: keyword arguments must be strings

contre

>>> dict({1:2}, **{'3':4})
{1: 2, '3': 4}
Stephan Scheller
la source
5

Ce n'est pas que ce n'est pas acceptable, mais cela dictsn'a pas été mis en œuvre de cette façon.

Si vous regardez l'ORM de Django, il utilise largement le chaînage. Ce n'est pas découragé, vous pouvez même hériter dictet ne remplacer que updatepour faire la mise à jour et return self, si vous le voulez vraiment.

class myDict(dict):
    def update(self, *args):
        dict.update(self, *args)
        return self
Esteban Küber
la source
Merci, cela pourrait patcher dict, je voulais juste savoir pourquoi dict () n'autorisait pas cette fonctionnalité elle-même (car c'est aussi simple que vous le démontrez). Est-ce que Django patch dict comme ça?
Paul Tarjan
2

aussi proche que possible de votre solution proposée

from collections import ChainMap

return self.add_award(ChainMap(award_dict, {
    "name" : name,
    "description" : desc_string % count,
    "points" : points,
    "parent_award" : parent,
}), siteAlias, alias).award
Matus
la source
1

Pour ceux qui arrivent en retard à la fête, j'avais mis un peu de temps ensemble (Py 3.7), montrant que les .update()méthodes basées semblent un peu (~ 5%) plus rapides lorsque les entrées sont conservées et sensiblement (~ 30%) plus rapides lors de la mise à jour sur place. .

Comme d'habitude, tous les repères doivent être pris avec un grain de sel.

def join2(dict1, dict2, inplace=False):
    result = dict1 if inplace else dict1.copy()
    result.update(dict2)
    return result


def join(*items):
    iter_items = iter(items)
    result = next(iter_items).copy()
    for item in iter_items:
        result.update(item)
    return result


def update_or(dict1, dict2):
    return dict1.update(dict2) or dict1


d1 = {i: str(i) for i in range(1000000)}
d2 = {str(i): i for i in range(1000000)}

%timeit join2(d1, d2)
# 258 ms ± 1.47 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit join(d1, d2)
# 262 ms ± 2.97 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit dict(d1, **d2)
# 267 ms ± 2.74 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit {**d1, **d2}
# 267 ms ± 1.84 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Les délais pour les opérations sur place sont un peu plus délicats, il faudrait donc le modifier le long d'une opération de copie supplémentaire (le premier timing est juste pour référence):

%timeit dd = d1.copy()
# 44.9 ms ± 495 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit dd = d1.copy(); join2(dd, d2)
# 296 ms ± 2.05 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit dd = d1.copy(); join2(dd, d2, True)
# 234 ms ± 1.02 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit dd = d1.copy(); update_or(dd, d2)
# 235 ms ± 1.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
norok2
la source
0
import itertools
dict_merge = lambda *args: dict(itertools.chain(*[d.iteritems() for d in args]))
Mat
la source
0

Je viens d'essayer cela moi-même dans Python 3.4 (je n'ai donc pas pu utiliser la {**dict_1, **dict_2}syntaxe sophistiquée ).

Je voulais pouvoir avoir des clés sans chaîne dans les dictionnaires et fournir une quantité arbitraire de dictionnaires.

De plus, je voulais créer un nouveau dictionnaire, alors j'ai choisi de ne pas l'utiliser collections.ChainMap(un peu la raison pour laquelle je ne voulais pas utiliser au dict.updatedépart.

Voici ce que j'ai fini par écrire:

def merge_dicts(*dicts):
    all_keys  = set(k for d in dicts for k in d.keys())
    chain_map = ChainMap(*reversed(dicts))
    return {k: chain_map[k] for k in all_keys}

merge_maps({'1': 1}, {'2': 2, '3': 3}, {'1': 4, '3': 5})
# {'1': 4, '3': 5, '2': 2}
billet de faveur
la source