Пользовательские словари Python: наследование от dict против UserDict

Оглавление

Создание классов, похожих на словари, может быть обязательным требованием в вашей карьере на Python. В частности, вы можете быть заинтересованы в создании пользовательских словарей с измененным поведением, новыми функциональными возможностями или и тем, и другим. В Python вы можете сделать это, наследуя от абстрактного базового класса , непосредственно создавая подклассы встроенного класса dict или наследуя от UserDict.

В этом руководстве вы узнаете, как:

  • Создавайте классы, подобные словарю, наследуя от встроенного dict класса
  • Определите типичные ошибки, которые могут возникнуть при наследовании от dict
  • Создавать словарные классы с помощью создания подклассов UserDict из модуля collections

Кроме того, вы закодируете несколько примеров, которые помогут вам понять плюсы и минусы использования dict и UserDict для создания пользовательских словарных классов.

Чтобы извлечь максимальную пользу из этого руководства, вы должны быть знакомы со встроенным классом Python dict и его стандартными функциональными возможностями. Вам также необходимо знать основы объектно-ориентированного программирования и понимать, как наследование работает в Python.

Создание словарных классов в Python

Встроенный класс dict предоставляет ценный и универсальный тип данных для сбора данных - словарь Python . Словари есть везде, в том числе в вашем коде и в коде самого Python.

Иногда стандартной функциональности словарей Python недостаточно для определенных случаев использования. В таких ситуациях вам, вероятно, придется создать пользовательский класс, подобный словарю. Другими словами, вам нужен класс, который ведет себя как обычный словарь, но с измененной или новой функциональностью.

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

  1. Расширение обычного словаря за счет добавления новых функциональных возможностей
  2. Изменение функциональности стандартного словаря

Обратите внимание, что вы также можете столкнуться с ситуациями, в которых вам потребуется расширить и стандартные функциональные возможности словаря.

В зависимости от ваших конкретных потребностей и уровня квалификации вы можете выбрать одну из нескольких стратегий создания пользовательских словарей. Вы можете:

  • Наследуется от соответствующего абстрактного базового класса, такого как MutableMapping
  • Наследуется непосредственно от встроенного класса Python dict
  • Подкласс UserDict из collections

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

Создание класса, подобного словарю, на основе абстрактного Базового Класса

Эта стратегия создания словарных классов требует, чтобы вы наследовали от абстрактного базового класса (ABC), например MutableMapping. Этот класс предоставляет конкретные общие реализации всех методов словаря, за исключением .__getitem__(), .__setitem__(), .__delitem__(), .__iter__(), и .__len__(), которые вам придется реализовать самостоятельно.

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

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

Основное преимущество этой стратегии заключается в том, что родительский ABC предупредит вас, если вы пропустите какой-либо метод в своей пользовательской реализации.

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

В этом руководстве вы сосредоточитесь на создании словарных классов путем наследования от встроенного класса dict и класса UserDict, которые, по-видимому, являются самыми быстрыми и практичными стратегиями.

Наследование от встроенного в Python Класса dict

Долгое время было невозможно классифицировать типы Python, реализованные в C. В Python 2.2 исправлена эта проблема. Теперь вы можете непосредственно создавать подклассы встроенных типов, включая dict. Это изменение дает подклассам несколько технических преимуществ, поскольку теперь они:

Первый пункт в этом списке может быть требованием к коду на C, который ожидает наличия встроенного класса на Python. Второй пункт позволяет добавить новые функциональные возможности в дополнение к стандартному поведению словаря. Наконец, третий пункт позволит вам ограничить атрибуты подкласса только теми атрибутами, которые предопределены в .__slots__.

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

Для этого вы можете создать подкласс dict, который переопределяет метод .__setitem__():

>>> class UpperCaseDict(dict):
...     def __setitem__(self, key, value):
...         key = key.upper()
...         super().__setitem__(key, value)
...

