Що таке "заморожений дикт"?


158
  • Заморожений набір - це заморожений набір.
  • Заморожений список може бути кортежем.
  • Що б застигла дикта? Незмінний, доступний диктант.

Я думаю, це може бути щось на кшталт collections.namedtuple, але це більше схоже на дикт із замороженими ключами (напівзаморожений дікт). Чи не так?

А «frozendict» повинен бути заморожений словником, він повинен мати keys, values, getі т.д., і підтримку in, forі т.д.

оновлення:
* є: https://www.python.org/dev/peps/pep-0603

Відповіді:


120

Python не має вбудованого типу frozendict. Виявляється, це не було б корисно занадто часто (хоча, мабуть, воно буде корисним частіше, ніж frozensetє).

Найпоширеніша причина бажати такого типу - це, коли функція запам'ятовування викликає функції з невідомими аргументами. Найбільш поширене рішення для зберігання хешируемого еквівалента диктату (де значення є хешируемими) - щось подібне tuple(sorted(kwargs.iteritems())).

Це залежить від того, щоб сортування не було трохи божевільним. Python не може позитивно пообіцяти, що сортування призведе до чогось розумного. (Але нічого іншого не обіцяє, тому не надто потійте.)


Ви могли б досить легко зробити якусь обгортку, яка працює так само, як диктант. Це може виглядати приблизно так

import collections

class FrozenDict(collections.Mapping):
    """Don't forget the docstrings!!"""

    def __init__(self, *args, **kwargs):
        self._d = dict(*args, **kwargs)
        self._hash = None

    def __iter__(self):
        return iter(self._d)

    def __len__(self):
        return len(self._d)

    def __getitem__(self, key):
        return self._d[key]

    def __hash__(self):
        # It would have been simpler and maybe more obvious to 
        # use hash(tuple(sorted(self._d.iteritems()))) from this discussion
        # so far, but this solution is O(n). I don't know what kind of 
        # n we are going to run into, but sometimes it's hard to resist the 
        # urge to optimize when it will gain improved algorithmic performance.
        if self._hash is None:
            hash_ = 0
            for pair in self.items():
                hash_ ^= hash(pair)
            self._hash = hash_
        return self._hash

Це має чудово працювати:

>>> x = FrozenDict(a=1, b=2)
>>> y = FrozenDict(a=1, b=2)
>>> x is y
False
>>> x == y
True
>>> x == {'a': 1, 'b': 2}
True
>>> d = {x: 'foo'}
>>> d[y]
'foo'

7
Я не знаю, про який рівень безпеки ниток хвилюються люди з подібними речами, але в цьому відношенні ваш __hash__метод може бути трохи вдосконалений. Просто використовуйте тимчасову змінну при обчисленні хеша і встановлюйте лише self._hashодин раз, коли ви отримаєте кінцеве значення. Таким чином, інший потік, який отримує хеш під час обчислення першого, просто зробить надмірний обчислення, а не отримає неправильне значення.
Jeff DQ

22
@Jeff Як правило, весь код скрізь не є безпечним для потоків, і вам слід обернути його навколо деяких структур синхронізації, щоб безпечно використовувати цей код. Крім того, ваше конкретне поняття безпеки потоку спирається на атомність присвоєння атрибутів об'єкта, що далеко не гарантоване.
Девін Жанп'єр

9
@Anentropic, Це зовсім не так.
Майк Грехем

17
Будьте попереджені: цей "FrozenDict" не обов'язково заморожений. Ніщо не заважає вам вводити змінений список як значення, і в цьому випадку хешування призведе до помилки. З цим нічого не обов'язково, але користувачі повинні знати. Інша справа: цей алгоритм хешуваннявання обраний погано, дуже схильний до хеш-зіткнень. Наприклад, {'a': 'b'} хеширует те саме, що і {'b': 'a'} і {'a': 1, 'b': 2} хеширує те саме, що і {'a': 2, ' b ': 1}. Кращим вибором буде self._hash ^ = hash ((ключ, значення))
Steve Byrnes

