Table: словари в Nim

Разбираемся, как в Nim устроены словари — тип Table из модуля std/tables — и чем работа с парами ключ-значение похожа на dict из Python.

Table — тип данных для хранения пар «ключ — значение» в Nim, доступный через стандартный модуль std/tables, аналог словаря dict в Python.

Зачем нужны пары ключ-значение

Мы уже умеем хранить список однотипных значений через seq — но что, если данные естественно связаны парами? Например, нужно быстро находить цену товара по его названию, или возраст ученика по имени. Можно было бы завести два параллельных seq — один с именами, другой с возрастами — и искать совпадающий индекс, но это неудобно и медленно при большом количестве данных: пришлось бы перебирать весь список, чтобы найти нужную пару.

Для таких задач существует Table — структура, которая хранит пары «ключ — значение» и умеет находить значение по ключу почти мгновенно, независимо от того, сколько всего пар она содержит. Если вы знакомы с Python, это прямой аналог dict: vozrast = {"Petya": 14, "Masha": 15} в Python и похожий Table в Nim решают одну и ту же задачу.

Импорт модуля tables

В отличие от типов вроде seq, которые доступны сразу, Table нужно явно подключить через import — в Nim стандартная библиотека разбита на модули, и не все из них загружаются в программу автоматически, чтобы не раздувать код лишним.

import std/tables

var vozrast = initTable[string, int]()
vozrast["Petya"] = 14
vozrast["Masha"] = 15

echo vozrast["Petya"]

Вывод:

14

Разбираем построчно

Строка import std/tables подключает модуль словарей из стандартной библиотеки — std/ здесь означает «стандартная библиотека», а tables — конкретный модуль внутри неё. Без этой строки имя Table и функция initTable были бы просто неизвестны компилятору.

Дальше initTable[string, int]() создаёт пустой словарь и явно указывает два типа в квадратных скобках: тип ключей (string — имена учеников) и тип значений (int — возраст). Это похоже на то, как мы указывали один тип элемента для seq[int], только здесь типов два — для ключа и для значения отдельно. Круглые скобки в конце () — обычный вызов функции, создающей пустую структуру.

Строки vozrast["Petya"] = 14 добавляют пару в словарь — синтаксис с квадратными скобками для записи по ключу выглядит так же, как в Python. А echo vozrast["Petya"] читает значение по ключу тем же способом — если ключ существует, вернётся связанное с ним значение.

Проверка наличия ключа

Что если обратиться по ключу, которого в словаре нет? Как и в Python, это вызовет ошибку — поэтому перед обращением к потенциально отсутствующему ключу принято сначала проверять его наличие через hasKey.

import std/tables

var ocenki = initTable[string, float]()
ocenki["Petya"] = 4.5

if ocenki.hasKey("Masha"):
  echo "Балл Маши: ", ocenki["Masha"]
else:
  echo "Записи о Маше пока нет"

Вывод:

Записи о Маше пока нет

Здесь ocenki.hasKey("Masha") возвращает booltrue, если ключ есть в таблице, и false в противном случае. Это тот же паттерн, что и в Python-коде вида if "Masha" in ocenki:, только оформленный как вызов метода, а не оператор in.

Перебор пар: pairs

Чтобы пройтись по всем парам ключ-значение сразу, используется функция pairs в цикле for.

import std/tables

var tsveta = initTable[string, string]()
tsveta["yabloko"] = "krasnyy"
tsveta["limon"] = "zheltyy"
tsveta["list"] = "zelenyy"

for klyuch, znachenie in pairs(tsveta):
  echo klyuch, " — ", znachenie

Вывод:

yabloko — krasnyy
limon — zheltyy
list — zelenyy

Конструкция for klyuch, znachenie in pairs(tsveta) одновременно достаёт и ключ, и значение на каждой итерации — в переменные с именами klyuch и znachenie (их можно назвать как угодно, это не зарезервированные слова). Важно: порядок перебора пар в Table не гарантированно совпадает с порядком добавления — если порядок важен для вашей задачи, его нужно обеспечивать отдельно, например через сортировку ключей.

Мини-пример: подсчёт слов

Соберём импорт, запись, проверку и перебор в одной практической задаче — посчитаем, сколько раз встречается каждое слово в списке.

import std/tables

let slova = @["kot", "pes", "kot", "popugay", "kot", "pes"]
var schetchik = initTable[string, int]()

for slovo in slova:
  if schetchik.hasKey(slovo):
    schetchik[slovo] = schetchik[slovo] + 1
  else:
    schetchik[slovo] = 1

for slovo, kolichestvo in pairs(schetchik):
  echo slovo, ": ", kolichestvo

Вывод:

kot: 3
pes: 2
popugay: 1

Здесь для каждого слова из seq мы проверяем, встречалось ли оно раньше: если да — увеличиваем счётчик на 1, если нет — заводим новую запись со значением 1. Это классическая задача «подсчёт частот», которая на словарях решается в разы проще и быстрее, чем на паре параллельных seq с поиском по индексу.

Частые ошибки новичков

Первая и самая частая ошибка — забыть import std/tables в начале файла и получить ошибку компиляции «undeclared identifier» на слове Table или initTable, хотя всё остальное написано верно.

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

Третья ошибка — ожидать от Table сохранения порядка добавления пар, как в современных версиях dict в Python (начиная с Python 3.7 порядок там гарантирован). В Nim обычная Table такой гарантии не даёт — для задач, где порядок важен, нужно либо сортировать ключи перед выводом, либо использовать специальный упорядоченный вариант таблицы.

Итоги

  • Table хранит пары «ключ — значение» и требует явного import std/tables перед использованием — в отличие от seq, который доступен сразу.
  • initTable[K, V]() создаёт пустой словарь с явно указанными типами ключа и значения; запись и чтение — через квадратные скобки, как в Python.
  • hasKey проверяет наличие ключа перед обращением к нему — обращение к несуществующему ключу без проверки завершает программу ошибкой.
  • pairs() в цикле for перебирает сразу и ключи, и значения, но без гарантии порядка добавления.
Проверьте себя
1. Что нужно сделать перед первым использованием Table в программе на Nim?
AНичего, Table доступна сразу, как seq
BДобавить строку import std/tables — этот тип не подключается автоматически
CУстановить его отдельно через менеджер пакетов, он не входит в стандартную библиотеку
DОбъявить его через ключевое слово table в блоке type
2. Что произойдёт, если обратиться по ключу, которого нет в Table, не проверив это заранее через hasKey?
ANim автоматически вернёт значение по умолчанию для этого типа
BПрограмма завершится с ошибкой во время выполнения
CКлюч будет создан автоматически с пустым значением
DКомпилятор откажется собирать такую программу заранее