>>> numbers = UpperCaseDict()
>>> numbers["one"] = 1
>>> numbers["two"] = 2
>>> numbers["three"] = 3

>>> numbers
{'ONE': 1, 'TWO': 2, 'THREE': 3}

Круто! Похоже, ваш пользовательский словарь работает хорошо. Однако в этом классе есть некоторые скрытые проблемы. Если вы попытаетесь создать экземпляр UpperCaseDict, используя некоторые данные инициализации, то получите неожиданное и вызывающее ошибки поведение:

>>> numbers = UpperCaseDict({"one": 1, "two": 2, "three": 3})
>>> numbers
{'one': 1, 'two': 2, 'three': 3}

Что только что произошло? Почему ваш словарь не преобразует ключи в заглавные буквы при вызове конструктора класса ? Похоже, что инициализатор класса, .__init__(), не вызывает .__setitem__() неявно для создания словаря. Таким образом, преобразование в верхний регистр никогда не выполняется.

К сожалению, эта проблема затрагивает другие методы работы со словарем, такие как .update() и .setdefault(), например:

>>> numbers = UpperCaseDict()
>>> numbers["one"] = 1
>>> numbers["two"] = 2
>>> numbers["three"] = 3

>>> numbers
{'ONE': 1, 'TWO': 2, 'THREE': 3}

>>> numbers.update({"four": 4})
>>> numbers
{'ONE': 1, 'TWO': 2, 'THREE': 3, 'four': 4}

>>> numbers.setdefault("five", 5)
5
>>> numbers
{'ONE': 1, 'TWO': 2, 'THREE': 3, 'four': 4, 'five': 5}

Опять же, в этих примерах ваша функциональность в верхнем регистре работает неправильно. Чтобы решить эту проблему, вы должны предоставить пользовательские реализации всех затронутых методов. Например, чтобы устранить проблему с инициализацией, вы можете написать метод .__init__(), который выглядит примерно так:

# upper_dict.py

class UpperCaseDict(dict):
    def __init__(self, mapping=None, /, **kwargs):
        if mapping is not None:
            mapping = {
                str(key).upper(): value for key, value in mapping.items()
            }
        else:
            mapping = {}
        if kwargs:
            mapping.update(
                {str(key).upper(): value for key, value in kwargs.items()}
            )
        super().__init__(mapping)

    def __setitem__(self, key, value):
        key = key.upper()
        super().__setitem__(key, value)

Здесь .__init__() преобразует ключи в заглавные буквы и затем инициализирует текущий экземпляр полученными данными.

С этим обновлением процесс инициализации вашего пользовательского словаря должен работать корректно. Попробуйте, запустив следующий код:

>>> from upper_dict import UpperCaseDict

>>> numbers = UpperCaseDict({"one": 1, "two": 2, "three": 3})
>>> numbers
{'ONE': 1, 'TWO': 2, 'THREE': 3}

>>> numbers.update({"four": 4})
>>> numbers
{'ONE': 1, 'TWO': 2, 'THREE': 3, 'four': 4}

При использовании собственного метода .__init__() устранена проблема с инициализацией. Однако другие методы, такие как .update(), продолжают работать некорректно, как вы можете заключить из того, что клавиша "four" не является прописной.

Почему dict подклассы ведут себя подобным образом? Встроенные типы были разработаны и реализованы с учетом принципа "открыто–закрыто". Таким образом, они открыты для расширения, но закрыты для модификации. Разрешение вносить изменения в основные функции этих классов потенциально может нарушить их инварианты. Поэтому разработчики ядра Python решили защитить их от изменений.

Вот почему создание подклассов встроенного класса dict может быть немного сложным, трудоемким и чреватым ошибками делом. К счастью, у вас все еще есть альтернативы. Класс UserDict из модуля collections является одним из них.

Подкласс UserDict Из collections

