Por que um python dict.update () retorna o objeto?

139

Eu estou tentando fazer:

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

Mas se me sentisse realmente complicado na função, e eu preferiria:

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

Por que a atualização não retorna o objeto para que você possa encadear?

O JQuery faz isso para encadear. Por que não é aceitável em python?

Paul Tarjan
fonte
14
* TL; DRnewdict = dict(dict001, **dict002)
dreftymac
2
@dreftymac, que não funciona de maneira compreensiva.
18719 Alancalvitti
@alancalvitti Sim, essa é realmente uma ressalva válida a ser destacada.
dreftymac

Respostas:

219

O Python está implementando principalmente um sabor pragmaticamente tingido de separação de consulta de comando : os mutadores retornam None(com exceções induzidas pragmaticamente como pop;-), para que não possam ser confundidas com os acessadores (e na mesma linha, atribuição não é uma expressão, a instrução separação de expressão existe e assim por diante).

Isso não significa que não há muitas maneiras de mesclar as coisas quando você realmente deseja, por exemplo, cria dict(a, **award_dict)um novo ditado muito parecido com o que você parece desejar .updateretornar - então, por que não usá-lo se realmente acha importante ?

Edit : btw, não é necessário, no seu caso específico, criar aao longo do caminho:

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

cria um único ditado com exatamente a mesma semântica que a sua a.update(award_dict)(incluindo, no caso de conflitos, o fato de que as entradas award_dictsubstituem as que você está dando explicitamente; para obter a outra semântica, ou seja, para ter entradas explícitas "vencendo" esses conflitos, passar award_dictcomo o único argumento posicional , antes das palavras-chave e sem o **formulário - dict(award_dict, name=nameetc etc).

Alex Martelli
fonte
Bem, isso criará outro dicionário depois que eu tiver que fazer um. Eu queria criar um ditado e, em seguida, adicionar vários outros valores e, em seguida, atribuí-lo a uma função.
Paul Tarjan
@Paul, e é exatamente isso que você está fazendo - com duas declarações (muito mais legíveis do que a maneira aninhada que você queria) que para você "pareciam realmente complicadas". Editando minha resposta para mostrar como evitar a criação de todos a, btw,
Alex Martelli
1
A solução original não é robusta. Se award_dict contiver chaves já especificadas, um SyntaxError será lançado para um argumento de palavra-chave repetido. O dict de solução do jamylak (itertools.chain (d1.iteritems (), .. d <n> .iteritems ())) não funciona apenas no caso em que os dicionários possuem chaves duplicadas, mas também permite mesclar vários dicionários com ditados posteriormente a cadeia tem precedência para o valor final.
Matt
2
Além disso, se as chaves na award_dict não são seqüência o intérprete irá lançar umTypeError
kunl
3
dict(old_dict, old_key=new_value)não lançará vários valores para a palavra-chave e retornará um novo ditado.
Charmy
35

A API do Python, por convenção, distingue entre procedimentos e funções. As funções calculam novos valores a partir de seus parâmetros (incluindo qualquer objeto de destino); Os procedimentos modificam objetos e não retornam nada (ou seja, eles retornam Nenhum). Portanto, procedimentos têm efeitos colaterais, funções não. update é um procedimento, portanto, ele não retorna um valor.

A motivação para fazer dessa maneira é que, caso contrário, você poderá obter efeitos colaterais indesejáveis. Considerar

bar = foo.reverse()

Se reverse (que inverte a lista no local) também devolve a lista, os usuários podem pensar que reverse retorna uma nova lista que é atribuída à barra e nunca notam que foo também é modificado. Fazendo retorno reverso Nenhum, eles imediatamente reconhecem que a barra não é o resultado da reversão e parecerão mais próximos do efeito do reverso.

Martin v. Löwis
fonte
1
Obrigado. Por que o reverso também não daria a opção de não fazê-lo no lugar? Atuação? fazer reverse(foo)parece estranho.
Paul Tarjan
Adicionar uma opção seria inapropriado: mudaria a natureza do método, dependendo de um parâmetro. No entanto, os métodos realmente devem ter tipos de retorno fixos (existem, infelizmente, casos em que essa regra é violada). É fácil criar uma cópia revertida: basta fazer uma cópia (usando bar=foo[:]) e depois reverter a cópia.
Martin v. Löwis 21/09/09
3
Eu acho que o motivo é explicito. Em bar = foo.reverse(), você poderia pensar que foonão é modificado. Para evitar confusão, você tem ambos foo.reverse()e bar = reversed(foo).
Roberto Bonvallet
O que há de errado em alterar a natureza de um parâmetro com base em um parâmetro?
Julien
22

Isso é fácil como:

(lambda d: d.update(dict2) or d)(d1)
Kostya Goloveshko
fonte
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}

Observe que, além de retornar o ditado mesclado, ele modifica o primeiro parâmetro no local. Então dict_merge (a, b) modificará a.

Ou, é claro, você pode fazer tudo em linha:

>>> (lambda a,b: a.update(b) or a)({'a':1, 'b':3},{'c':5})
{'a': 1, 'c': 5, 'b': 3}
Crispin Wellington
fonte
10
-1 lambdanão deve ser utilizado como que, em vez disso usar função convencional defem vez
jamylak
8
Nem sequer precisa de um lambda, basta usara.update(b) or a
Pycz
10

reputação insuficiente para o comentário deixado na resposta superior

@ Beardc isso não parece ser coisa de CPython. PyPy me fornece "TypeError: palavras-chave devem ser cadeias de caracteres"

A solução **kwargsfunciona apenas porque o dicionário a ser mesclado possui apenas chaves do tipo string .

ie

>>> 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}
Stephan Scheller
fonte
5

Não é que não seja aceitável, mas que dicts não foi implementado dessa maneira.

Se você observar o ORM do Django, ele faz uso extensivo de encadeamento. Não é desencorajado, você pode até herdar dicte substituir apenas updatepara atualizar e return self, se você realmente quiser.

class myDict(dict):
    def update(self, *args):
        dict.update(self, *args)
        return self
Esteban Küber
fonte
Obrigado, isso poderia corrigir o dict, eu só queria saber por que o dict () não permitiu essa funcionalidade em si (já que é tão fácil quanto você demonstra). O patch do Django dita assim?
Paul Tarjan
2

o mais próximo possível da sua solução 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
Matus
fonte
1

Para aqueles que chegaram atrasados ​​à festa, eu tinha marcado um tempo (Py 3.7), mostrando que .update() métodos baseados parecem um pouco (~ 5%) mais rápidos quando as entradas são preservadas e visivelmente (~ 30%) mais rápidos quando apenas atualizamos no local .

Como sempre, todas as referências devem ser tomadas com um grão de sal.

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)

Os tempos para as operações no local são um pouco mais complicados, portanto, ele precisa ser modificado ao longo de uma operação de cópia extra (o primeiro tempo é apenas para referência):

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

Apenas tentei isso no Python 3.4 (por isso não foi capaz de usar a {**dict_1, **dict_2}sintaxe sofisticada ).

Eu queria poder ter chaves não-string nos dicionários, além de fornecer uma quantidade arbitrária de dicionários.

Além disso, eu queria criar um novo dicionário, por isso optei por não usar collections.ChainMap(meio que o motivo de não querer usar dict.updateinicialmente.

Aqui está o que eu acabei escrevendo:

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}
brinde
fonte