Вопрос-Ответ

Accessing dict keys like an attribute?

Доступ к ключам dict как к атрибуту?

Я нахожу более удобным доступ к ключам dict как obj.foo вместо obj['foo'], поэтому я написал этот фрагмент:

class AttributeDict(dict):
def __getattr__(self, attr):
return self[attr]
def __setattr__(self, attr, value):
self[attr] = value

Однако я предполагаю, что должна быть какая-то причина, по которой Python не предоставляет эту функциональность "из коробки". Каковы были бы предостережения и подводные камни при доступе к ключам dict таким образом?

Переведено автоматически
Ответ 1

Обновление - 2020

Поскольку этот вопрос был задан почти десять лет назад, с тех пор в самом Python многое изменилось.

Хотя подход в моем первоначальном ответе все еще актуален для некоторых случаев (например, устаревшие проекты, привязанные к более старым версиям Python, и случаи, когда вам действительно нужно обрабатывать словари с очень динамичными строковыми ключами), я думаю, что в целом классы данных, представленные в Python 3.7, являются очевидным / правильным решением для подавляющего большинства случаев использования AttrDict.

Оригинальный ответ

Лучший способ сделать это:

class AttrDict(dict):
def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs)
self.__dict__ = self

Некоторые плюсы:


  • Это действительно работает!

  • Никакие методы класса словаря не затеняются (например, .keys() работают просто отлично. Если, конечно, вы не присвоите им какое-либо значение, см. Ниже)

  • Атрибуты и элементы всегда синхронизированы

  • Попытка получить доступ к несуществующему ключу в качестве атрибута корректно вызывает AttributeError вместо KeyError

  • Поддерживает [Tab] автозаполнение (например, в jupyter и ipython)

Минусы:


  • Такие методы, как .keys() не будут работать нормально, если они будут перезаписаны входящими данными

  • Вызывает утечку памяти в Python < 2.7.4 / Python3 < 3.2.3

  • Pylint сходит с ума от E1123(unexpected-keyword-arg) и E1103(maybe-no-member)

  • Для непосвященных это кажется чистой магией.

Краткое объяснение того, как это работает


  • Все объекты python внутренне хранят свои атрибуты в словаре с именем __dict__.

  • Нет требования, чтобы внутренний словарь __dict__ должен был быть "просто dict", поэтому мы можем назначить любой подкласс dict() внутреннему словарю.

  • В нашем случае мы просто назначаем AttrDict() экземпляр, который мы создаем (как мы делаем в __init__).

  • Вызвав метод super()'s __init__(), мы убедились, что он (уже) ведет себя точно так же, как словарь, поскольку эта функция вызывает весь код для создания экземпляра словаря.

Одна из причин, по которой Python не предоставляет эту функциональность "из коробки"

Как отмечено в списке "против", это объединяет пространство имен хранимых ключей (которые могут поступать из произвольных и / или ненадежных данных!) с пространством имен встроенных атрибутов метода dict. Например:

d = AttrDict()
d.update({'items':["jacket", "necktie", "trousers"]})
for k, v in d.items(): # TypeError: 'list' object is not callable
print "Never reached!"
Ответ 2

Все допустимые строковые символы могут быть частью ключа, если вы используете нотацию массива. Например, obj['!#$%^&*()_']

Ответ 3

В котором я отвечаю на заданный вопрос

Почему Python не предлагает это "из коробки"?

Я подозреваю, что это связано с дзеном Python: "Должен быть один - и предпочтительно только один - очевидный способ сделать это". Это создало бы два очевидных способа доступа к значениям из словарей: obj['key'] и obj.key.

Предостережения и подводные камни

К ним относятся возможные неясности и путаница в коде. т. е. Следующее может ввести в заблуждение кого-то еще, кто собирается поддерживать ваш код позже, или даже вас, если вы не собираетесь возвращаться к нему некоторое время. Опять же, от Zen: "Читаемость имеет значение!"

>>> KEY = 'spam'
>>> d[KEY] = 1
>>> # Several lines of miscellaneous code here...
... assert d.spam == 1

Если d создается экземпляр или KEY определяется или d[KEY] назначается далеко от того места, где d.spam используется, это может легко привести к путанице в отношении того, что делается, поскольку это не часто используемая идиома. Я знаю, что это может сбить меня с толку.