Начиная с версии Python 1.6, язык предоставляет UserDict как часть стандартной библиотеки. Этот класс изначально находился в модуле, названном в честь самого класса. В Python 3 UserDict был перенесен в модуль collections, который является более интуитивно понятным местом для этого, исходя из основного назначения класса.

UserDict был создан еще тогда, когда невозможно было напрямую наследовать от Python dict. Несмотря на то, что необходимость в этом классе была частично устранена возможностью прямого создания подклассов встроенного класса dict, UserDict по-прежнему доступен в стандартной библиотеке, как для удобства, так и для обеспечения обратной совместимости.

UserDict это удобная оболочка для обычного объекта dict. Этот класс обеспечивает то же поведение, что и встроенный тип данных dict, с дополнительной возможностью предоставления вам доступа к базовому словарю через атрибут .data instance. Эта функция может облегчить создание пользовательских классов, подобных словарю, о чем вы узнаете позже в этом руководстве.

UserDict был специально разработан для создания подклассов, а не для прямого создания экземпляров, что означает, что основная цель класса - позволить вам создавать классы, подобные словарю, с помощью наследования.

Есть и другие скрытые отличия. Чтобы обнаружить их, вернитесь к исходной реализации UpperCaseDict и обновите ее, как показано в приведенном ниже коде:

>>> from collections import UserDict

>>> class UpperCaseDict(UserDict):
...     def __setitem__(self, key, value):
...         key = key.upper()
...         super().__setitem__(key, value)
...

На этот раз вместо наследования от dict вы наследуете от UserDict, который вы импортировали из модуля collections. Как это изменение повлияет на поведение вашего класса UpperCaseDict? Ознакомьтесь со следующими примерами:

>>> numbers = UpperCaseDict({"one": 1, "two": 2})

>>> numbers["three"] = 3
>>> numbers.update({"four": 4})
>>> numbers.setdefault("five", 5)
5

>>> numbers
{'ONE': 1, 'TWO': 2, 'THREE': 3, 'FOUR': 4, 'FIVE': 5}

Теперь UpperCaseDict всегда работает корректно. Вам не нужно предоставлять пользовательские реализации .__init__(), .update(), или .setdefault(). Класс просто работает! Это связано с тем, что в UserDict все методы, которые обновляют существующие ключи или добавляют новые, последовательно зависят от вашей версии .__setitem__().

Как вы узнали ранее, наиболее заметным различием между UserDict и dict является атрибут .data, который содержит обернутый словарь. Прямое использование .data может сделать ваш код более простым, поскольку вам не нужно постоянно вызывать super() для обеспечения желаемой функциональности. Вы можете просто получить доступ к .data и работать с ним так же, как с любым обычным словарем.

Классы, похожие на словари для кодирования: практические примеры

Вы уже знаете, что подклассы dict не вызывают .__setitem__() из таких методов, как .update() и .__init__(). Этот факт заставляет подклассы dict вести себя иначе, чем типичный класс Python с .__setitem__() методом.

Чтобы обойти эту проблему, вы можете наследовать от UserDict, который вызывает .__setitem__() из всех операций, которые устанавливают или обновляют значения в базовом словаре. Благодаря этой функции UserDict можно сделать ваш код более безопасным и компактным.

По общему признанию, когда вы думаете о создании класса, подобного словарю, наследование от dict более естественно, чем наследование от UserDict. Это происходит потому, что все разработчики Python знают о dict, но не все разработчики Python знают о существовании UserDict.

Наследование от dict часто влечет за собой определенные проблемы, которые, вероятно, можно устранить, используя вместо этого UserDict. Однако эти проблемы не всегда актуальны. Их актуальность во многом зависит от того, как вы хотите настроить функциональность словаря.

Суть в том, что UserDict не всегда является правильным решением. В общем, если вы хотите расширить стандартный словарь, не затрагивая его основную структуру, то вполне допустимо наследовать от dict. С другой стороны, если вы хотите изменить поведение основного словаря, переопределив его специальные методы, то UserDict - это ваша лучшая альтернатива.

