Warum gibt ein Python dict.update () das Objekt nicht zurück?


139

Ich versuche zu tun:

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

Aber wenn ich mich in der Funktion wirklich umständlich gefühlt hätte und ich es lieber getan hätte:

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

Warum gibt update das Objekt nicht zurück, damit Sie es verketten können?

JQuery führt dies aus, um eine Verkettung durchzuführen. Warum ist es in Python nicht akzeptabel?


14
* TL; DRnewdict = dict(dict001, **dict002)
dreftymac

2
@dreftymac, das funktioniert allerdings nicht im Verständnis.
Alancalvitti

@alancalvitti Ja, das ist in der Tat eine gültige Einschränkung.
Dreftymac

Antworten:


218

Python implementiert meistens eine pragmatisch gefärbte Variante der Befehl-Abfrage-Trennung : Mutatoren kehren zurück None(mit pragmatisch induzierten Ausnahmen wiepop ;-), sodass sie unmöglich mit Accessoren verwechselt werden können (und in diesem Sinne ist die Zuweisung kein Ausdruck, die Aussage -Expressionstrennung ist da und so weiter).

Das bedeutet nicht, dass es nicht viele Möglichkeiten gibt, Dinge zusammenzuführen, wenn Sie wirklich wollen, z. B. dict(a, **award_dict)ein neues Diktat zu erstellen , das dem ähnelt, das Sie scheinbar zurückgeben möchten. .updateWarum also nicht DAS verwenden, wenn Sie es wirklich für wichtig halten? ?

Bearbeiten : Übrigens müssen Sie in Ihrem speziellen Fall auch aunterwegs nichts erstellen :

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

erstellt ein einzelnes Diktat mit genau der gleichen Semantik wie Ihre a.update(award_dict)(einschließlich im Falle von Konflikten die Tatsache, dass Einträge award_dictdie von Ihnen explizit angegebenen überschreiben; um die andere Semantik zu erhalten, dh um explizite Einträge zu haben, die solche Konflikte "gewinnen", passieren award_dictals einziges Positions arg, vor dem Schlüsselwort eines und beraubte der **Form - dict(award_dict, name=nameetc etc).


Nun, das wird ein weiteres Wörterbuch erstellen, nachdem ich ein erstellen musste. Ich wollte ein Diktat erstellen, dann eine Reihe anderer Werte hinzufügen und es dann einer Funktion geben.
Paul Tarjan

@Paul, und genau das tun Sie - mit zwei Aussagen (viel lesbarer als die verschachtelte Art, die Sie wollten), die sich für Sie "wirklich umständlich anfühlten". Bearbeiten meiner Antwort, um zu zeigen, wie man das Erstellen ainsgesamt vermeidet , übrigens
Alex Martelli

1
Die ursprüngliche Lösung ist nicht robust. Wenn honor_dict bereits angegebene Schlüssel enthält, wird ein SyntaxError für ein wiederholtes Schlüsselwortargument ausgelöst. Das Lösungsdiktat von jamylak (itertools.chain (d1.iteritems (), .. d <n> .iteritems ())) funktioniert nicht nur, wenn Wörterbücher doppelte Schlüssel haben, sondern ermöglicht es Ihnen auch, später problemlos mehrere Wörterbücher mit Diktaten zusammenzuführen Die Kette hat Vorrang vor dem Endwert.
Matt

2
Wenn die Schlüssel in grant_dict keine Zeichenfolge sind, wirft der Interpreter TypeError
Uhr

3
dict(old_dict, old_key=new_value)wirft nicht mehrere Werte für das Schlüsselwort und gibt ein neues Diktat zurück.
Charmy

35

Die Python-API unterscheidet konventionell zwischen Prozeduren und Funktionen. Funktionen berechnen neue Werte aus ihren Parametern (einschließlich aller Zielobjekte). Prozeduren ändern Objekte und geben nichts zurück (dh sie geben None zurück). Prozeduren haben also Nebenwirkungen, Funktionen nicht. update ist eine Prozedur, daher wird kein Wert zurückgegeben.

Die Motivation dafür ist, dass Sie sonst unerwünschte Nebenwirkungen bekommen können. Erwägen

bar = foo.reverse()

Wenn reverse (wodurch die Liste an Ort und Stelle umgekehrt wird) auch die Liste zurückgeben würde, könnten Benutzer denken, dass reverse eine neue Liste zurückgibt, die bar zugewiesen wird, und niemals bemerken, dass foo auch geändert wird. Wenn Sie Reverse Return None ausführen, erkennen sie sofort, dass der Balken nicht das Ergebnis der Umkehrung ist, und sehen genauer nach, wie sich Reverse auswirkt.


1
Danke dir. Warum sollte Reverse nicht auch die Option geben, dies nicht an Ort und Stelle zu tun? Performance? tun reverse(foo)fühlt sich komisch an.
Paul Tarjan

Das Hinzufügen einer Option wäre unangemessen: Es würde die Art der Methode abhängig von einem Parameter ändern. Methoden sollten jedoch wirklich feste Rückgabetypen haben (es gibt leider Fälle, in denen diese Regel verletzt wird). Es ist einfach, eine zurückgesetzte Kopie zu erstellen: Erstellen Sie einfach eine Kopie (mit bar=foo[:]) und setzen Sie die Kopie dann zurück.
Martin v. Löwis

3
Ich denke, der Grund ist explizit. In bar = foo.reverse()könnte man denken, dass foodas nicht geändert wird. Um Verwirrung zu vermeiden, haben Sie beide foo.reverse()und bar = reversed(foo).
Roberto Bonvallet

Was ist falsch daran, die Art eines Parameters basierend auf einem Parameter zu ändern?
Julien


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}