6
Якщо ви додасте змінений запис у незмінний об'єкт, дві можливі поведінки полягають у тому, щоб створити помилку при створенні об'єкта або помилку при хешировании об'єкта. Кортежі роблять останні, заморожений набір робить перший. Я напевно думаю, що ви прийняли гарне рішення скористатися останнім підходом. Тим не менш, я думаю, що люди можуть бачити, що FrozenDict і заморожений набір мають подібні назви, і приходять до висновку, що вони повинні поводитися аналогічно. Тому я думаю, що варто попередити людей про цю різницю. :-)
Стів Бернс

63

Цікаво, що, хоча ми рідко корисні frozensetв python, все ще немає заморожених карт. Ідея була відхилена в PEP 416 - Додайте вбудований тип frozendict . Ідея може бути переглянута в Python 3.9, див. PEP 603 - Додавання типу замороженої карти до колекцій .

Отже, рішення пітона 2 для цього:

def foo(config={'a': 1}):
    ...

Все ще здається дещо кульгавим:

def foo(config=None):
    if config is None:
        config = default_config = {'a': 1}
    ...

У python3 у вас є варіант цього :

from types import MappingProxyType

default_config = {'a': 1}
DEFAULTS = MappingProxyType(default_config)

def foo(config=DEFAULTS):
    ...

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

Таким чином, зміни у default_configзаповіті будуть оновлені, DEFAULTSяк очікувалося, але ви не можете записати в сам об’єкт проксі-відображення.

Справді, це не зовсім те саме, що "незмінний, доступний дикт", - але це гідна заміна з огляду на такі ж випадки використання, для яких ми могли б захотіти фрозендикт.


2
Чи є якась конкретна причина для зберігання проксі-сервера в змінній модуля? Чому б не просто def foo(config=MappingProxyType({'a': 1})):? Ваш приклад все ще дозволяє глобальну модифікацію default_configтакож.
jpmc26

Також я підозрюю, що подвійне присвоєння config = default_config = {'a': 1}- це друкарська помилка.
jpmc26

21

Якщо припустити, що ключі та значення словника є незмінними (наприклад, рядки), то:

>>> d
{'forever': 'atones', 'minks': 'cards', 'overhands': 'warranted', 
 'hardhearted': 'tartly', 'gradations': 'snorkeled'}
>>> t = tuple((k, d[k]) for k in sorted(d.keys()))
>>> hash(t)
1524953596

Це хороше, канонічне, незмінне зображення дикту (забороняючи божевільну порівняльну поведінку, що псує сорт).
Майк Грехем

6
@devin: погоджено повністю, але я дозволю своїй публікації стати прикладом того, що часто існує ще кращий спосіб.
msw

14
Ще краще було б помістити його в заморожений набір, який не вимагає, щоб ключі або значення мали певний послідовний порядок.
асмеурер

7
З цим лише одна проблема: у вас більше немає картографування. В цьому і полягає вся суть замороженого дикту в першу чергу.
Божевільний фізик

2
Цей метод справді приємний, коли повертаємось до дикту. простоdict(t)
кодикодер

12

Немає fronzedict, але ви можете використовувати MappingProxyTypeте, що було додано до стандартної бібліотеки з Python 3.3:

>>> from types import MappingProxyType
>>> foo = MappingProxyType({'a': 1})
>>> foo
mappingproxy({'a': 1})
>>> foo['a'] = 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> foo
mappingproxy({'a': 1})

з застереженням:TypeError: can't pickle mappingproxy objects
Раду

Мені подобається ідея цього. Я спробую це спробувати.
Дуг

10

Ось код, яким я користувався. Я підкласив заморожений набір. Переваги цього полягають у наступному.

  1. Це справді непорушний об’єкт. Не покладаючись на добру поведінку майбутніх користувачів та розробників.
  2. Легко перетворювати туди-сюди між звичайним словником та замороженим словником. FrozenDict (orig_dict) -> заморожений словник. dict (заморожений_ вирок) -> регулярний dict.

