Чому python dict.update () не повертає об'єкт?


139

Я намагаюся зробити:

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

Але якби я відчував себе справді громіздким у роботі, я б скоріше зробив це:

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

Чому оновлення не повертає об'єкт, щоб ви могли ланцюг?

JQuery робить це, щоб зробити ланцюжок. Чому це не прийнятно в python?


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

2
@dreftymac, однак це не працює в розумінні.
alancalvitti

@alancalvitti Так, це дійсно один дійсний застереження.
dreftymac

Відповіді:


219

Python здебільшого реалізує прагматично відтінковий смак розділення команд-запитів : мутатори повертаються None(з прагматично спричиненими винятками, такими як pop;-), тому їх неможливо сплутати з аксесуарами (і, таким же чином, призначення не є виразом, заявою -експресія є і так далі).

Це не означає, що існує не так багато способів з’єднати речі, коли ви дійсно хочете, наприклад, dict(a, **award_dict)робить новий вигляд, як і той, який, здається, бажаєте .updateповернутись - так чому б не використовувати ТО, якщо ви дійсно вважаєте, що це важливо ?

Редагувати : btw, у вашому конкретному випадку не потрібно створювати aпопутно:

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

створює єдиний дикт із точно такою ж семантикою, як і ваш a.update(award_dict)(у тому числі, у випадку конфліктів, той факт, що записи award_dictзаміняють ті, які ви чітко даєте; отримати іншу семантику, тобто мати чіткі записи, які "виграють" такі конфлікти, передавати award_dictяк єдиний позиційний аргумент перед ключовим словом та позбавлення **форми - dict(award_dict, name=nameтощо, тощо).


Ну, це створить інший словник після того, як мені довелося скласти. Я хотів створити дікт, а потім додати купу інших значень, а потім надати його функції.
Пол Тарджан

@Paul, і це саме те, що ти робиш - з двома твердженнями (набагато зрозумілішими, ніж вкладений спосіб, який ти хотів), які тобі "відчували себе справді громіздкими". Редагування моєї відповіді, щоб показати, як уникнути створення aвзагалі, btw,
Алекс Мартеллі

1
Оригінальне рішення не є надійним. Якщо присвоєння вирок містить ключі, уже вказані, для повторного аргументу ключового слова буде передано SyntaxError. Рішення рішення jamylak (itertools.chain (d1.iteritems (), .. d <n> .iteritems ())) працює не лише у тому випадку, коли словники мають повторювані ключі, але також легко дозволяє об'єднати декілька словників із диктами пізніше в ланцюг, що має перевагу над кінцевим значенням.
Метт

2
Крім того, якщо ключі в присуді вирок не є рядковими, інтерпретатор викине aTypeError
kunl

3
dict(old_dict, old_key=new_value)не кине кілька значень для ключового слова та не поверне новий вираз
Charmy

35

API Python за домовленістю розрізняє процедури та функції. Функції обчислюють нові значення за своїми параметрами (включаючи будь-який цільовий об'єкт); процедури змінюють об'єкти і нічого не повертають (тобто вони не повертають None). Отже, процедури мають побічні ефекти, функції не мають. оновлення - це процедура, отже, воно не повертає значення.

Мотивація зробити це таким чином полягає в тому, що в іншому випадку ви можете отримати небажані побічні ефекти. Розглянемо

bar = foo.reverse()

Якщо зворотний (який повертає список на місці) також поверне список, користувачі можуть подумати, що реверс повертає новий список, який присвоюється бар, і ніколи не помічають, що foo також змінюється. Здійснюючи зворотне повернення None, вони одразу визнають, що смужка не є результатом сторнування, і будуть виглядати ближче, що таке ефект зворотного.


1
Дякую. Чому б не повернути назад і не дати можливість не робити цього на місці? Продуктивність? робити reverse(foo)дивно.
Пол Тарджан

Додавання параметра було б недоцільним: це змінило би характер методу залежно від параметра. Однак методи дійсно повинні мати фіксований тип повернення (на жаль, є випадки, коли це правило порушено). Створити повернену копію легко: просто зробіть копію (використовуючи bar=foo[:]), а потім відновіть копію.
Мартін проти Левіса

3
Я думаю, що причина - це явність. У bar = foo.reverse(), ви могли б подумати, що fooце не змінено. Щоб уникнути плутанини, у вас є і те, foo.reverse()і bar = reversed(foo).
Роберто Бонвальлет

Що не так у зміні характеру параметра на основі параметра?
Жульєн


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}

Зауважте, що, як і повернення об'єднаного диктату, він змінює перший параметр на місці. Отже dict_merge (a, b) змінить a.

Або, звичайно, ви можете зробити все це вбудовано:

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

10
-1 lambdaне слід використовувати , як , що, замість того, щоб використовувати звичайну функцію defзамість
jamylak

8
Вам навіть не потрібна лямбда, просто використовуйтеa.update(b) or a
Pycz

10

недостатньо репутації для коментарів, залишених на верхній відповіді

@beardc це, здається, не річ CPython. PyPy дає мені "TypeError: ключові слова повинні бути рядками"

Рішення функціонує **kwargsлише тому, що словник, який потрібно об'єднати, має лише ключі рядка типу .

тобто

>>> dict({1:2}, **{3:4})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: keyword arguments must be strings

проти

>>> dict({1:2}, **{'3':4})
{1: 2, '3': 4}

5

Справа не в тому, що це не прийнятно, а скоріше, що dictsце не було реалізовано таким чином.

Якщо ви подивитесь на ORM Джанго, він широко використовує ланцюжок. Це не відлякує, ви навіть можете успадковувати dictі лише переосмислювати updateоновлення, і return self, якщо цього дуже хочете.

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

Дякую, що це може виправити dict, я просто хотів дізнатися, чому dict () не дозволяє сам цей функціонал (оскільки це так просто, як ви демонструєте). Чи патч Джанго диктує так?
Пол Тарджан

2

якнайближче до запропонованого вами рішення, як я міг отримати

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

Для тих, хто запізнюється на вечірку, я зібрав декілька часу (Py 3.7), показуючи, що .update()засновані методи виглядають трохи (~ 5%) швидше, коли входи зберігаються і помітно (~ 30%) швидше, коли просто оновлюється на місці .

Як завжди, всі орієнтири слід брати з зерном солі.

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)

Час виконання операцій на місці трохи складніше, тому його потрібно буде змінити в ході додаткової операції копіювання (перша хронологія - лише для ознайомлення):

%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

Просто я сам пробував це в Python 3.4 (тому не міг використати химерний {**dict_1, **dict_2}синтаксис).

Я хотів мати можливість мати в рядках нерядкові ключі, а також надати довільну кількість словників.

Крім того, я хотів створити новий словник, тому я вирішив не використовувати collections.ChainMap(якоюсь причиною я не хотів користуватися dict.updateспочатку.

Ось що я закінчив писати:

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}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.