Beachten Sie, dass nicht nur das zusammengeführte Diktat zurückgegeben wird, sondern auch der erste Parameter an Ort und Stelle geändert wird. Dikt_merge (a, b) ändert also a.

Oder natürlich können Sie alles inline erledigen:

>>> (lambda a,b: a.update(b) or a)({'a':1, 'b':3},{'c':5})
{'a': 1, 'c': 5, 'b': 3}

10
-1 lambdasollte so nicht verwendet werden, anstatt herkömmliche Funktion verwenden defstatt
jamylak

8
a.update(b) or a
Benötigen

10

nicht genug Ruf für Kommentar links oben Antwort

@beardc das scheint keine CPython-Sache zu sein. PyPy gibt mir "TypeError: Schlüsselwörter müssen Zeichenfolgen sein"

Die Lösung mit **kwargsfunktioniert nur, weil das zusammenzuführende Wörterbuch nur Schlüssel vom Typ Zeichenfolge enthält .

dh

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

vs.

>>> dict({1:2}, **{'3':4})
{1: 2, '3': 4}

5

Es ist nicht so, dass es nicht akzeptabel ist, sondern dass dictses nicht so implementiert wurde.

Wenn Sie sich Djangos ORM ansehen, wird die Verkettung in großem Umfang genutzt. Es ist nicht entmutigend, Sie könnten sogar erben dictund nur überschreiben update, um Updates durchzuführen und return self, wenn Sie es wirklich wollen.

class myDict(dict):
    def update(self, *args):
        dict.update(self, *args)
        return self

Vielen Dank, dies könnte diktieren, ich wollte nur wissen, warum dict () diese Funktionalität selbst nicht zulässt (da es so einfach ist, wie Sie es demonstrieren). Diktiert der Django-Patch so?
Paul Tarjan

2

so nah wie möglich an Ihrer vorgeschlagenen Lösung

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

1

Für diejenigen, die zu spät zur Party kommen, hatte ich ein Timing zusammengestellt (Py 3.7), um das zu zeigen .update() basierte Methoden ein bisschen (~ 5%) schneller aussehen, wenn Eingaben erhalten bleiben, und merklich (~ 30%) schneller, wenn sie nur an Ort und Stelle aktualisiert werden .

Wie üblich sollten alle Benchmarks mit einem Körnchen Salz genommen werden.

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)

Die Timings für die In-Place-Operationen sind etwas kniffliger, daher müsste sie entlang einer zusätzlichen Kopieroperation geändert werden (die erste Timing dient nur als Referenz):

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

0

Ich habe es gerade selbst in Python 3.4 versucht (konnte also die ausgefallene {**dict_1, **dict_2}Syntax nicht verwenden).

Ich wollte in der Lage sein, Schlüssel ohne Zeichenfolge in Wörterbüchern zu haben und eine beliebige Anzahl von Wörterbüchern bereitzustellen.

Außerdem wollte ich ein neues Wörterbuch collections.ChainMaperstellen, also habe ich mich dafür entschieden, es nicht zu verwenden (irgendwie der Grund, warum ich es anfangs nicht verwenden wollte dict.update.

Folgendes habe ich am Ende geschrieben:

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}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.