В любом случае, помните, что dict написан на C и очень оптимизирован для повышения производительности. В то же время, UserDict написан на чистом Python, что может представлять собой существенное ограничение с точки зрения производительности.

При принятии решения о наследовании от dict или UserDict следует учитывать несколько факторов. Эти факторы включают, но не ограничиваются ими, следующие:

  • Объем работы
  • Риск возникновения ошибок и багов
  • Простота использования и кодирования
  • Производительность

В следующем разделе вы познакомитесь с первыми тремя факторами из этого списка, приведя несколько практических примеров. О влиянии на производительность вы узнаете чуть позже, в разделе, посвященном производительности.

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

В качестве первого примера предположим, что вам нужен словарь, который хранит ключи на американском английском и позволяет выполнять поиск ключей как на американском, так и на британском английском. Чтобы закодировать этот словарь, вам потребуется изменить как минимум два специальных метода , .__setitem__() и .__getitem__().

Метод .__setitem__() позволит вам всегда сохранять ключи на американском английском. Метод .__getitem__() позволит получить значение, связанное с данным ключом, независимо от того, написано ли оно на американском или британском английском языке.

Поскольку вам необходимо изменить основное поведение класса dict, использование UserDict было бы лучшим вариантом для кодирования этого класса. С помощью UserDict вам не нужно будет предоставлять пользовательские реализации .__init__(), .update(), и так далее.

Когда вы создаете подкласс UserDict, у вас есть два основных способа кодирования вашего класса. Вы можете положиться на атрибут .data, который может облегчить кодирование, или вы можете положиться на super() и специальные методы.

Вот код, который основан на .data:

# spelling_dict.py

from collections import UserDict

UK_TO_US = {"colour": "color", "flavour": "flavor", "behaviour": "behavior"}

class EnglishSpelledDict(UserDict):
    def __getitem__(self, key):
        try:
            return self.data[key]
        except KeyError:
            pass
        try:
            return self.data[UK_TO_US[key]]
        except KeyError:
            pass
        raise KeyError(key)

    def __setitem__(self, key, value):
        try:
            key = UK_TO_US[key]
        except KeyError:
            pass
        self.data[key] = value

В этом примере вы сначала определяете константу UK_TO_US, содержащую британские слова в качестве ключей и соответствующие американские слова в качестве значений.

Затем вы определяете EnglishSpelledDict, наследуя от UserDict. Метод .__getitem__() выполняет поиск текущего ключа. Если ключ существует, то метод возвращает его. Если ключ не существует, то метод проверяет, был ли ключ написан на британском английском. Если это так, то ключ переводится на американский английский и извлекается из базового словаря.

Метод .__setitem__() пытается найти ключ ввода в словаре UK_TO_US. Если ключ ввода существует в UK_TO_US, то он переводится на американский английский. Наконец, метод присваивает входные данные value целевому объекту key.

Вот как работает ваш класс EnglishSpelledDict на практике:

>>> from spelling_dict import EnglishSpelledDict

>>> likes = EnglishSpelledDict({"color": "blue", "flavour": "vanilla"})

>>> likes
{'color': 'blue', 'flavor': 'vanilla'}

>>> likes["flavour"]
vanilla
>>> likes["flavor"]
vanilla

>>> likes["behaviour"] = "polite"
>>> likes
{'color': 'blue', 'flavor': 'vanilla', 'behavior': 'polite'}

>>> likes.get("colour")
'blue'
>>> likes.get("color")
'blue'

>>> likes.update({"behaviour": "gentle"})
>>> likes
{'color': 'blue', 'flavor': 'vanilla', 'behavior': 'gentle'}

Создавая подклассы UserDict, вы избавляете себя от написания большого количества кода. Например, вам не нужно предоставлять такие методы, как .get(), .update(), или .setdefault(), потому что их реализации по умолчанию будут автоматически зависеть от ваших методов .__getitem__() и .__setitem__() .

