В данной главе мы рассмотрим множества и словари — основу всех ключевых объектов в Python. Огромное количество системных структур и параметров, использующихся в языке, построены именно на словарях.
Быстрый поиск
Хеш-функция
Основу словарей и множеств задаёт структура данных «хеш-таблица». Поэтому для начала обратимся к непосредственному изучению именно хеш-функции.
Основные свойства, которым должна удовлетворять хеш-функция:
- Однозначность — для одинаковых входных данных должны получаться одинаковые хеши;
- Перевод области определения в актуально меньшую область значений.
Все остальные свойства — взаимооднозначность, равномерность, невосстановимость и т. п. — необязательны, зависят от применения, и не всегда достижимы.
В случае Python можно выделить такие особенности хеш-функции:
- + Достаточно высокая скорость вычисления. Вообще говоря, вычисление хеша производится единожды, после чего значение просто хранится в данных объекта. Причём с недавнего времени значение хеша зависит от некоторых внешних параметров, в частности, от случайно генерируемого параметра окружения. Это необходимо для невозможности подбора хеша перебором. Однако при явном задании данного параметра возможно получать одинаковые значения для одних и тех же объектов:
[user@userPC: ~] $ PYTHONHASHSEED=100500 python3.13 Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813] on linux Type "help", "copyright", "credits" or "license" for more information. >>> hash("qwer") 5546997520659696577 >>> [user@userPC: ~] $ PYTHONHASHSEED=42 python3.13 Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813] on linux Type "help", "copyright", "credits" or "license" for more information. >>> hash("qwer") 7251206384522659612 >>> [user@userPC: ~] $ PYTHONHASHSEED=100500 python3.13 Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813] on linux Type "help", "copyright", "credits" or "license" for more information. >>> hash("qwer") 5546997520659696577 >>> - + Фиксированный размер — 64-битное целое
- ± Актуальное неравенство для «почти похожих»: например, для чисел из диапазона, совпадающих с точностью до символа строк и т. п.
>>> hash("qwerty") 2586875249310804606 >>> hash("qwertu") 44477320390538267 >>> - ? Взаимооднозначности в хеше Python нет. Как минимум, это нарушает строгость перевода в меньшую область значений. Взаимооднозначность желательна для какого-то фиксированного набора элементов, с которыми мы хотим работать ( для минимизации коллизий ), но повлиять на этот параметр никак отдельно нельзя. К слову, пронаблюдать коллизии совсем просто, не обязательно для этого перебирать множество объектов в надежде, что их хеш совпадёт, пример лежит совсем недалеко от нуля:
>>> hash(-2) -2 >>> hash(-1) -2 >>> - - Несмотря на неравенство хешей почти похожих объектов, разброс значений для них явно не требуется (поскольку хеш не используется для каких-то криптографических целей, например, генерации ключей, а служит исключительно для некоторого унифицированного определения объекта):
>>> hash(123456) 123456 >>> hash(123457) 123457 >>> - - Описанный выше пример наглядно демонстрирует, что невосстановимость объекта по его хешу в Python явно не требуется.
Основное использование хеша объектов — предварительное сравнение. Например, поэлементное сравнение строк имеет линейную сложность. Однако предварительное сравнение хешей объектов явно укажет на их неравенство. И лишь в случае совпадения хешей необходимо будет, действительно, проводить поэлементное сравнение (поскольку хеши для разных объектов всё же могли совпасть).
Поскольку хеш не вычисляется каждый раз, а хранится с момента создания объекта, легко понять, что он есть только у константных объектов. Для работы с хешем используется специальный метод константного объекта __hash__
>>> hash("qwqefgsdhdnm")
-737543193532195808
>>> h = hash("qwqefgsdhdnm")
>>> h.bit_length()
60
>>> ("qwer").__hash__()
-6608975883897917595
>>>
Последний и вполне логичный вопрос, возникающий при разговоре о хеш-функции: «Почему в качестве неё не используется id?». Да, это также уникальный числовой идентификатор, присущий вообще любому объекту («Даже модифицирующемуся! Явно лучше хеша!»), однако в нём нарушается основное требование хеш-функции — однозначность:
>>> a = 100500
>>> b = 100500
>>>
>>> id(a), id(b)
(140661950981744, 140661950978448)
>>> hash(a), hash(b)
(100500, 100500)
>>>
>>> a == b
True
>>> hash(a) == hash(b)
True
>>>
>>> a is b
False
>>> id(a) == id(b)
False
>>>
Множество
Простейший объект, построенный на основе хеш-таблицы — множество. Его основная особенность — поиск элемента за константное время.
>>> s = {123456, "qwe", ()}
>>> s
{123456, 'qwe', ()}
>>> 123 in s
False
>>> 123456 in s
True
>>>
Множество может хранить только хешируемые (а, значит, строго константные) объекты, распределяя их согласно некоторой функции от значения хеша объекта и собственных параметров (размера таблицы, её заполненности и т.д.). В простейшем случае эта функция представляет из себя просто взятие остатка от деления на размер таблицы значения хеша объекта. При этом не исключено, что позиция может быть занята. В таком случае производится пересчёт позиции по некоторым встроенным формулам. В случае заполнения таблицы примерно на две трети, происходит геометрическое масштабирование таблицы по достаточно непростой схеме. Точные формулы пересчёта этих значений можно найти в исходных текстах самого Python.
>>> from math import log2
>>> sz, s = {}, set()
>>> for i in range(1, 204800):
... s.add(str(i)*2)
... sz[s.__sizeof__()] = i
...
>>> len(s)
204799
>>> s.__sizeof__()
8388808
>>> sz
{200: 4, 712: 18, 2248: 76, 8392: 306, 32968: 1228, 131272: 4914, 524488: 19660, 2097352: 78642, 419450
4: 157285, 8388808: 204799}
>>> 4194504 / (157285 * 16)
1.6667609752996153
>>>
Множества могут быть созданы из любой последовательности, и, как и большинство объектов Python, поддерживают циклические конструкторы.
>>> {i*2 + 1 for i in range(8) if i != 6}
{1, 3, 5, 7, 9, 11, 15}
>>> set("qweqwewqeweqweqwew")
{'e', 'w', 'q'}
>>>
Над множествами реализованы отдельный набор операций, позволяющих быстро производить типичные для них действия:
- Пересечение множеств,
- Объединение множеств,
- Симметрическая разность (элементы, принадлежащие строго одному из множеств),
- Разность множеств (множество-уменьшаемое без элементов, которые есть во множестве-вычитаемом)
Для всех указанных операций есть и их аугментные варианты.
>>> a, b = set(range(4)), set(range(2, 7))
>>> a
{0, 1, 2, 3}
>>> b
{2, 3, 4, 5, 6}
>>> a & b
{2, 3}
>>> a | b
{0, 1, 2, 3, 4, 5, 6}
>>> a ^ b
{0, 1, 4, 5, 6}
>>> a - b
{0, 1}
>>> b - a
{4, 5, 6}
>>>
Элементы множества упорядочены по хешам, а не по добавлению в множество:
>>> a = {"QWER", "ADSF", 100}
>>> a
{'ADSF', 100, 'QWER'}
>>> for i in a:
... print(f"{i}: {hash(i)}")
...
ADSF: 7621957227430526177
100: 100
QWER: -6426309142167868195
>>>
Множества сами по себе являются изменяемыми объектами, однако в Python существует вариант константного множества — frozenset. И, поскольку оно константное, его можно использовать в качестве элемента другого множества:
>>> a = {100, "qwe", (1, 2, 3)}
>>> b = {"QQ", a}
Traceback (most recent call last):
File "<python-input-8>", line 1, in <module>
b = {"QQ", a}
^^^^^^^^^
TypeError: unhashable type: 'set'
>>> c = frozenset(a)
>>> c
frozenset({(1, 2, 3), 100, 'qwe'})
>>> b = {"QQ", c}
>>> b
{'QQ', frozenset({(1, 2, 3), 100, 'qwe'})}
>>>
Словарь
Словарь — самый главный объект в Python. Отличие словаря от множества —возможность хранения не только ключа, по которому и ведётся распределение по хеш-таблице, но и любого произвольного объекта-значения.
>>> d = {}
>>> d["qwer"] = 100500
>>> d
{'qwer': 100500}
>>>
До Python3.6 словарь реализовывался на основе множества с ещё одной колонкой, отвечающей за значение, в структуре. Схематично хеш-таблицу словаря можно было представить так:
entries = [['--', '--', '--'],
[-8522787127447073495, 'barry', 'green'],
['--', '--', '--'],
['--', '--', '--'],
['--', '--', '--'],
[-9092791511155847987, 'timmy', 'red'],
['--', '--', '--'],
[-6480567542315338377, 'guido', 'blue']]
Данное решение было очень неоптимизировано. Решение пришло из PyPy — интерпретатора Python, написанного на Python. Там разработчики предложили разбить структуру на две: отдельно хранить список индексов объектов согласно распределению по хеш-таблице, а данные объектов хранить другим плотно упакованным списком:
indices = [None, 1, None, None, None, 0, None, 2]
entries = [[-9092791511155847987, 'timmy', 'red'],
[-8522787127447073495, 'barry', 'green'],
[-6480567542315338377, 'guido', 'blue']]
Такая реализация позволила не просто оптимизировать память и скорость доступа к элементам, но и внесла новую логику упорядочения элементов — не по хешу, а по принципу добавления в словарь
>>> d = {i*2 + 1: f"{i}" for i in range(24)}
>>> d
{1: '0', 3: '1', 5: '2', 7: '3', 9: '4', 11: '5', 13: '6', 15: '7', 17: '8', 19: '9', 21: '10', 23: '11
', 25: '12', 27: '13', 29: '14', 31: '15', 33: '16', 35: '17', 37: '18', 39: '19', 41: '20', 43: '21',
45: '22', 47: '23'}
>>> d[100] = 100500
>>> d
{1: '0', 3: '1', 5: '2', 7: '3', 9: '4', 11: '5', 13: '6', 15: '7', 17: '8', 19: '9', 21: '10', 23: '11
', 25: '12', 27: '13', 29: '14', 31: '15', 33: '16', 35: '17', 37: '18', 39: '19', 41: '20', 43: '21',
45: '22', 47: '23', 100: 100500}
>>> d.popitem()
(100, 100500)
>>> d.popitem()
(47, '23')
>>> d.popitem()
(45, '22')
>>> d.popitem()
(43, '21')
>>>
>>> d.keys()
dict_keys([1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41])
>>> d[18] = "QQ"
>>> d
{1: '0', 3: '1', 5: '2', 7: '3', 9: '4', 11: '5', 13: '6', 15: '7', 17: '8', 19: '9', 21: '10', 23: '11
', 25: '12', 27: '13', 29: '14', 31: '15', 33: '16', 35: '17', 37: '18', 39: '19', 41: '20', 18: 'QQ'}
>>> d.keys()
dict_keys([1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 18])
>>>
Единственный минус такого способа хранения — долгое удаление элемента, однако поскольку в процессе работы со словарями удаление производится редко, это не стало большой проблемой.
Благодаря новой реализации хранения упростилась реализация специальных типов словаря: dict_keys — с описанием всех ключей в порядке добавления, dict_values — с описанием их значений, dict_items — с описанием пар «ключ-значение»:
>>> list(d)
[1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 18]
>>> d.keys()
dict_keys([1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 18])
>>> k = d.keys()
>>> type(k)
<class 'dict_keys'>
>>>
>>> d.values()
dict_values(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', 'QQ'])
>>>
>>> d.items()
dict_items([(1, '0'), (3, '1'), (5, '2'), (7, '3'), (9, '4'), (11, '5'), (13, '6'), (15, '7'), (17, '8'), (19, '9'), (21, '10'), (23, '11'), (25, '12'), (27, '13'), (29, '14'), (31, '15'), (33, '16'), (35,'17'), (37, '18'), (39, '19'), (41, '20'), (18, 'QQ')])
>>>
>>> for key in d:
... print(key, end = " ")
... print()
...
1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 18
>>>
>>> for key, val in d.items():
... print(key,val, end = " || ")
... print()
...
1 0 || 3 1 || 5 2 || 7 3 || 9 4 || 11 5 || 13 6 || 15 7 || 17 8 || 19 9 || 21 10 || 23 11 || 25 12 || 27 13 || 29 14 || 31 15 || 33 16 || 35 17 || 37 18 || 39 19 || 41 20 || 18 QQ ||
>>>
Проверка для ключей, взятие значений без выпадания ошибки при работе — дефолтное значение или автопроверка:
Как и в случае с множеством, словарь позволяет проводить константный поиск по ключу:
>>> d
{1: '0', 3: '1', 5: '2', 7: '3', 9: '4', 11: '5', 13: '6', 15: '7', 17: '8', 19: '9', 21: '10', 23: '11',
25: '12', 27: '13', 29: '14', 31: '15', 33: '16', 35: '17', 37: '18', 39: '19', 41: '20', 18: 'QQ'}
>>> 24 in d
False
>>> 25 in d
True
>>>
При работе с элементами словаря можно использовать разную реакцию на отсутствие элемента: обычная индексация к элементу с несуществующим ключом вызывает срабатывание исключения KeyError. Специальный метод get при отсутствии элемента возвращает None по умолчанию, ему также можно задать возвращаемое значение явно. Однако ни один из этих способов ни коим образом не модифицирует словарь. Для того, чтобы при отсутствии соответствующего ключа сразу задать в словаре такой со значением по умолчанию, можно воспользоваться специальным методом setdefault
>>> d[24]
Traceback (most recent call last):
File "<python-input-30>", line 1, in <module>
d[24]
~^^^^
KeyError: 24
>>> d.get(24)
>>> print(d.get(24))
None
>>> d.get(24, "Nope.")
'Nope.'
>>> d.get(25)
'12'
>>> d.setdefault(24, "QQ")
'QQ'
>>> d.setdefault(25, "QQ")
'12'
>>> d
{1: '0', 3: '1', 5: '2', 7: '3', 9: '4', 11: '5', 13: '6', 15: '7', 17: '8', 19: '9', 21: '10', 23: '11',
25: '12', 27: '13', 29: '14', 31: '15', 33: '16', 35: '17', 37: '18', 39: '19', 41: '20', 18: 'QQ', 24: 'QQ'}
>>>
Для добавления и модификации данных в словаре используется метод update: он получает объект, из которого можно итерационно получать пары ключ-значение (другой словарь, список пар, иное) и модифицирует значения существующих ключей и добавляет новые пары в конец словаря:
>>> d = {2*i + 1: f'{i}' for i in range(4)}
>>> d
{1: '0', 3: '1', 5: '2', 7: '3'}
>>> help(d.update)
>>> d.update({3: '100', 8: "Haha"})
>>> d
{1: '0', 3: '100', 5: '2', 7: '3', 8: 'Haha'}
>>>
Хорошим способом быстро получать пары ключ-значение — использовать функцию zip. Это встроенная функция, получающая несколько последовательностей на вход и возвращающая последовательность из кортежей, в каждом из которых собраны наборы из последовательных элементов множеств:
>>> zip("asdf", [1, 2, 3, 4, 5], ( (), (), (), () ) )
<zip object at 0x7fafaea12bc0>
>>> z = zip("asdf", [1, 2, 3, 4, 5], ( (), (), (), () ) )
>>> for item in z:
... print(item)
...
('a', 1, ())
('s', 2, ())
('d', 3, ())
('f', 4, ())
>>>
Таким образом становится просто создавать словарь из списка ключей и значений:
>>> dict(zip("QWERT", range(10, 14)))
{'Q': 10, 'W': 11, 'E': 12, 'R': 13}
>>>
Словарь поддерживает некоторые операции над множествами, в частности, объединение словарей с приоритетом значений второго при совпадении ключей (по аналогии с update). Однако в случае пересечения приоритет не определён, из-за чего его для словарей в виде встроенного метода нет:
>>> d = {2*i + 1: f'{i}' for i in range(4)} up
>>> e = {2*i + 3: f'{i}' for i in range(2, 7)}
>>> d
{1: '0', 3: '1', 5: '2', 7: '3'}
>>> e
{7: '2', 9: '3', 11: '4', 13: '5', 15: '6'}
>>> d | e
{1: '0', 3: '1', 5: '2', 7: '2', 9: '3', 11: '4', 13: '5', 15: '6'}
>>> e | d
{7: '3', 9: '3', 11: '4', 13: '5', 15: '6', 1: '0', 3: '1', 5: '2'}
>>> d & e
Traceback (most recent call last):
File "<python-input-45>", line 1, in <module>
d & e
~~^~~
TypeError: unsupported operand type(s) for &: 'dict' and 'dict'
>>>
Словари в Python
Словари в Python используются повсеместно. Самый известный словарь, на основе которого работает вся логика основного пространства имён — globals. Все глобальные объекты хранятся именно в нём, и через него их можно модифицировать:
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': '_pyrepl', '__loader__': None, '__spec__': None, '__annotations__': {},
'__builtins__': <module 'builtins' (built-in)>, '__file__': '/usr/lib/python3.13/_pyrepl/__main__.py',
'__cached__': '/usr/lib/python3.13/_pyrepl/__pycache__/__main__.cpython-313.pyc'}
>>> g = globals()
>>> type(g)
<class 'dict'>
>>> a = 100500
>>> g
{'__name__': '__main__', '__doc__': None, '__package__': '_pyrepl', '__loader__': None, '__spec__': None, '__annotations__': {},
'__builtins__': <module 'builtins' (built-in)>, '__file__': '/usr/lib/python3.13/_pyrepl/__main__.py',
'__cached__': '/usr/lib/python3.13/_pyrepl/__pycache__/__main__.cpython-313.pyc', 'g': {...}, 'a': 100500}
>>> g["a"] = 42
>>> a
42
>>>
Вообще говоря, в Python операция связывания никак не оперирует именами. Есть просто пространство имён globals, В котором может храниться всё, что угодно. В том числе, и объекты, к которым обычным способом просто невозможно обратиться (например, в их названии не только разрешённые для идентификаторов символы):
>>> ..! = 100500
File "<python-input-8>", line 1
..! = 100500
^
SyntaxError: invalid syntax
>>> globals()["..!"] = 100500
>>> globals()["..!"]
100500
>>>
Следующие объекты, активно использующие словари — функции eval и exec, выполняющие выражения или даже целые блоки Python-кода. В качестве аргументов они могут принимать словари, выступающие для них глобальным и локальным пространствами имён. Причём явное указание этих пространств перекрывает им видимость globals(), поэтому для задания, например, локальных переменных необходимо явно указать параметр глобального пространства имён:
>>> a, b = 2, 3
>>> eval("a + b")
5
>>> eval("a + b",{'a': 10, 'b': 20})
30
>>>
>>> c = 123
>>> eval("a + b + c",{'a': 10, 'b': 20})
Traceback (most recent call last):
File "<python-input-21>", line 1, in <module>
eval("a + b + c",{'a': 10, 'b': 20})
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<string>", line 1, in <module>
NameError: name 'c' is not defined
>>> eval("a + b + c", globals(), {'a': 10, 'b': 20})
153
>>>
>>> exec("""
... for i in range(5):
... print(i + a)
... """, {"a": 100500})
100500
100501
100502
100503
100504
>>>
Вспомним немного о функциях: все их параметры разделяются на строго позиционные, позиционно-именные и строго именные.
>>> def fun(a, /, b = 2, *, c=3):
... print(a, b, c)
...
>>> fun(1, 2, c=3)
1 2 3
>>> fun(1, b=2, c=3)
1 2 3
>>>
Нами рассматривалась возможность передачи в функция сразу кортежа позиционных параметров. На деле, возможно также передавать словарь именных:
>>> def fun(*args, **kwargs):
... print(args, kwargs)
...
>>> fun(1, 2, 3)
(1, 2, 3) {}
>>> fun(1, 2, 3, d=4, f=5)
(1, 2, 3) {'d': 4, 'f': 5}
>>>
И их также можно распаковывать:
>>> d = {'q': 123, 'g': 100500}
>>> fun(d, **d)
({'q': 123, 'g': 100500},) {'q': 123, 'g': 100500}
>>>
Модуль collections
Поскольку данная глава посвящена контейнерам, нельзя обойти стороной специальный модуль collections, хранящий большой набор контейнеров.
Первый рассматриваемый контейнер — дека (deque — Double-Ended QUEue) — уже упоминался нами ранее. Данный контейнер представляет собой список с константным добавлением элементов не только в конец, но и в его начало:
>>> from collections import deque
>>> d = deque([1, 2, 3])
>>> d
deque([1, 2, 3])
>>> d.append(4)
>>> d
deque([1, 2, 3, 4])
>>> d.appendleft(-10)
>>> d
deque([-10, 1, 2, 3, 4])
>>> d.pop()
4
>>> d.popleft()
-10
>>>
Следующий контейнер — namedtuple. Он позволяет задать некоторые параметры для неизменяемого кортежа:
- Специализированное имя
- Имена для позиционных элементов кортежа (по которым можно обращаться к элементам)
- Значения полей по умолчанию
>>> C = collections.namedtuple("Coord", ['x', 'y'], defaults = [0, 0])
>>> C()
Coord(x=0, y=0)
>>> C(1, 45)
Coord(x=1, y=45)
>>> C().x
0
>>> c = C(2, 3)
>>> c.y
3
>>> c[1]
3
>>>
ChainMap — специальный контейнер-интерфейс, не создающий новый объект, но позволяющий работать с псевдообъединением вообще любых объектов-контейнеров
>>> from collections import ChainMap
>>> d = {2*i + 1: f'{i}' for i in range(4)}
>>> e = ["Q", '37', 574]
>>> c = ChainMap(d, e)
>>> c
ChainMap({1: '0', 3: '1', 5: '2', 7: '3'}, ['Q', '37', 574])
>>> list(c)
['Q', '37', 574, 1, 3, 5, 7]
>>> c[5]
'2'
>>> c[1]
'0'
>>> c[2]
574
>>>
Отдельное внимание заслуживают контейнеры на основе словарей. Первый такой контейнер — Counter. Он автоматически подсчитывает количество элементов множества-параметра, и с помощью специальных методов most_common и total может выдавать статистику по этим данным. В остальном, работа с Counter ничем не отличается от работы с обычным словарём:
>>> c = collections.Counter("QWEQRQQRQRWQREWWERWERWE")
>>> c
Counter({'Q': 6, 'W': 6, 'R': 6, 'E': 5})
>>> d = collections.Counter("QWERQDSADSEWQERQADS")
>>> d
Counter({'Q': 4, 'E': 3, 'D': 3, 'S': 3, 'W': 2, 'R': 2, 'A': 2})
>>> c + d
Counter({'Q': 10, 'W': 8, 'E': 8, 'R': 8, 'D': 3, 'S': 3, 'A': 2})
>>> c - d
Counter({'W': 4, 'R': 4, 'Q': 2, 'E': 2})
>>> d - c
Counter({'D': 3, 'S': 3, 'A': 2})
>>> c.most_common()
[('Q', 6), ('W', 6), ('R', 6), ('E', 5)]
>>> c.total()
23
>>>
Следующий контейнер — defaultdict — словарь с умолчанием. В случае обращения к несуществующему в нём элементу он автоматически выполняет некоторую функцию-обработчик и её результат сохранит в качестве значения по умолчанию для заданного ключа:
>>> def factory():
... return 100500
...
>>> d = collections.defaultdict(factory)
>>> d
defaultdict(<function factory at 0x7fbe3d46d1c0>, {})
>>> d[123]
100500
>>> d[125]
100500
>>>
При помощи данного словаря можно провернуть интересный трюк по генерации дерева:
>>> def tree(): return collections.defaultdict(tree)
...
>>> T = tree()
>>> T[123]
defaultdict(<function tree at 0x7fbe3d46d260>, {})
>>> T[123][234]
defaultdict(<function tree at 0x7fbe3d46d260>, {})
>>> T
defaultdict(<function tree at 0x7fbe3d46d260>, {123: defaultdict(<function tree at 0x7fbe3d46d260>, {23
4: defaultdict(<function tree at 0x7fbe3d46d260>, {})})})
>>>