Dlaczego python dict.update () nie zwraca obiektu?


139

Próbuję zrobić:

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

Ale jeśli czułbym się naprawdę niewygodny w tej funkcji, a wolałbym zrobić:

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

Dlaczego aktualizacja nie zwraca obiektu, aby można było połączyć?

JQuery robi to w celu tworzenia łańcuchów. Dlaczego nie jest to dopuszczalne w Pythonie?


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

2
@dreftymac, to jednak nie działa ze zrozumieniem.
alancalvitti

@alancalvitti Tak, jest to rzeczywiście jedno ważne zastrzeżenie, na które należy zwrócić uwagę.
dreftymac

Odpowiedzi:


219

Python w większości implementuje pragmatycznie zabarwioną odmianę separacji poleceń i zapytań : mutatory zwracają None(z pragmatycznie indukowanymi wyjątkami, takimi jak pop;-), więc nie można ich pomylić z akcesoriami (i podobnie przypisanie nie jest wyrażeniem, instrukcja - istnieje separacja wyrażeń i tak dalej).

Nie oznacza to, że nie ma wielu sposobów na scalenie rzeczy, gdy naprawdę chcesz, np. dict(a, **award_dict)Tworzy nowy dykt, podobny do tego, który wydaje się, że chcesz, aby został .updatezwrócony - więc dlaczego nie użyć TEGO, jeśli naprawdę uważasz, że to ważne ?

Edycja : przy okazji, nie ma potrzeby, w twoim konkretnym przypadku, tworzyć apo drodze, albo:

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

tworzy pojedynczy dykt z dokładnie taką samą semantyką jak twoja a.update(award_dict)(włączając, w przypadku konfliktów, fakt, że wpisy w award_dictnadpisują te, które podajesz jawnie; aby uzyskać inną semantykę, tj. aby mieć wyraźne wpisy "wygrywające" takie konflikty, przejść award_dictjako jedyny argument pozycyjny , przed słowami kluczowymi jedynkami i pozbawiony **postaci -dict(award_dict, name=name itd. itd.).


Cóż, spowoduje to utworzenie innego słownika po tym, jak musiałem utworzyć plik. Chciałem stworzyć dyktando, a następnie dodać kilka innych wartości, a następnie nadać je funkcji.
Paul Tarjan,

@Paul, i to jest dokładnie to, co robisz - z dwoma instrukcjami (znacznie bardziej czytelnymi niż zagnieżdżony sposób, w jaki chciałeś), które dla Ciebie „były naprawdę uciążliwe”. Edycja mojej odpowiedzi, aby pokazać, jak acałkowicie uniknąć tworzenia , przy okazji
Alex Martelli

1
Oryginalne rozwiązanie nie jest solidne. Jeśli Award_dict zawiera klucze już określone, zostanie zgłoszony błąd SyntaxError dla powtarzającego się argumentu słowa kluczowego. jamylak's solution dict (itertools.chain (d1.iteritems (), .. d <n> .iteritems ())) działa nie tylko w przypadku, gdy słowniki mają zduplikowane klucze, ale także w łatwy sposób umożliwia późniejsze połączenie wielu słowników z dyktami łańcuch ma pierwszeństwo dla wartości końcowej.
Matt

2
Ponadto, jeśli klucze w award_dict nie są ciągami, interpreter wyśle ​​aTypeError
kunl

3
dict(old_dict, old_key=new_value)nie wyrzuci wielu wartości słowa kluczowego i nie zwróci nowego dict.
Charmy

35

API Pythona, zgodnie z konwencją, rozróżnia procedury i funkcje. Funkcje obliczają nowe wartości na podstawie swoich parametrów (w tym dowolnego obiektu docelowego); procedury modyfikują obiekty i niczego nie zwracają (tj. zwracają None). Zatem procedury mają skutki uboczne, a funkcje nie. aktualizacja jest procedurą, dlatego nie zwraca wartości.

Motywacją do zrobienia tego w ten sposób jest to, że w przeciwnym razie mogą wystąpić niepożądane skutki uboczne. Rozważać

bar = foo.reverse()

Jeśli reverse (które odwraca listę w miejscu) również zwróci listę, użytkownicy mogą pomyśleć, że reverse zwraca nową listę, która zostanie przypisana do bar, i nigdy nie zauważą, że foo również zostanie zmodyfikowane. Dokonując odwrócenia zwrotu Brak, natychmiast rozpoznają, że słupek nie jest wynikiem odwrócenia i przyjrzą się dokładniej efektowi odwrócenia.


1
Dziękuję Ci. Dlaczego odwrócenie nie dałoby również opcji, aby nie robić tego na miejscu? Występ? robienie reverse(foo)czuje się dziwnie.
Paul Tarjan,

Dodanie opcji byłoby niewłaściwe: zmieniłoby charakter metody w zależności od parametru. Jednak metody powinny naprawdę mieć ustalone typy zwracanych (są niestety przypadki, w których ta reguła jest zepsuta). Utworzenie przywróconej kopii jest łatwe: po prostu wykonaj kopię (używając bar=foo[:]), a następnie przywróć kopię.
Martin v. Löwis

3
Myślę, że powodem jest jawność. W bar = foo.reverse(), można pomyśleć, że foonie jest modyfikowany. Aby uniknąć nieporozumień, masz zarówno foo.reverse()i bar = reversed(foo).
Roberto Bonvallet

Co jest złego w zmianie charakteru parametru opartego na parametrze?
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}

