Perché un pitone dict.update () non restituisce l'oggetto?


139

Sto cercando di fare:

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

Ma se mi sentissi davvero ingombrante nella funzione, e avrei preferito farlo:

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

Perché l'aggiornamento non restituisce l'oggetto in modo da poter eseguire la catena?

JQuery fa questo per fare il concatenamento. Perché non è accettabile in Python?


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

2
@dreftymac, tuttavia non funziona in termini di comprensione.
alancalvitti,

@alancalvitti Sì, questo è davvero un avvertimento valido da sottolineare.
dreftymac,

Risposte:


219

Python sta implementando principalmente un sapore pragmaticamente sfumato di separazione query-comando : i mutatori ritornano None(con eccezioni indotte pragmaticamente come pop;-), quindi non possono essere confusi con gli accessori (e allo stesso modo, l'assegnazione non è un'espressione, l'affermazione -la separazione delle espressioni c'è, e così via).

Ciò non significa che non ci siano molti modi per unire le cose quando vuoi davvero, ad esempio, dict(a, **award_dict)crea un nuovo dict molto simile a quello che sembri desiderare .updaterestituito, quindi perché non usarlo se ritieni che sia davvero importante ?

Modifica : a proposito, non è necessario, nel tuo caso specifico, creare alungo la strada, sia:

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

crea un singolo dict con esattamente la stessa semantica della tua a.update(award_dict)(incluso, in caso di conflitti, il fatto che le voci in award_dictsovrascrivono quelle che stai esplicitamente dando; per ottenere l'altra semantica, cioè per avere voci esplicite che "vincono" tali conflitti, passare award_dictcome unico argomento posizionale , prima di quelli di parole chiave e privo del **modulo - dict(award_dict, name=nameecc. ecc.).


Bene, questo creerà un altro dizionario dopo che ho dovuto creare un. Volevo creare un dict, quindi aggiungere un sacco di altri valori e quindi assegnarlo a una funzione.
Paul Tarjan,

@Paul, ed è esattamente quello che stai facendo - con due affermazioni (molto più leggibili del modo annidato che volevi) che per te "ti sono sentite davvero ingombranti". Modifica la mia risposta per mostrare come evitare di creare del atutto, tra l'altro,
Alex Martelli,

1
La soluzione originale non è robusta. Se award_dict contiene chiavi già specificate, verrà generato un SyntaxError per un argomento di parola chiave ripetuto. La soluzione di jamylak dict (itertools.chain (d1.iteritems (), .. d <n> .iteritems ())) non funziona solo nel caso in cui i dizionari abbiano chiavi duplicate, ma consente anche di unire più dizionari con dicts in seguito la catena che ha la precedenza per il valore finale.
Matt,

2
Inoltre, se le chiavi in ​​award_dict non sono string, l'interprete lancerà unTypeError
kunl il

3
dict(old_dict, old_key=new_value)non genererà più valori per la parola chiave e non restituirà nuovo dict.
Charmy,

35

L'API di Python, per convenzione, distingue tra procedure e funzioni. Le funzioni calcolano nuovi valori in base ai loro parametri (incluso qualsiasi oggetto target); le procedure modificano gli oggetti e non restituiscono nulla (ovvero restituiscono Nessuno). Quindi le procedure hanno effetti collaterali, le funzioni no. L'aggiornamento è una procedura, quindi non restituisce un valore.

La motivazione per farlo in questo modo è che, altrimenti, potresti ottenere effetti collaterali indesiderati. Tener conto di

bar = foo.reverse()

Se reverse (che inverte l'elenco sul posto) restituisce anche l'elenco, gli utenti possono pensare che reverse restituisca un nuovo elenco che viene assegnato alla barra e non si accorga mai che anche foo viene modificato. Effettuando il ritorno inverso Nessuno, riconoscono immediatamente che la barra non è il risultato dell'inversione e guarderanno più da vicino quale sia l'effetto del contrario.


1
Grazie. Perché invertire non darebbe anche la possibilità di non farlo sul posto? Prestazione? fare reverse(foo)è strano.
Paul Tarjan,

L'aggiunta di un'opzione sarebbe inappropriata: cambierebbe la natura del metodo in base a un parametro. Tuttavia, i metodi dovrebbero avere tipi di restituzione fissi (ci sono, purtroppo, casi in cui questa regola è infranta). È facile creare una copia ripristinata: basta fare una copia (usando bar=foo[:]), quindi ripristinare la copia.
Martin v. Löwis,

3
Penso che la ragione sia esplicativa. In bar = foo.reverse(), potresti pensare che foonon sia stato modificato. Per evitare confusione, hai entrambi foo.reverse()e bar = reversed(foo).
Roberto Bonvallet,

Cosa c'è di sbagliato nel cambiare la natura di un parametro basato su un parametro?
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}

Si noti che oltre a restituire il dict unito, modifica il primo parametro sul posto. Quindi dict_merge (a, b) modificherà a.

Oppure, ovviamente, puoi fare tutto in linea:

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

10
-1 lambdanon deve essere utilizzato come quella, utilizzare la funzione convenzionale definvece
jamylak

8
Non ho nemmeno bisogno di una lambda, basta usarea.update(b) or a
Pycz

10

reputazione insufficiente per il commento lasciato sulla risposta migliore

@beardc questa non sembra essere una cosa di CPython. PyPy mi dà "TypeError: le parole chiave devono essere stringhe"

La soluzione **kwargsfunziona solo perché il dizionario da unire ha solo chiavi di tipo stringa .

vale a dire

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

Non è che non sia accettabile, ma piuttosto che dictsnon sono stati implementati in questo modo.

Se guardi l'ORM di Django, fa ampio uso del concatenamento. Non è scoraggiato, potresti persino ereditare dicte sovrascrivere solo updateper fare l'aggiornamento e return self, se lo desideri davvero.

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

Grazie, questo potrebbe patchare dict, volevo solo sapere perché dict () non ha consentito questa funzionalità stessa (dal momento che è facile come hai dimostrato). La patch di Django impone in questo modo?
Paul Tarjan,

2

il più vicino possibile alla soluzione proposta

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

Per quelli che arrivano tardi alla festa, ho messo insieme un po 'di tempismo (Py 3.7), dimostrando che i .update()metodi basati sembrano un po' (~ 5%) più veloci quando gli input sono preservati e notevolmente (~ 30%) più veloci quando si aggiorna sul posto .

Come al solito, tutti i parametri di riferimento dovrebbero essere presi con un granello di sale.

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)

I tempi per le operazioni sul posto sono un po 'più complicati, quindi dovrebbe essere modificato lungo un'operazione di copia aggiuntiva (il primo tempo è solo per riferimento):

%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

Ho appena provato questo in Python 3.4 (quindi non sono stato in grado di usare la {**dict_1, **dict_2}sintassi elaborata ).

Volevo poter avere chiavi senza stringa nei dizionari e fornire una quantità arbitraria di dizionari.

Inoltre, volevo creare un nuovo dizionario, quindi ho deciso di non utilizzare collections.ChainMap(un po 'il motivo per cui non volevo usare dict.updateinizialmente.

Ecco cosa ho finito per scrivere:

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}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.