Если вам нужно написать меньше кода, то у вас будет меньше работы. Что еще более важно, вы будете в большей безопасности, потому что меньшее количество кода часто влечет за собой меньший риск ошибок.

Примечание: Если вам нужно, чтобы ключевое слово del работало с обоими вариантами написания, вам придется реализовать пользовательский метод .__delitem__() в вашем словаре EnglishSpelledDict. Аналогично, если вы хотите, чтобы тестов на членство работали с обоими вариантами написания, вам придется переопределить метод .__contains__().

Основным недостатком этой реализации является то, что если вы когда-нибудь решите обновить EnglishSpelledDict и сделать его наследуемым от dict, то вам придется переписать большую часть кода, чтобы исключить использование .data.

В приведенном ниже примере показано, как обеспечить ту же функциональность, что и раньше, используя super() и некоторые специальные методы. На этот раз ваш пользовательский словарь полностью совместим с dict, так что вы можете изменить родительский класс в любое удобное для вас время:

# spelling_dict.py

from collections import UserDict

UK_TO_US = {"colour": "color", "flavour": "flavor", "behaviour": "behavior"}

class EnglishSpelledDict(UserDict):
    def __getitem__(self, key):
        try:
            return super().__getitem__(key)
        except KeyError:
            pass
        try:
            return super().__getitem__(UK_TO_US[key])
        except KeyError:
            pass
        raise KeyError(key)

    def __setitem__(self, key, value):
        try:
            key = UK_TO_US[key]
        except KeyError:
            pass
        super().__setitem__(key, value)

Эта реализация выглядит немного иначе, чем оригинальная, но работает так же. Кроме того, ее может быть сложнее кодировать, потому что вы больше не используете .data. Вместо этого вы используете super(), .__getitem__(), и .__setitem__(). Этот код требует определенных знаний о модели данных Python, которая является сложной и продвинутой темой.

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

Примечание: Помните, что если вы наследуете напрямую от dict, то вам нужно переопределить .__init__() и другие методы, чтобы они также переводили ключи в Американское написание при добавлении ключей в словарь.

Часто бывает удобнее расширить функциональность стандартного словаря, создав подкласс UserDict, чем подкласс dict. Основная причина заключается в том, что встроенный dict имеет некоторые упрощения в реализации и оптимизации, которые в конечном итоге вынуждают вас переопределять методы, которые вы можете просто унаследовать, если используете UserDict в качестве родительского класса.

Словарь, Который Обращается К Ключам Через Значения

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

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

Вот возможная реализация этого пользовательского словаря:

# value_dict.py

class ValueDict(dict):
    def key_of(self, value):
        for k, v in self.items():
            if v == value:
                return k
        raise ValueError(value)

    def keys_of(self, value):
        for k, v in self.items():
            if v == value:
                yield k

На этот раз, вместо наследования от UserDict, вы наследуете от dict. Почему? В этом примере вы добавляете функциональность, которая не изменяет основные возможности словаря. Поэтому более подходящим является наследование от dict. Это также более эффективно с точки зрения производительности, как вы увидите позже в этом руководстве.

Метод .key_of() выполняет итерацию по парам ключ-значение в базовом словаре. Условный оператор проверяет наличие значений, соответствующих целевому значению. Блок кода if возвращает ключ с первым совпадающим значением. Если целевой ключ отсутствует, то метод генерирует запрос. ValueError.

Как метод генератора, который выдает ключи по запросу, .keys_of() выдаст только те ключи, значение которых соответствует value, указанному в качестве аргумента в вызов метода.

Вот как этот словарь работает на практике:

>>> from value_dict import ValueDict

>>> inventory = ValueDict()
>>> inventory["apple"] = 2
>>> inventory["banana"] = 3
>>> inventory.update({"orange": 2})

>>> inventory
{'apple': 2, 'banana': 3, 'orange': 2}

