web-dev-qa-db-fra.com

Pourquoi un python dict.update () ne retourne pas l'objet?

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 lourd 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 l'enchaîner?

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

115
Paul Tarjan

Python implémente principalement une version teintée de façon pragmatique de séparation de la requête de commande : les mutateurs retournent None (avec des exceptions induites de manière pragmatique telles que pop ;-) donc ils ne peuvent pas éventuellement être confondu avec les accesseurs (et dans le même ordre d'idées, l'affectation n'est pas une expression, la séparation expression-expression est là, etc.).

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

Edit : btw, pas besoin, dans votre cas particulier, de créer a en cours de route, soit:

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 votre a.update(award_dict) (y compris, en cas de conflits, le fait que les entrées dans award_dict remplacent celles que vous donnez explicitement; pour obtenir les autres sémantiques , c'est-à-dire que pour que des entrées explicites "gagnent" de tels conflits, passez award_dict comme unique positional arg, avant les mots-clés, et sans le formulaire ** - dict(award_dict, name=name etc etc).

188
Alex Martelli

L'API de Python, par convention, distingue 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 ne renvoient aucun). Les procédures ont donc des effets secondaires, pas les fonctions. update est une procédure, donc il ne retourne pas de valeur.

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

bar = foo.reverse()

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

33
Martin v. Löwis
>>> 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}
13

C'est facile car:

(lambda d: d.update(dict2) or d)(d1)
8
Kostya Goloveshko

réputation insuffisante pour le commentaire laissé sur la réponse du haut

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

La solution avec **kwargs ne fonctionne que parce que le dictionnaire à fusionner n'a que clés de type chaîne.

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}
8
Stephan Scheller

Ce n'est pas que ce n'est pas acceptable, mais plutôt que dicts n'a pas été implémenté 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 de dict et ne remplacer que update pour effectuer la mise à jour et return self, si vous le voulez vraiment.

class myDict(dict):
    def update(self, *args):
        dict.update(self, *args)
        return self
5
Esteban Küber

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
2
Matus

Pour ceux qui arrivent en retard à la fête, j'avais mis en place un timing (Py 3.7), montrant que les méthodes basées sur .update() paraissent un peu (~ 5%) plus rapidement lorsque les entrées sont préservées et sensiblement (~ 30%) plus rapide 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)

Le timing des opérations sur place est un peu plus délicat, il devrait donc être modifié 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)
0
norok2

Je viens de l'essayer moi-même dans Python 3.4 (donc je n'ai pas pu utiliser la fantaisie {**dict_1, **dict_2} syntaxe).

Je voulais pouvoir avoir des clés non-chaîne dans les dictionnaires ainsi que fournir une quantité arbitraire de dictionnaires.

De plus, je voulais créer un nouveau dictionnaire, j'ai donc choisi de ne pas utiliser collections.ChainMap (un peu la raison pour laquelle je ne voulais pas utiliser dict.update initialement.

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}
0
freebie
import itertools
dict_merge = lambda *args: dict(itertools.chain(*[d.iteritems() for d in args]))
0
Matt