¿Por qué un pitón dict.update () no devuelve el objeto?

139

Estoy tratando de hacer:

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

Pero si me siento realmente engorroso en la función, y hubiera preferido hacerlo:

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

¿Por qué la actualización no devuelve el objeto para que pueda encadenar?

JQuery hace esto para encadenar. ¿Por qué no es aceptable en Python?

Paul Tarjan
fuente
14
* TL; DRnewdict = dict(dict001, **dict002)
dreftymac
2
@dreftymac, eso no funciona en las comprensiones sin embargo.
alancalvitti
@alancalvitti Sí, esa es una advertencia válida para señalar.
dreftymac

Respuestas:

219

Python está implementando principalmente un sabor pragmáticamente teñido de separación de consulta de comando : los mutadores regresan None(con excepciones inducidas pragmáticamente como pop;-) para que no puedan confundirse con los accesores (y en la misma línea, la asignación no es una expresión, la declaración -expresión de separación está ahí, y así sucesivamente).

Eso no significa que no haya muchas maneras de fusionar las cosas cuando realmente quiere, por ejemplo, crea dict(a, **award_dict)un nuevo dictado muy parecido al que parece que desea .updatedevolver, así que ¿por qué no usar ESO si realmente cree que es importante? ?

Editar : por cierto, no es necesario, en su caso específico, crear aen el camino, ya sea:

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

crea un solo dict con exactamente la misma semántica que la suya a.update(award_dict)(incluido, en caso de conflictos, el hecho de que las entradas award_dictanulan las que está dando explícitamente; para obtener la otra semántica, es decir, tener entradas explícitas que "ganen" tales conflictos, pasar award_dictcomo el único argumento posicional , antes las palabras clave, y sin el **formulario ( dict(award_dict, name=nameetc., etc.).

Alex Martelli
fuente
Bueno, eso creará otro diccionario después de que tenga que hacer un. Quería crear un dict, y luego agregar un montón de otros valores, y luego asignarlo a una función.
Paul Tarjan
@Paul, y eso es exactamente lo que estás haciendo: con dos declaraciones (mucho más legibles que la forma anidada que querías) que para ti "te sentiste realmente engorroso". Editando mi respuesta para mostrar cómo evitar crear por acompleto, por cierto,
Alex Martelli
1
La solución original no es robusta. Si award_dict contiene claves ya especificadas, se lanzará un SyntaxError para un argumento de palabra clave repetido. La solución dict de jamylak (itertools.chain (d1.iteritems (), .. d <n> .iteritems ())) no solo funciona en el caso de que los diccionarios tengan claves duplicadas, sino que también le permite combinar fácilmente varios diccionarios con dictos más adelante en la cadena tiene prioridad para el valor final.
Matt
2
Además, si las claves en award_dict no son cadenas, el intérprete arrojará unTypeError
kunl el
3
dict(old_dict, old_key=new_value)no arrojará múltiples valores para la palabra clave y devolverá un nuevo dict.
Charmy
35

La API de Python, por convención, distingue entre procedimientos y funciones. Las funciones calculan nuevos valores a partir de sus parámetros (incluido cualquier objeto de destino); Los procedimientos modifican objetos y no devuelven nada (es decir, no devuelven ninguno). Entonces los procedimientos tienen efectos secundarios, las funciones no. La actualización es un procedimiento, por lo tanto, no devuelve un valor.

La motivación para hacerlo de esa manera es que de lo contrario, puede obtener efectos secundarios indeseables. Considerar

bar = foo.reverse()

Si reverse (que invierte la lista en el lugar) también devolvería la lista, los usuarios pueden pensar que reverse devuelve una nueva lista que se asigna a la barra, y nunca notan que foo también se modifica. Al realizar el retorno inverso Ninguno, reconocen de inmediato que la barra no es el resultado de la inversión, y mirarán más de cerca cuál es el efecto de la inversión.

Martin v. Löwis
fuente
1
Gracias. ¿Por qué no dar marcha atrás también da la opción de no hacerlo en el lugar? ¿Actuación? hacer se reverse(foo)siente raro.
Paul Tarjan
Agregar una opción sería inapropiado: cambiaría la naturaleza del método dependiendo de un parámetro. Sin embargo, los métodos realmente deberían tener tipos de retorno fijos (desafortunadamente, hay casos en que esta regla se rompe). Es fácil crear una copia revertida: simplemente haga una copia (usando bar=foo[:]), luego revierta la copia.
Martin v. Löwis
3
Creo que la razón es lo explícito. En bar = foo.reverse(), podrías pensar que foono está modificado. Para evitar confusiones, tienes ambos foo.reverse()y bar = reversed(foo).
Roberto Bonvallet
¿Qué hay de malo en cambiar la naturaleza de un parámetro basado en un parámetro?
Julien
22

Esto es fácil como:

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

Tenga en cuenta que además de devolver el dict combinado, modifica el primer parámetro en el lugar. Entonces dict_merge (a, b) modificará a.

O, por supuesto, puedes hacerlo todo en línea:

>>> (lambda a,b: a.update(b) or a)({'a':1, 'b':3},{'c':5})
{'a': 1, 'c': 5, 'b': 3}
Crispin Wellington
fuente
10
-1 lambdano debe usarse así, en su lugar use la función convencional en su deflugar
jamylak
8
Ni siquiera necesito una lambda, solo usea.update(b) or a
Pycz
10

no hay suficiente reputación para comentarios en la respuesta superior

@beardc esto no parece ser cosa de CPython. PyPy me da "TypeError: las palabras clave deben ser cadenas"

La solución **kwargssolo funciona porque el diccionario que se fusionará solo tiene claves de tipo cadena .

es decir

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

No es que no sea aceptable, sino que dicts no se implementaron de esa manera.

Si nos fijamos en el ORM de Django, hace un amplio uso del encadenamiento. No se desaconseja, incluso podría heredar dicty solo anular updatepara actualizar y return self, si realmente lo desea.

class myDict(dict):
    def update(self, *args):
        dict.update(self, *args)
        return self
Esteban Küber
fuente
Gracias, esto podría parchear dict, solo quería saber por qué dict () no permitió esta funcionalidad en sí misma (ya que es tan fácil como lo demuestras). ¿Django parche dict así?
Paul Tarjan
2

tan cerca de su solución propuesta como pude conseguir

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

Para aquellos que llegan tarde a la fiesta, había preparado un poco de tiempo (Py 3.7), demostrando que .update() métodos basados ​​se ven un poco (~ 5%) más rápido cuando se conservan las entradas y notablemente (~ 30%) más rápido cuando solo se actualiza en el lugar .

Como de costumbre, todos los puntos de referencia deben tomarse con un grano 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)

Los tiempos para las operaciones en el lugar son un poco más complicados, por lo que debería modificarse junto con una operación de copia adicional (el primer tiempo es solo para referencia):

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

Solo he estado probando esto en Python 3.4 (por lo que no pude usar la {**dict_1, **dict_2}sintaxis elegante ).

Quería poder tener claves sin cadenas en los diccionarios, así como proporcionar una cantidad arbitraria de diccionarios.

Además, quería hacer un nuevo diccionario, así que opté por no usarlo collections.ChainMap(como la razón por la que no quería usarlo dict.updateinicialmente).

Esto es lo que terminé escribiendo:

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}
regalo de promoción
fuente