Zauważ, że oprócz zwrócenia scalonego dyktu, modyfikuje on lokalnie pierwszy parametr. Więc dict_merge (a, b) zmodyfikuje plik.

Lub, oczywiście, możesz to wszystko zrobić w tekście:

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

10
-1 lambdanie powinny być wykorzystywane w ten sposób, zamiast wykorzystania funkcji konwencjonalnego defzamiast
jamylak

8
Nie potrzebuję nawet lambdy, po prostu użyja.update(b) or a
Pycz

10

nie ma wystarczającej reputacji, by zostawić komentarz przy górnej odpowiedzi

@beardc to nie wygląda na rzecz CPythona. PyPy wyświetla „TypeError: słowa kluczowe muszą być ciągami”

Rozwiązanie **kwargstylko działa, ponieważ słownik, który ma zostać scalony, zawiera tylko klucze typu string .

to znaczy

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

Nie chodzi o to, że jest to niedopuszczalne, ale raczej, że dictsnie zostały wdrożone w ten sposób.

Jeśli spojrzysz na ORM Django, szeroko wykorzystuje on łańcuchy. Nie jest to zniechęcające, możesz nawet odziedziczyć dicti tylko zastąpić updateaktualizację i return self, jeśli naprawdę tego chcesz.

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

Dziękuję, to może dyktować patch, chciałem tylko wiedzieć, dlaczego dict () nie pozwalał na tę funkcjonalność jako taką (skoro jest to tak proste, jak zademonstrowałeś). Czy patch Django tak narzuca?
Paul Tarjan,

2

tak blisko proponowanego rozwiązania, jak tylko mogłem

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

Dla tych, którzy spóźnili się na imprezę, przygotowałem trochę czasu (Py 3.7), pokazując to .update() metody bazujące wyglądają nieco (~ 5%) szybciej, gdy dane wejściowe są zachowane i zauważalnie (~ 30%) szybciej, gdy tylko aktualizują się na miejscu .

Jak zwykle do wszystkich benchmarków należy podchodzić z przymrużeniem oka.

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)

Czasy operacji w miejscu są nieco trudniejsze, więc należałoby je zmodyfikować w ramach dodatkowej operacji kopiowania (pierwszy czas jest tylko w celach informacyjnych):

%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

Właśnie próbowałem tego sam w Pythonie 3.4 (więc nie mogłem użyć fantazyjnego {**dict_1, **dict_2} składni).

Chciałem mieć klucze niebędące ciągami znaków w słownikach, a także udostępniać dowolną liczbę słowników.

Chciałem też stworzyć nowy słownik, więc zdecydowałem się go nie używać collections.ChainMap(taki powód, dla którego początkowo nie chciałem używać dict.update.

Oto, co napisałem:

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}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.