Оновлення 21 січня 2015 р .: Оригінальний фрагмент коду, який я опублікував у 2014 році, використовував for-цикл, щоб знайти ключ, який відповідає. Це було неймовірно повільно. Тепер я зібрав реалізацію, яка використовує переваги хешованих функцій заморожених завдань. Пари ключових значень зберігаються у спеціальних контейнерах, де функції __hash__та __eq__функції базуються лише на ключі. Цей код також був формально перевірений одиницею, на відміну від того, що я розмістив тут у серпні 2014 року.

Ліцензія в стилі MIT

if 3 / 2 == 1:
    version = 2
elif 3 / 2 == 1.5:
    version = 3

def col(i):
    ''' For binding named attributes to spots inside subclasses of tuple.'''
    g = tuple.__getitem__
    @property
    def _col(self):
        return g(self,i)
    return _col

class Item(tuple):
    ''' Designed for storing key-value pairs inside
        a FrozenDict, which itself is a subclass of frozenset.
        The __hash__ is overloaded to return the hash of only the key.
        __eq__ is overloaded so that normally it only checks whether the Item's
        key is equal to the other object, HOWEVER, if the other object itself
        is an instance of Item, it checks BOTH the key and value for equality.

        WARNING: Do not use this class for any purpose other than to contain
        key value pairs inside FrozenDict!!!!

        The __eq__ operator is overloaded in such a way that it violates a
        fundamental property of mathematics. That property, which says that
        a == b and b == c implies a == c, does not hold for this object.
        Here's a demonstration:
            [in]  >>> x = Item(('a',4))
            [in]  >>> y = Item(('a',5))
            [in]  >>> hash('a')
            [out] >>> 194817700
            [in]  >>> hash(x)
            [out] >>> 194817700
            [in]  >>> hash(y)
            [out] >>> 194817700
            [in]  >>> 'a' == x
            [out] >>> True
            [in]  >>> 'a' == y
            [out] >>> True
            [in]  >>> x == y
            [out] >>> False
    '''

    __slots__ = ()
    key, value = col(0), col(1)
    def __hash__(self):
        return hash(self.key)
    def __eq__(self, other):
        if isinstance(other, Item):
            return tuple.__eq__(self, other)
        return self.key == other
    def __ne__(self, other):
        return not self.__eq__(other)
    def __str__(self):
        return '%r: %r' % self
    def __repr__(self):
        return 'Item((%r, %r))' % self