>>> inventory.key_of(2)
'apple'
>>> inventory.key_of(3)
'banana'

>>> list(inventory.keys_of(2))
['apple', 'orange']

Круто! Ваш словарь ValueDict работает так, как ожидалось. Он наследует основные функции словаря от dict Python и реализует новые функциональные возможности в дополнение к этому.

В общем, вам следует использовать UserDict для создания класса, подобного словарю, который действует подобно встроенному классу dict, но настраивает некоторые его основные функциональные возможности, в основном специальные методы, такие как .__setitem__() и .__getitem__().

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

Словарь С Дополнительными Функциональными Возможностями

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

Метод Описание
.apply(action) Принимает в качестве аргумента вызываемое действие и применяет его ко всем значениям в базовом словаре.
.remove(key) Удаляет заданный ключ из базового словаря.
.is_empty() Возвращает True или False в зависимости от того, пуст словарь или нет.

Для реализации этих трех методов вам не нужно изменять основное поведение встроенного класса dict. Таким образом, создание подкласса dict вместо UserDict, по-видимому, является правильным решением.

Вот код, который реализует необходимые методы поверх dict:

# extended_dict.py

class ExtendedDict(dict):
    def apply(self, action):
        for key, value in self.items():
            self[key] = action(value)

    def remove(self, key):
        del self[key]

    def is_empty(self):
        return len(self) == 0

В этом примере .apply() принимает вызываемый параметр в качестве аргумента и применяет его к каждому значению в базовом словаре. Затем преобразованному значению присваивается исходный ключ. Метод .remove() использует инструкцию del для удаления целевого ключа из словаря. Наконец, .is_empty() использует встроенную функцию len(), чтобы узнать, пуст словарь или нет.

Вот как работает ExtendedDict:

>>> from extended_dict import ExtendedDict

>>> numbers = ExtendedDict({"one": 1, "two": 2, "three": 3})
>>> numbers
{'one': 1, 'two': 2, 'three': 3}

>>> numbers.apply(lambda x: x**2)
>>> numbers
{'one': 1, 'two': 4, 'three': 9}

>>> numbers.remove("two")
>>> numbers
{'one': 1, 'three': 9}

>>> numbers.is_empty()
False

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

Затем .remove() принимает существующий ключ в качестве аргумента и удаляет соответствующую пару ключ-значение из словаря. Наконец, .is_empty() возвращает False, потому что numbers не является пустым. Он вернул бы True, если бы базовый словарь был пустым.

Учитывая производительность

Наследование от UserDict может привести к снижению производительности, поскольку этот класс написан на чистом Python. С другой стороны, встроенный класс dict написан на C и высоко оптимизирован для повышения производительности. Итак, если вам нужно использовать пользовательский словарь в коде, критически важном для производительности, то обязательно уделите времени своему коду, чтобы найти потенциальные проблемы с производительностью.

Чтобы проверить, могут ли возникнуть проблемы с производительностью при наследовании от UserDict вместо dict, вернитесь к своему классу ExtendedDict и скопируйте его код в два разных класса, один из которых наследуется от dict а другой, унаследованный от UserDict.

Ваши классы должны выглядеть примерно так:

# extended_dicts.py

from collections import UserDict

class ExtendedDict_dict(dict):
    def apply(self, action):
        for key, value in self.items():
            self[key] = action(value)

    def remove(self, key):
        del self[key]

    def is_empty(self):
        return len(self) == 0

class ExtendedDict_UserDict(UserDict):
    def apply(self, action):
        for key, value in self.items():
            self[key] = action(value)

    def remove(self, key):
        del self[key]

    def is_empty(self):
        return len(self) == 0

Единственное различие между этими двумя классами заключается в том, что ExtendedDict_dict подклассы dict и ExtendedDict_UserDict подклассы UserDict.

