Доступ к ключам диктовки как к атрибуту? - PullRequest
252 голосов
/ 13 февраля 2011

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

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

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

Ответы [ 24 ]

261 голосов
/ 31 января 2013

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

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

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

  • Это на самом деле работает!
  • Методы класса словаря не затенены (например, .keys() работают нормально)
  • Атрибуты и элементы всегда синхронизированы
  • Попытка доступа к несуществующему ключу в качестве атрибута корректно вызывает AttributeError вместо KeyError

Минусы:

  • Такие методы, как .keys(), будут , а не будут работать просто отлично, если они будут перезаписаны входящими данными
  • Вызывает утечка памяти в Python <2.7.4 / Python3 <3.2.3 </li>
  • Пилинт идет бананом с E1123(unexpected-keyword-arg) и E1103(maybe-no-member)
  • Для непосвященных это похоже на чистую магию.

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

  • Все объекты Python хранят свои атрибуты внутри словаря с именем __dict__.
  • Нет требования, чтобы внутренний словарь __dict__ должен был быть "простым диктом", поэтому мы можем назначить любой подкласс dict() для внутреннего словаря.
  • В нашем случае мы просто назначаем экземпляр AttrDict(), который мы создаем (как в __init__).
  • Вызвав метод super() __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!"
124 голосов
/ 13 февраля 2011

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

72 голосов
/ 16 февраля 2011

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

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

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

64 голосов
/ 09 апреля 2015

, где я отвечаю на вопрос, который был задан

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

Я подозреваю, что это имеет отношение к Zen of 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 Cookbook (Ch. 9) .

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

Я предпочитаю эстетику spam.eggs, а не 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}
>>>

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

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

  • Связка - это тот, с которым я больше знаком. Подкласс dict, так что у вас есть все эти функции.
  • AttrDict также выглядит так, как будто это также довольно хорошо, но я не настолько знаком с ним и не просмотрел источник так подробно, как у меня Bunch .
  • Как отмечается в комментариях Ротарети, 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}

<ч />

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

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

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

Хотя я никогда не использовал этот вариант использования (опять же, я склонен использовать вложенный 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}
20 голосов
/ 21 ноября 2012

Caveat emptor: По некоторым причинам такие классы, кажется, нарушают многопроцессорный пакет. Я только что некоторое время боролся с этой ошибкой, прежде чем нашел это ТАК: Нахождение исключения в многопроцессорной среде Python

18 голосов
/ 13 февраля 2011

Что делать, если вам нужен ключ, который был методом, например __eq__ или __getattr__?

И вы не сможете получить запись, которая начинается не с буквы,поэтому использование 0343853 в качестве ключа отсутствует.

А что, если вы не хотите использовать строку?

16 голосов
/ 03 июня 2014

Вы можете извлечь удобный класс контейнеров из стандартной библиотеки:

from argparse import Namespace

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

class Namespace(_AttributeHolder):
    """Simple object for storing attributes.

    Implements equality by attribute names and values, and provides a simple
    string representation.
    """

    def __init__(self, **kwargs):
        for name in kwargs:
            setattr(self, name, kwargs[name])

    __hash__ = None

    def __eq__(self, other):
        return vars(self) == vars(other)

    def __ne__(self, other):
        return not (self == other)

    def __contains__(self, key):
        return key in self.__dict__
11 голосов
/ 13 февраля 2011

кортежей могут быть использованы ключи dict.Как бы вы получили доступ к кортежу в своей конструкции?

Кроме того, namedtuple - это удобная структура, которая может предоставлять значения через атрибут доступа.

8 голосов
/ 13 февраля 2011

Вообще не работает.Не все действительные ключи dict создают адресуемые атрибуты («ключ»).Итак, вам нужно быть осторожным.

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

5 голосов
/ 11 марта 2016

Вот краткий пример неизменяемых записей с использованием встроенного collections.namedtuple:

def record(name, d):
    return namedtuple(name, d.keys())(**d)

и пример использования:

rec = record('Model', {
    'train_op': train_op,
    'loss': loss,
})

print rec.loss(..)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...