class FrozenDict(frozenset):
    ''' Behaves in most ways like a regular dictionary, except that it's immutable.
        It differs from other implementations because it doesn't subclass "dict".
        Instead it subclasses "frozenset" which guarantees immutability.
        FrozenDict instances are created with the same arguments used to initialize
        regular dictionaries, and has all the same methods.
            [in]  >>> f = FrozenDict(x=3,y=4,z=5)
            [in]  >>> f['x']
            [out] >>> 3
            [in]  >>> f['a'] = 0
            [out] >>> TypeError: 'FrozenDict' object does not support item assignment

        FrozenDict can accept un-hashable values, but FrozenDict is only hashable if its values are hashable.
            [in]  >>> f = FrozenDict(x=3,y=4,z=5)
            [in]  >>> hash(f)
            [out] >>> 646626455
            [in]  >>> g = FrozenDict(x=3,y=4,z=[])
            [in]  >>> hash(g)
            [out] >>> TypeError: unhashable type: 'list'

        FrozenDict interacts with dictionary objects as though it were a dict itself.
            [in]  >>> original = dict(x=3,y=4,z=5)
            [in]  >>> frozen = FrozenDict(x=3,y=4,z=5)
            [in]  >>> original == frozen
            [out] >>> True

        FrozenDict supports bi-directional conversions with regular dictionaries.
            [in]  >>> original = {'x': 3, 'y': 4, 'z': 5}
            [in]  >>> FrozenDict(original)
            [out] >>> FrozenDict({'x': 3, 'y': 4, 'z': 5})
            [in]  >>> dict(FrozenDict(original))
            [out] >>> {'x': 3, 'y': 4, 'z': 5}   '''

    __slots__ = ()
    def __new__(cls, orig={}, **kw):
        if kw:
            d = dict(orig, **kw)
            items = map(Item, d.items())
        else:
            try:
                items = map(Item, orig.items())
            except AttributeError:
                items = map(Item, orig)
        return frozenset.__new__(cls, items)

    def __repr__(self):
        cls = self.__class__.__name__
        items = frozenset.__iter__(self)
        _repr = ', '.join(map(str,items))
        return '%s({%s})' % (cls, _repr)

    def __getitem__(self, key):
        if key not in self:
            raise KeyError(key)
        diff = self.difference
        item = diff(diff({key}))
        key, value = set(item).pop()
        return value

    def get(self, key, default=None):
        if key not in self:
            return default
        return self[key]

    def __iter__(self):
        items = frozenset.__iter__(self)
        return map(lambda i: i.key, items)

    def keys(self):
        items = frozenset.__iter__(self)
        return map(lambda i: i.key, items)

    def values(self):
        items = frozenset.__iter__(self)
        return map(lambda i: i.value, items)

    def items(self):
        items = frozenset.__iter__(self)
        return map(tuple, items)

    def copy(self):
        cls = self.__class__
        items = frozenset.copy(self)
        dupl = frozenset.__new__(cls, items)
        return dupl

    @classmethod
    def fromkeys(cls, keys, value):
        d = dict.fromkeys(keys,value)
        return cls(d)

    def __hash__(self):
        kv = tuple.__hash__
        items = frozenset.__iter__(self)
        return hash(frozenset(map(kv, items)))

    def __eq__(self, other):
        if not isinstance(other, FrozenDict):
            try:
                other = FrozenDict(other)
            except Exception:
                return False
        return frozenset.__eq__(self, other)

    def __ne__(self, other):
        return not self.__eq__(other)


if version == 2:
    #Here are the Python2 modifications
    class Python2(FrozenDict):
        def __iter__(self):
            items = frozenset.__iter__(self)
            for i in items:
                yield i.key

        def iterkeys(self):
            items = frozenset.__iter__(self)
            for i in items:
                yield i.key

        def itervalues(self):
            items = frozenset.__iter__(self)
            for i in items:
                yield i.value

        def iteritems(self):
            items = frozenset.__iter__(self)
            for i in items:
                yield (i.key, i.value)

        def has_key(self, key):
            return key in self

        def viewkeys(self):
            return dict(self).viewkeys()

        def viewvalues(self):
            return dict(self).viewvalues()

        def viewitems(self):
            return dict(self).viewitems()

    #If this is Python2, rebuild the class
    #from scratch rather than use a subclass
    py3 = FrozenDict.__dict__
    py3 = {k: py3[k] for k in py3}
    py2 = {}
    py2.update(py3)
    dct = Python2.__dict__
    py2.update({k: dct[k] for k in dct})

    FrozenDict = type('FrozenDict', (frozenset,), py2)

1
Зауважте, що ви також ліцензували його під CC BY-SA 3.0, розмістивши його тут. Принаймні, це перевага . Я здогадуюсь, що правовою основою для цього є погода на деякі T & C, коли ви вперше підписалися.
Євгеній Сергеєв

1
Я зламав мозок, намагаючись придумати спосіб пошуку ключового хешу без дикту. Перевизначення хеша Itemдля хеша ключа - це акуратний хак!
clacke

На жаль, тривалість запуску diff(diff({key}))досі залишається лінійною за розміром FrozenDict, тоді як звичайний час доступу до дікт є постійним у середньому випадку.
Денніс

6

Я думаю про frozendict кожного разу, коли я записую функцію, як це:

def do_something(blah, optional_dict_parm=None):
    if optional_dict_parm is None:
        optional_dict_parm = {}