Чтобы проверить их производительность, вы можете начать с определения времени выполнения основных операций со словарем, таких как создание экземпляра класса. Запустите следующий код в своем интерактивном сеансе на Python:

>>> import timeit
>>> from extended_dicts import ExtendedDict_dict
>>> from extended_dicts import ExtendedDict_UserDict

>>> init_data = dict(zip(range(1000), range(1000)))

>>> dict_initialization = min(
...     timeit.repeat(
...         stmt="ExtendedDict_dict(init_data)",
...         number=1000,
...         repeat=5,
...         globals=globals(),
...     )
... )

>>> user_dict_initialization = min(
...     timeit.repeat(
...         stmt="ExtendedDict_UserDict(init_data)",
...         number=1000,
...         repeat=5,
...         globals=globals(),
...     )
... )

>>> print(
...     f"UserDict is {user_dict_initialization / dict_initialization:.3f}",
...     "times slower than dict",
... )
UserDict is 35.877 times slower than dict

В этом фрагменте кода вы используете модуль timeit вместе с функцией min() для измерения времени выполнения фрагмента кода. В этом примере целевой код состоит из создания экземпляра ExtendedDict_dict и ExtendedDict_UserDict.

После выполнения этого кода для измерения времени вы сравниваете оба времени инициализации. В этом конкретном примере инициализация класса, основанного на UserDict, выполняется медленнее, чем у класса, производного от dict. Этот результат является показателем серьезной разницы в производительности.

Измерение времени выполнения новых функций также может быть интересным. Например, вы можете проверить время выполнения .apply(). Чтобы выполнить эту проверку, выполните следующий код:

>>> extended_dict = ExtendedDict_dict(init_data)
>>> dict_apply = min(
...     timeit.repeat(
...         stmt="extended_dict.apply(lambda x: x**2)",
...         number=5,
...         repeat=2,
...         globals=globals(),
...     )
... )

>>> extended_user_dict = ExtendedDict_UserDict(init_data)
>>> user_dict_apply = min(
...     timeit.repeat(
...         stmt="extended_user_dict.apply(lambda x: x**2)",
...         number=5,
...         repeat=2,
...         globals=globals(),
...     )
... )

>>> print(
...     f"UserDict is {user_dict_apply / dict_apply:.3f}",
...     "times slower than dict",
... )
UserDict is 1.704 times slower than dict

Разница в производительности между классом, основанным на UserDict, и классом, основанным на dict, на этот раз не такая большая, но она все еще существует.

Часто, когда вы создаете пользовательский словарь с помощью подкласса dict, вы можете ожидать, что стандартные операции со словарем будут более эффективными в этом классе, чем в классе, основанном на UserDict. С другой стороны, время выполнения новой функциональности в обоих классах может быть одинаковым. Как бы вы узнали, какой способ наиболее эффективен? Что ж, вам нужно учитывать время выполнения вашего кода.

Стоит отметить, что если вы хотите изменить основные функциональные возможности словаря, то, вероятно, лучше всего использовать UserDict, потому что в этом случае вам придется в основном переписывать класс dict на чистом Python.

Заключение

Теперь вы знаете, как создавать пользовательские классы, подобные словарю, с измененным поведением и новыми функциональными возможностями. Вы научились делать это, создав подкласс непосредственно из встроенного класса dict и унаследовав его от класса UserDict, доступного в модуле collections.

В этом руководстве вы узнали, как:

  • Создавайте классы, подобные словарю, наследуя от встроенного dict класса
  • Определите типичные ошибки при наследовании встроенного в Python dict класса
  • Создавать классы, подобные словарю, путем создания подклассов UserDict из модуля collections

Вы также написали несколько практических примеров, которые помогли вам понять плюсы и минусы использования UserDict против dict при создании пользовательских словарных классов.

Теперь вы готовы создавать свои пользовательские словари и использовать всю мощь этого полезного типа данных в Python в соответствии с вашими потребностями в программировании.

Back to Top