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?


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


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.


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


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}

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}

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

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

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)

0
import itertools
dict_merge = lambda *args: dict(itertools.chain(*[d.iteritems() for d in args]))

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}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.