6
Кожен раз, коли я бачу подібний коментар, я впевнений, що я кудись накрутив і поставив {} за замовчуванням, і повернусь назад і погляну на свій нещодавно написаний код.
Райан Гіберт

1
Так, рано чи пізно це стикається з усіма голодними гончами.
Марк Віссер

8
Легше формулювання:optional_dict_parm = optional_dict_parm or {}
Еммануель

2
У цьому випадку ви можете використовувати як аргумент значення за замовчуванням. types.MappingProxyType({})
GingerPlusPlus

@GingerPlusPlus Ви могли б це написати як відповідь?
jonrsharpe

5

Ви можете використовувати frozendict з utilspieпакету як:

>>> from utilspie.collectionsutils import frozendict

>>> my_dict = frozendict({1: 3, 4: 5})
>>> my_dict  # object of `frozendict` type
frozendict({1: 3, 4: 5})

# Hashable
>>> {my_dict: 4}
{frozendict({1: 3, 4: 5}): 4}

# Immutable
>>> my_dict[1] = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/mquadri/workspace/utilspie/utilspie/collectionsutils/collections_utils.py", line 44, in __setitem__
    self.__setitem__.__name__, type(self).__name__))
AttributeError: You can not call '__setitem__()' for 'frozendict' object

Відповідно до документа :

frozendict (dict_obj) : приймає obj типу dict та повертає dhable та незмінні dict



3

Так, це моя друга відповідь, але це зовсім інший підхід. Перша реалізація була в чистому пітоні. Цей у Cython. Якщо ви знаєте, як використовувати та компілювати модулі Cython, це так само швидко, як і звичайний словник. Приблизно .04 до .06 мікросекунди для отримання єдиного значення.

Це файл "rozen_dict.pyx "

import cython
from collections import Mapping

cdef class dict_wrapper:
    cdef object d
    cdef int h

    def __init__(self, *args, **kw):
        self.d = dict(*args, **kw)
        self.h = -1

    def __len__(self):
        return len(self.d)

    def __iter__(self):
        return iter(self.d)

    def __getitem__(self, key):
        return self.d[key]

    def __hash__(self):
        if self.h == -1:
            self.h = hash(frozenset(self.d.iteritems()))
        return self.h

class FrozenDict(dict_wrapper, Mapping):
    def __repr__(self):
        c = type(self).__name__
        r = ', '.join('%r: %r' % (k,self[k]) for k in self)
        return '%s({%s})' % (c, r)

__all__ = ['FrozenDict']

Ось файл "setup.py"

from distutils.core import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize('frozen_dict.pyx')
)

Якщо у вас встановлений Cython, збережіть два вище файли в одній і тій же директорії. Перейдіть до цього каталогу в командному рядку.

python setup.py build_ext --inplace
python setup.py install

І вам слід зробити.


3

Основним недоліком namedtupleє те, що його потрібно вказати перед його використанням, тому він менш зручний для випадків одноразового використання.

Однак є практичне рішення, яке можна використовувати для вирішення багатьох таких випадків. Скажімо, ви хочете мати незмінний еквівалент наступного дикту:

MY_CONSTANT = {
    'something': 123,
    'something_else': 456
}

Це можна наслідувати так:

from collections import namedtuple

MY_CONSTANT = namedtuple('MyConstant', 'something something_else')(123, 456)

Можна навіть написати додаткову функцію для автоматизації цього:

def freeze_dict(data):
    from collections import namedtuple
    keys = sorted(data.keys())
    frozen_type = namedtuple(''.join(keys), keys)
    return frozen_type(**data)

a = {'foo':'bar', 'x':'y'}
fa = freeze_dict(data)
assert a['foo'] == fa.foo

Звичайно, це працює лише для плоских диктів, але реалізувати рекурсивну версію не повинно.


1
Така ж проблема, як і в інших відповідях кортежу: getattr(fa, x)замість цього вам не потрібно робити fa[x]жодного keysметоду на кінчиках пальців, і всі інші причини відображення карти можуть бути бажаними.
Божевільний фізик