Кроме того, если вы измените значение KEY следующим образом (но пропустите изменение d.spam), теперь вы получите:

>>> KEY = 'foo'
>>> d[KEY] = 1
>>> # Several lines of miscellaneous code here...
... assert d.spam == 1
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
AttributeError: 'C' object has no attribute 'spam'

ИМО, не стоит затраченных усилий.

Другие элементы

Как отмечали другие, в качестве ключа dict можно использовать любой хэшируемый объект (не только строку). Например,

>>> d = {(2, 3): True,}
>>> assert d[(2, 3)] is True
>>>

это законно, но

>>> C = type('C', (object,), {(2, 3): True})
>>> d = C()
>>> assert d.(2, 3) is True
File "<stdin>", line 1
d.(2, 3)
^
SyntaxError: invalid syntax
>>> getattr(d, (2, 3))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: getattr(): attribute name must be string
>>>

нет. Это дает вам доступ ко всему диапазону печатаемых символов или других хэшируемых объектов для ключей вашего словаря, которых у вас нет при доступе к атрибуту объекта. Это делает возможным такое волшебство, как метакласс кэшированных объектов, подобный рецепту из поваренной книги Python (глава 9).

Где я редактирую

Я предпочитаю эстетику spam.eggs over spam['eggs'] (я думаю, это выглядит чище), и я действительно начал жаждать этой функциональности, когда познакомился с namedtuple. Но удобство возможности выполнять следующее превосходит его.

>>> KEYS = 'spam eggs ham'
>>> VALS = [1, 2, 3]
>>> d = {k: v for k, v in zip(KEYS.split(' '), VALS)}
>>> assert d == {'spam': 1, 'eggs': 2, 'ham': 3}
>>>

Это простой пример, но я часто ловлю себя на том, что использую dict в ситуациях, отличных от тех, которые я бы использовал obj.key обозначение (например, когда мне нужно прочитать prefs из XML-файла). В других случаях, когда у меня возникает соблазн создать экземпляр динамического класса и добавить к нему некоторые атрибуты по эстетическим соображениям, я продолжаю использовать dict для обеспечения согласованности, чтобы улучшить читаемость.

Я уверен, что OP уже давно решил эту проблему к своему удовлетворению, но если он все еще хочет эту функциональность, тогда я предлагаю ему загрузить один из пакетов из pypi, который ее предоставляет:


  • Мне больше знакомасвязка. Подкласс dict, так что у вас есть вся эта функциональность.

  • AttrDict также выглядит довольно неплохо, но я не так хорошо знаком с ним и не просматривал исходный код так подробно, как у меня есть Bunch.

  • Addict Активно поддерживается и обеспечивает доступ, подобный attr, и многое другое.

  • Как отмечено в комментариях Rotareti, Bunch устарел, но существует активный форк под названием Munch.

Однако, чтобы улучшить читаемость его кода, я настоятельно рекомендую ему не смешивать свои стили обозначения. Если он предпочитает эту нотацию, то ему следует просто создать экземпляр динамического объекта, добавить к нему желаемые атрибуты и на этом заканчивать:

>>> C = type('C', (object,), {})
>>> d = C()
>>> d.spam = 1
>>> d.eggs = 2
>>> d.ham = 3
>>> assert d.__dict__ == {'spam': 1, 'eggs': 2, 'ham': 3}


В котором я обновляю, чтобы ответить на последующий вопрос в комментариях

В комментариях (ниже) Элмо спрашивает:


Что, если вы хотите пойти еще глубже? (имеется в виду тип (...) )


Хотя я никогда не использовал этот вариант использования (опять же, я склонен использовать вложенный dict для согласованности), работает следующий код:

>>> C = type('C', (object,), {})
>>> d = C()
>>> for x in 'spam eggs ham'.split():
... setattr(d, x, C())
... i = 1
... for y in 'one two three'.split():
... setattr(getattr(d, x), y, i)
... i += 1
...
>>> assert d.spam.__dict__ == {'one': 1, 'two': 2, 'three': 3}
Ответ 4

Из этого другого вопроса SO есть отличный пример реализации, который упрощает ваш существующий код. Как насчет:

class AttributeDict(dict):
__slots__ = ()
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__

Намного лаконичнее и не оставляет места для дополнительной обработки ваших __getattr__ и __setattr__ функций в будущем.

python dictionary