1

Підкласифікація dict

Я бачу цю картину в дикій природі (github) і хотів згадати її:

class FrozenDict(dict):
    def __init__(self, *args, **kwargs):
        self._hash = None
        super(FrozenDict, self).__init__(*args, **kwargs)

    def __hash__(self):
        if self._hash is None:
            self._hash = hash(tuple(sorted(self.items())))  # iteritems() on py2
        return self._hash

    def _immutable(self, *args, **kws):
        raise TypeError('cannot change object - object is immutable')

    __setitem__ = _immutable
    __delitem__ = _immutable
    pop = _immutable
    popitem = _immutable
    clear = _immutable
    update = _immutable
    setdefault = _immutable

Приклад використання:

d1 = FrozenDict({'a': 1, 'b': 2})
d2 = FrozenDict({'a': 1, 'b': 2})
d1.keys() 
assert isinstance(d1, dict)
assert len(set([d1, d2])) == 1  # hashable

Плюси

  • підтримка get(), keys(), items()( iteritems()на PY2) і все смакоту з dictз коробки без явного їх реалізації
  • використовує внутрішньо, dictщо означає продуктивність ( dictнаписано c в CPython)
  • елегантна проста і без чорної магії
  • isinstance(my_frozen_dict, dict)повертає True - хоча python заохочує введення качок для багатьох пакетів isinstance(), це може врятувати багато налаштувань та налаштувань

Мінуси

  • будь-який підклас може замінити це або отримати доступ до нього всередині (ви не можете дійсно на 100% захистити щось у python, ви повинні довіряти своїм користувачам і надати гарну документацію).
  • якщо ви дбаєте про швидкість, можливо, ви хочете зробити __hash__трохи швидше.

Я зробив порівняння швидкості в іншій нитці, і виявляється, переосмислення __setitem__та успадкування dictшалено швидко порівняно з багатьма альтернативами.
Тортується


0

Мені потрібно було в один момент отримати доступ до фіксованих клавіш для чогось, що було свого роду глобально-постійним видом, і я вирішив щось подібне:

class MyFrozenDict:
    def __getitem__(self, key):
        if key == 'mykey1':
            return 0
        if key == 'mykey2':
            return "another value"
        raise KeyError(key)

Використовуйте це як

a = MyFrozenDict()
print(a['mykey1'])

ПОПЕРЕДЖЕННЯ: Я не рекомендую це для більшості випадків використання, оскільки це призводить до досить серйозних компромісів.


Наступне було б рівне за потужністю без зменшення ефективності. Однак це лише спрощення прийнятої відповіді ... `` `клас FrozenDict: def __init __ (self, data): self._data = data def __getitem __ (self, key): return self._data [key]` ` `
Ювал

@Yuval, що відповідь не рівнозначна. Для початку api відрізняється тим, що йому потрібні дані для init. Це також означає, що він більше не доступний у всьому світі. Крім того, якщо _data вимкнено, ваше повернене значення змінюється. Я знаю, що є значні компроміси - як я вже сказав, я не рекомендую це для більшості випадків використання.
Адже

-1

За відсутності підтримки рідної мови ви можете це зробити самостійно або використовувати існуюче рішення. На щастя, Python дозволяє просто продовжувати базова реалізація.

class frozen_dict(dict):
    def __setitem__(self, key, value):
        raise Exception('Frozen dictionaries cannot be mutated')

frozen_dict = frozen_dict({'foo': 'FOO' })
print(frozen['foo']) # FOO
frozen['foo'] = 'NEWFOO' # Exception: Frozen dictionaries cannot be mutated

# OR

from types import MappingProxyType

frozen_dict = MappingProxyType({'foo': 'FOO'})
print(frozen_dict['foo']) # FOO
frozen_dict['foo'] = 'NEWFOO' # TypeError: 'mappingproxy' object does not support item assignment

Ваш клас замороженого вироку не доступний
miracle173
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.