Приглашаем посетить
Набоков (nabokov-lit.ru)

Хэши

Назад
Глава 5 Хэши
Вперед

Хэши

Введение

Как люди, так и части компьютерных программ взаимодействуют между собой самым причудливым образом. Отдельные скалярные переменные похожи на отшельников, ведущих замкнутое существование в рамках собственной личности. Массив напоминает партию, где множество индивидуумов объединяется под именем харизматического предводителя. Где-то между ними расположилась удобная ниша, в которой живут совокупности связей "один-к-одному" - хэши. В старой документации по Perl хэши часто назывались ассоциативными массивами, но термин получается слишком длинным. Аналогичные структуры данных существуют и в других языках, где они обозначаются другими терминами - хэш-таб-лицы, таблицы, словари, отображения и даже а-списки, в зависимости от языка. К сожалению, отношения хэшей являются не равными, а подчиненными - например, "Энди - начальник Ната"; "Кровяное давление пациента - 112/62" или "Название журнала с индексом ISSN 1087-903X - The Perl Journal". Хэш всего лишь предоставляет удобные средства для получения ответов на вопросы типа: "Кто является начальником Ната?" или "Как называется журнал 1087-903X"? Вы не сможете спросить "Чьим начальником является Эндн?" Впрочем, поиску ответов на подобные вопросы посвящен один из рецептов этой главы. Однако у хэшей есть свои преимущества. В Perl хэш является встроенным типом данных. Благодаря применению хэшей многие сложные алгоритмы сводятся к простой выборке значений. Кроме того, хэши предоставляют быстрые и удобные средства для построения индексов и таблиц просмотра. Если для простой скалярной переменной применяется идентификатор типа $, а для массива - @, то для хэшей используется идентификатор %. Префикс % относится лишь к ссылкам на хэш в целом. Значение ключа представляет собой скалярную величину, поэтому для него используется символ $ (по аналогии с тем, как для ссылок на отдельный элемент массива используется префикс $). Следовательно, отношение "начальник Ната" должно записываться в виде $boss{"Nat"}. В обычных массивах используются числовые индексы, но индексы хэшей всегда являются строковыми. Ассоциированные значения могут быть произвольными скалярными величинами, в том числе ссылками. Используя ссылки в качестве ассоциированных значений, можно создавать хэши для хранения не только строк и чисел, но и массивов, других хэшей или объектов (вернее, ссылок на массивы, хэши или объекты). Хэши могут инициализироваться с помощью списков, содержащих пары "ключ/ значение":

%аgе = ( "Nat", 24, "Jules", 25, "Josh", 17 );


Такая запись эквивалентна следующей:

$age{"Nat"} = 24;
$age{"Jules"} = 25;
$age{"Josh"} = 17;

Для упрощения инициализации хэшей был создан оператор, оператор =>. В основном он представляет собой более наглядную замену для занятой. Например, возможна следующая инициализация хэша:

%food_color = (
"Apple" => "red", "Banana" => "yellow", "Lemon" => "yellow", "Carrot" => "orange" );


(хэш %i ooa_coior используется во многих примерах этой главы). Такая инициализация также является примером списковой эквивалентности - в некоторых отношениях хэш ведет себя так, словно он является списком пар "ключ/значение". Мы воспользуемся этим в нескольких рецептах, в частности - для объединения и инвертирования. В отличие от обычной занятой, оператор => обладает особым свойством: любое предшествующее ему слово интерпретируется как строковое значение. Это позволяет убрать кавычки и сделать программу более попятной. Однословные ключи хэшей также автоматически интерпретируются как строки, поэтому вместо $hash{"somekey"} можно написать просто $hash{somekey}. Приведенная выше инициализация %food_color записывается в следующем виде:

%food_color = (
Apple => "red", Banana => "yellow", Lemon => "yellow", Carrot => "orange"
Одно из важных свойств хэшей заключается в том, что их элементы хранятся в особой последовательности, обеспечивающей выборку. Следовательно, независимо от порядка занесения данных в хэш, порядок их хранения будет непредсказуемым.

Смотри также: Описание функний unshift и splice в perlfunc(1).

5.1. Занесение элемента в хэш

Проблема

Требуется добавить в хэш новый элемент.

Решение

Присвоите нужное значение в записи вида:
$ХЭШ{$КЛЮЧ} = $ЗНАЧЕНИЕ;

Комментарий

Пропесс занесения данных в хэш весьма тривиален. В языках, где хэш не относится к встроенным типам данных, приходится беспокоиться о переполнении, изменении размеров и коллизиях в хэш-таблицах. В Perl обычное присваивание решает сразу все проблемы. Если ключ уже занят, то есть содержит предыдущее значение, память автоматически освобождается (по аналогии с присваиванием скалярной переменной).
# Хэш %food_color определяется во введении
$food_color{Raspberry} = "pink":
print "Known foods:\n";
foreach $food (keys %food_color) { print "$food\n";
}


Known foods:
Banana
Apple
Raspberry
Carrot
Lemon

Если в качестве ключа хэша используется неопределенная величина undef, она преобразуется в пустую строку "" (что сопровождается предупреждением при запуске с параметром -w). Вероятно, неопределенный ключ undef - это не то, что вы хотели. С другой стороны, undef является вполне допустимым значением в хэ-шах. Однако при выборке значения для ключа, отсутствующего в хэше, вы также получите undef. Это означает, что для проверки существования ключа $key в хэше %hash простая логическая проверка if ($hash{$key}) не подходит. Присутствие ключа в хэше проверяется записью вида exists($hash{$key}); определенность ассоциированного значения - defined($hash{$key}), а его истинность - if ($hash{$key} /. Во внутренних алгоритмах хэширования Perl перестановки строки попадают на одну и ту же позицию. Если в ключах хэша многократно встречаются перестановки одной строки (скажем, "spare" и "craps"), быстродействие хэша заметно падает. На практике это происходит редко.

Смотри также: Раздел "List Value Constructors" peiidata(1) рецепт 5.2.

5.2. Проверка наличия ключа в хэше

Проблема

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

Решение

Воспользуйтесь функцией exists:

# Содержит ли %ХЭШ ключ $КЛЮЧ?
if (ех1з1з($ХЭШ{$КЛЮЧ})) {
# Ключ существует
} else {
# Ключ не существует
}

Комментарий

В следующем фрагменте функция exists проверяет, присутствует ли ключ в хэше %food_color:

# Хэш %food_color определяется во введении
foreach $name ("Banana", "Martini") {
if (exists $food_color{$name}) {
print "$name is a tood.\n";
} else {
print "$name is a drink.\n";
}
}
Banana is a food. Martini is a drink, Функция exists проверяет только наличие ключа в хэше. Она не сообщает об ассоциированном значении, определено ли оно, истинно или ложно. На первый взгляд кажется, что отличия несущественны. Однако в действительности проблемы такого рода плодятся быстро, как кролики. Возьмем следующий фрагмент:

%аgе = ();
$age{"Toddler"} = 3;
$age{"Unborn"} = 0:
$age{"Phantasm"} = undef;
foreach $thing ("Toddler", "Unborn", "Phantasm", "Relic"} { print "$thing: ";
print "Exists " if exists $age{$thing};
print "Defined "if defined $age{thing}:
print "True " if $age{$thing};
print "\n";
}
Toddler: Exists Defined True Unborn: Exists Defined Phantasm: Exists Relic: Элемент $age{ "Toddler"} проходит все три проверки - существования, определенности и истинности. Он существует, потому что мы присвоили ключу "Toddler" значение в хэше. Он определен, потому что значение не равно undef. Наконец, он истинен, потому что присвоенная величина не является одним из ложных значений Perl. Элемент $age{" Unborn"} проходит только проверки существования и определенности. Он существует, потому что ключу "Unborn" было присвоено значение в хэше, и определен, потому что это значение не равно undef. Однако он не является истинным, потому что 0 интерпретируется в Perl как одна из ложных величин. Элемент $age{ "Phantasm"} проходит только проверку существования. Он существует, потому что ключу "Phantasm" было присвоено значение в хэше. Поскольку это значение представляет собой undef, проверка определенности не работает. Так как undef также считается в Perl одним из ложных значений, проверка истинности тоже не работает. Наконец, $age{ "Relic"} не проходит ни одну из проверок. Значение для "Relic" не заносилось в хэш, поэтому проверка на существование завершается неудачей. Из-за отсутствия ассоциированного значения попытка обратиться к $age{ "Relic"} дает undef. Как мы знаем из примера с "Phantasm", undef не проходит проверки определенности и истинности. Иногда undef полезно сохранить в хэше. Это означает: "такой ключ встречается, но с ним не связано никакого полезного значения". Например, рассмотрим программу, которая определяет размер файлов из переданного списка. Следующий фрагмент пытается пропускать файлы, которые уже встречались в списке, однако это не касается файлов нулевой длины и встречавшихся ранее несуществующих файлов:

%name = ();
while (о) {
chomp;
next if $name{$_}; # НЕВЕРНО !
$name{$_} = -s $_;
}


Замена неправильной строки следующим вызовом exists позволяет пропускать нулевые и несуществующие файлы: next if exists $name{$_}; В самом первом примере предполагается, что все, что не является едой (food), относится к напиткам (dnnk). В реальном мире подобные допущения весьма опасны.

Смотри также: Описание функций exists и defined в perlfunc(1). Концепция истинности рассматривается в разделе "Scalar Values" perldata(1).

5.3. Удаление из хэша

Проблема

Требуется удалить элемент из хэша, чтобы он не опознавался функцией keys, values или each. Например, если в хэше имена работников ассоциируются с окладами, после увольнения работника необходимо удалить его строку из хэша.

Решение

Воспользуйтесь функцией delete: # Удалить $КЛЮЧ и ассоциированное значение из хэша %ХЭШ с)е1е1е($ХЭШ{$КЛЮЧ});

Комментарий

Многие ошибочно пытаются удалять элементы из хэша с помощью undef - undef ${ХЭШ{$КЛЮЧ} или $ХЭШ{$КЛЮЧ} = undef. В обоих случаях в хэше будет присутствовать элемент с ключом $КЛЮЧ и значением undef. Функция delete - единственное средство для удаления конкретных элементов из хэша. Удаленный элемент не появится ни в списке keys, ни в итерациях each; функция exists возвращает для него ложное значение. Следующий фрагмент демонстрирует отличия undef от delete:

# Хэш %food_color определяется во введении
sub print_foods {
my ( >foods = keys %food_color,
my $food;
print "Keys: @foods\n";
print "Values: ";
foreach $food (Ofoods) {
my $color = $food_color{$food};
if (defined $color) { print "$color ";
} else {
print "(undef)"; }
}
print \n";
}
print "Initially:\n";
print_foods();
print "\nWith Banana undef\n";
undef $food_color{"Banana"};
print_foods();
print "\nWith Banana deleted\n";
delete $food_color{"Banana"};
print_foods();


Initially:
Keys: Banana Apple Carrot Lemon Values: yellow red orange yellow
With Banana undef
Keys: Banana Apple Carrot Lemon
Values: (undef) red orange yellow
With Banana deleted Keys: Apple Carrot Lemon Values: red orange yellow Как видите, после присвоения $food_color{"Banana"} = undef ключ "banana" остается в хэше. Элемент не удаляется; просто мы присвоили ему undef. С другой стороны, функция delete действительно удалила данные из хэша - ключ "banana" исчезает из списка, возвращаемого функцией keys. Функция delete также может вызываться для среза хэша, это приводит к удалению всех указанных ключей:
delete @food_color{"Banana", "Apple", "Cabbage"};


Смотри также: Описание функций delete и keys в peiifunc(1). Применение keys продемонстрировано в рецепте 5.4.

5.4. Перебор хэша

Проблема

Требуется выполнить некоторые действия с каждым элементом (то есть парой "ключ/значение") хэша.

Решение

Воспользуйтесь функцией each в цикле while:
while(($Kлюч, $ЗНАЧЕНИЕ) = each(%ХЭШ)) { # Сделать что-то,с $КЛЮЧ и $ЗНАЧЕНИЕ
}
Если хэш не очень велик, можно вызвать keys в цикле fоreach:
foreach $КЛЮЧ (keys %ХЭШ) {
$ЗНАЧЕНИЕ = $ХЭШ{$КЛЮЧ};
# Сделать что-то с $КЛЮЧ и $ЗКАЧЕНИЕ }

Комментарий

Следующий простой пример перебирает элементы хэша %food_color из введения:
# Хэш %food_color определяется во введении
while(($food, $color.) = each(%food_color)) { print "$food is $color.\n";
}

Banana is yellow. Apple is red. Carrot is orange. Lemon is yellow. В примере с foreach можно обойтись без переменной $со1ос, поскольку она используется всего один раз. Достаточно написать:
print "Stood is $food_color{$food}.\n".

При каждом вызове each для одного и того же хэша функция возвращает "следующую" пару ключ/значение. Слово "следующую" взято в кавычки, потому что пары возвращаются в порядке, соответствующем внутренней структуре хэша, и этот порядок почти никогда не совпадает с числовым или алфавитным. За последним элементом each возвращает пустой список (); результат интерпретируется как ложный, и цикл while завершается. В примере с foreach использована функция keys, которая строит список всех ключей из хэша еще перед началом выполнения цикла. Преимущество each заключается в том, что пары "ключ/значение" извлекаются по одной. Если хэш содержит много ключей, отказ от предварительного построения полного списка существенно экономит память и время. Однако функция each не позволяет управлять порядком обработки пар. Применение foreach и keys для перебора списка позволяет установить свой порядок обработки. Предположим, нам понадобилось вывести содержимое хэша в алфавитном порядке ключей:
foreach $food (sort keys %food_color) { print "$food is $food_color{$food}.\n";
}

Apple is red. Banana is yellow. Carrot is orange. Lemon is yellow.

Подобное применение to reach встречается довольно часто. Функция keys строит список ключей в хэше, после чего to reach перебирает их. Если хэш состоит из большого числа элементов, возникает опасность, что возвращаемый keys список займет много памяти. Приходится выбирать между затратами памяти и возможностью обработки элементов в определенном порядке. Сортировка подробнее рассматривается в рецепте 5.9. Поскольку функции keys, values и each используют одни и те же внутренние структуры данных, следует внимательно следить за чередованием вызовов этих функций или преждевременным выходом из цикла each. При каждом вызове keys или values текущая позиция each сбрасывается. Следующий фрагмент зацикливается и бесконечно выводит первый ключ, возвращаемый each:

while ( ($k,$v) = each %food_color) {
print "Processing $k\n";
keys %food_color; # Возврат к началу
%food_color }
Модификация хэша во время его перебора в each или f о reach, как правило, сопряжена с опасностью. При добавлении или удалении ключей из хэша функция each ведет себя по-разному для связанных и несвязанных хэшей. Цикл fо reach перебирает заранее построенный список ключей, поэтому после начала цикла он ничего не знает о добавленных или удаленных ключах. Ключи, добавленные внутри цикла, не включаются автоматически в список перебираемых ключей, а удаленные внутри цикла ключи не удаляются из этого списка. Программа countfrom из примера 5.1 читает файл почтового ящика и выводит количество сообщений от каждого отправителя. Отправитель определяется по строке From: (в этом отношении сценарий не очень интеллектуален, однако нас сейчас интересуют операции с хэшами, а не обработка почтовых файлов). Передайте имя почтового ящика в командной строке или используйте " -" для перенаправления.
Пример 5.1. countfrom

#!/usr/bin/perl
# countfrom - подсчет сообщений от каждого отправителя
$filename = $argv[0] || "-oo;
open(FILE, "<$filename") or die "Can't open $filename : $!";
while() {
if (/"From: (.*)/) { $from{$1}++ }
}
toreach $person (sort keys %from) { print "$person: $from{$person}\n";


Смотри также: Описание функций each и keys в peiifunc(1), описание циклов for и foreach в рецепте 4.5.

5.5. Вывод содержимого хэша

Проблема

Требуется вывести содержимое хэша, однако конструкции print "%ХЭШ" и print %ХЭШ не работают.

Решение

Одно из возможных решений - перебрать все нары "ключ/значение" в хэше (см. рецепт 5.4) и вывести их:

while ( ($k,$v) = each %hash) { print "$k => $v\n";
}

Также можно построить список строк с помощью mар:
print тар { "$_ => $hash{$_}\n" } keys %hash;

Или воспользуйтесь фокусом из рецепта 1.10 и интерполируйте хэш как список:
print "@{[ %hash ]}\n";

Или сохраните хэш во временном массиве и выведите его:
{
my @temp = %hash;
print "@temp";
}

Комментарий

Все перечисленные приемы обладают различными возможностями по управлению порядком н форматированием вывода, а также различной эффективностью. Первый способ (перебор хэша) чрезвычайно гибок и эффективен но затратам памяти. Вы можете как угодно форматировать выходные данные, при этом понадобятся всего две скалярные переменные - текущий ключ и значение. Использование цикла foreach позволяет вывести хэш с упорядочением ключей (ценой построения отсортированного списка):
foreach $k (sort keys %hash) {
print "$k => $hash{$k}\n";
}
Функция map не уступает перебору по богатству возможностей. Сортировка ключей по-прежнему позволяет работать с элементами в произвольном порядке, Выходные данные можно как угодно срорматировать. На этот раз создастся список строк (например, "КЛЮЧ==>ЗНАЧЕНИЕ", как в приведенном выше примере), передаваемый print. Два последних приема представляют собой фокусы, связанные с интерполяцией. Интерпретация хэша как списка не позволяет предсказать или управлять порядком вывода пар "ключ/значение". Более того, данные в этом случае выводятся в виде списка ключей и значений, элементы которого разделяются текущим содержимым переменной $". В отличие от других приемов, вам не удастся вывести каждую пару на новой строке или отделить ключи от значений символом =>.

Смотри также: Описание переменной $" в perlvar(1); описание функций foreach, map, keys, sort и each в perlfunc(1). Строковая интерполяция рассматривается в рецепте 1.10, а перебор хэша - в рецепте 5.4.

5.6. Перебор элементов хэша в порядке вставки

Проблема

Функции keys и each извлекают элементы хэша в довольно странном порядке. Вы хотите получить элементы в порядке вставки.

Решение


Воспользуйтесь модулем Tie::IxHash.
use Tie::IxHash;
tie %ХЭШ, "Tie::IxHash";
# Операции с хэшем %ХЭШ
@keys = keys %ХЭШ; # Массив @keys отсортирован в порядке вставки

Комментарий

Модуль Tie::IxHash заставляет функции keys, each и values возвращать элементы в порядке занесения в хэш. Это часто избавляет от необходимости заранее обрабатывать ключи хэша какой-нибудь сложной сортировкой или поддерживать отдельный массив, содержащий ключи в порядке их вставки. Tie::IxHash также представляет объектно-ориентированный интерфейс к функциям splice, push, pop, shift, unshift, keys, values и delete, а также многим другим. Следующий пример демонстрирует использование keys и each: # Инициализировать use Tie::IxHash;
tie %food_color, "Tie::IxHas";
$food_color{Banana} = "yellow";
$food_color{Apple} = "green";
$food_color{Lemon} = "yellow";
print "In insertion order, the foods are:\n";
foreach $food (keys %food_color) { print " $food\n";
}
print "Still in insertion order, the foods' colors are:\n' while (( $food, $color ) =
each %food_color ) { print "$food is colored $color.\n":
}

In insertion order, the foods are:
Banana
Apple
Lemon Still in insertion order, the foods' colors are:
Banana is colored Yellow. Apple is colored Green. Lemon is colored Yellow.

Смотри также: Документация по модулю Tie::IxHash от CPAN; рецепт 13.15.

5.7. Хэши с несколькими ассоциированными значениями

Проблема

Требуется хранить в хэше несколько значений, ассоциированных с одним ключом.

Решение

Сохраните в хэше ссылку на массив для хранения ассоциированных значений.

Комментарий

В хэше могут храниться только скалярные величины. Однако ссылки являются скалярными величинами. Таким образом, проблема решается сохранением в $ХЭШ {$КЛЮЧ} ссылки на массив со значениями, ассоциированными с ключом $КЛЮЧ. Обычные операции с хэшами - вставка, удаление, перебор и проверка существования - переписываются для операций с массивами (push, splice и foreach). Следующий фрагмент реализует простую вставку в хэш. Он обрабатывает выходные данные команды who(i) на компьютере с UNIX и выводит краткий список пользователей с терминалами, на которых они зарегистрированы:
%ttys =();
open(WHO, "who|") or die "can't open who: $!";
while () {
($user, $tty) = split;
push( @{$ttys{$user}}, $tty );
}
foreach $user (sort keys %ttys) { print "$user: @{$ttys{$user}}\n";
}
Вся суть этого фрагмента заключена в строке push, где содержится версия $tty{$user} = $tty для многозначного хэша. Все имена терминалов интерполируются в строке print конструкцией @{$ttys{user}}. Если бы, например, нам потребовалось вывести владельца каждого терминала, мы бы организовали перебор анонимного массива:

foreach $user (sort keys %ttys) {
print "$user: ", scalar( @>{$ttys{$user}} ), "ttys.\n";
foreach $tty (sort @{$ttys{$user}}) {
@stat = stat("/dev/$tty");
$user = @stat ? ( getpwuid($stat[4]) )[0] : "(not available)";
print "\t$tty (owned by $user)\n";
}
}

Функция exists может иметь два значения: "Существует ли в хэше хотя бы одно значение для данного ключа?" и "Существует ли данное значение для данного ключа?" Чтобы реализовать вторую интерпретацию, придется просмотреть массив в поисках нужной величины. Первая трактовка exists косвенно связана с функцией delete: если мы можем гарантировать, что ни один анонимный массив никогда не остается пустым, можно воспользоваться встроенной функцией exists. Чтобы убедиться, что анонимные массивы не остаются пустыми, их следует проверять после удаления элемента:
sub multihash_delete {
my {$hash, $key, $value) = @_;
my $i;
return unless ref( $hash->{$key} );
for ($i = 0; $i < @{ $hash->{$key} }; $i++) { if ($hash->{$key}->[$i] eq $value) {
splice( @{$hash->{$key}}, $i, 1);
last;
}
}
delete $hash->{$key} unless @{$hash->{$key}};
}


Альтернативная реализация многозначных хэшеи приведена в главе 13 "Классы, объекты и связи", где они реализуются как связанные обычные хэши.

Смотри также: Описание функций splice, delete, push, foreach и exists в perlfunc(1); рецепт 11.1. Связи рассматриваются в рецепте 13.15.

5.8. Инвертирование хэша

Проблема

Хэш связывает ключ с ассоциированным значением. У вас имеется хэш и значение, для которого требуется определить ключ.

Решение

Воспользуйтесь функцией reverse для создания инвертированного хэша, где ассоциированные значения исходного хэша являются ключами, и наоборот. # %ХЭШ связывает ключи со значениями %ОБРАТНЫЙ = reverse %ХЭШ;

Комментарий

В этом решении используется списковая эквивалентность хэшей, о которой упоминалось во введении. В списковом контексте reverse интерпретирует %ХЭШ как список и меняет местами составляющие его элементов. Одно из важнейших свойств списковой интерпретации хэша заключается в том, что элементы списка представляют собой пары "ключ/значение". После инвертирования такого списка первым элементом становится значение, а вторым - ключ. Если интерпретировать такой список как хэш, его значения будут являться ключами исходного хэша, и наоборот. Приведем пример:

%surname = ( "mickey" => "Mantle", "Babe" => "Ruth");
%first_name = reverse %surname;
print $first_name{"Mantle", "\n"};

Mickey
Если интерпретировать %surname как список, мы получим следующее:
("Mickey", "Mantle", "Babe", "Ruth")
(а может быть, ("Babe", "Ruth", "Mickey", "Mantle"), поскольку порядок элементов
непредсказуем). После инвертирования список выглядит так:
("Ruth", "Babe", "Mantle", "Mickey") Интерпретация его в качестве хэша дает следующее:
("Ruth" => "Babe", "Mantle" => "Mickey") В примере 5.2 приведена программа foodfind. Если передать ей название продукта, она сообщает цвет, а если передать цвет - она сообщает название. Пример 5.2. foodfind

#!/usr/bin/perl -w
# foodfind - поиск продуктов по названию или цвету
$given = shift @argv or die "usage: foodfind food_or_color\n";
%color = (
"Apple" => "red",
"Banana" => "yellow", 'Lemon" => "yellow", 'Carrot" => "orange"
}
%food = reverse %color;
if (exists $color{$given}) {
print "$given is a food with color $color{$given}.\n";
} if (exists $food{$given}) {
print "$food{$given} is a food with color $given.\n";
}


Если два ключа исходного хэша имеют одинаковые значения ("Lemon" и "Banana" в предыдущем примере), то инвертированный хэш будет содержать лишь один из них (какой именно - зависит от порядка хэширования, так что непредсказуемо). Дело в том, что хэши в Perl no определению имеют уникальные ключи. Чтобы инвертировать хэш с повторяющимися значениями, следует воспользоваться методикой рецепта 5.7 - то есть построить хэш, ассоциированные значения которого представляют собой списки ключей исходного хэша:
# Хэш %food_color определяется во введении
while (($food,$color) = each(%food_color)) {
push(@{foods_with_color{$color}}, $food);
}
print "@{$foods_with_color{yellow}} were yellowfoods.n";

Banana Lemon were yellow foods, Кроме того, это позволит модифицировать программу foodfind так, чтобы она работала с цветами, соответствующими сразу нескольким продуктам. Например, при вызове foodfind yellow будут выводиться и Banana, и Lemon. Если какие-либо значения исходного хэша были не простыми строками и числами, а ссылками, при инвертировании возникает проблема - ссылки не могут использоваться в качестве ключей, если только вы не воспользуетесь модулем Tie::RefHash (см. рецепт 5.12).

Смотри также: Описание функций reverse в perlfunc(t); рецепт 13.15.

5.9. Сортировка хэша

Проблема

Требуется работать с элементами хэша в определенном порядке.

Решение

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

# %hash - сортируемый хэш
@keys = sort { criteriono } (keys %hash);
foreach $key (Okeys) { $value = $hash{$key};
# Сделать что-то с $key, $value
}

Комментарий

Хотя хранить элементы хэша в заданном порядке невозможно (без использования модуля Tie:IxHash, упомянутого в рецепте 5.6), перебирать их можно в любом порядке. Существует множество разновидностей одного базового механизма: вы извлекаете ключи, упорядочиваете их функцией sort и обрабатываете элементы в новом порядке. Допускается применение любых хитростей сортировки, упоминавшихся в главе 4 "Массивы". Рассмотрим пару практических примеров. В первом фрагменте sort просто используется для упорядочения ключей по алфавиту:

foreach $food (sort keys %food_color) { print "$food is $food_color($food).\n":
} Другой фрагмент сортирует ключи по ассоциированным значениям:

foreach $food (sort { $food_color{$a} cmp $food_color{$b} } ) keys %food_color) {
print "$food is $food_color{$food}.\n";
}
Наконец, сортировка выполняется по длине ассоциированных значений:
@foods = sort { length($food_color{$a}) <=> length($food_color{$b}) }
keys %food_color;
foreach $food (Ofoods) {
print "$food is $food_color{$food}.\n":
}


Смотри также: Описание функций sort и keys в perlfunc(1); рецепт 5.6. Сортировка списков рассматривается в рецепте 4.15.

5.10. Объединение хэшей

Проблема

Требуется создать новый хэш, содержащий элементы двух существующих хэшей.

Решение

Интерпретируйте хэши как списки и объедините их так, как это делается со списками:
%merged = (%a, %В);
Для экономии памяти можно организовать перебор элементов и построить новый хэш следующим образом:
%merged = ();
while ( ($k,$v) = each(%a) ) {
$merged{$k} = $v;
} while ( ($k,$v) = each(%b) ) {
$merged{$k} = $v;
}

Комментарий

В первом варианте, как и в предыдущем рецепте инвертирования хэшей, используется списковая эквивалентность, о которой говорилось во введении. (%A, %В) интерпретируется как список пар "ключ/значение". Когда он присваивается объединенному хэшу %merged, Perl преобразует список пар снова в хэш. Рассмотрим, как эта методика реализуется на практике:
# Хэш %food_color определяется во Введении
%drink_color = ( galliano => "yellow", "Mat Tai" => "blue" );
%ingested_colors = (%drink_color, %food_color);
Ключи обоих входных хэшей присутствуют в выходном не более одного раза. Если в хэшах найдутся совпадающие ключи, в итоговый хэш включается тот ключ, который встретился последним. Прямое присваивание компактно и наглядно, но при больших размерах хэшей оно приводит к большим расходам памяти. Это связано с тем, что перед выполнением присваивания итоговому хэшу Perl разворачивает оба хэша во временный список. Пошаговое объединение с помощью each, показанное ниже, избавит вас от этих затрат. Заодно вы сможете решить, как поступать с совпадающими ключами. С применением each первый фрагмент записывается следующим образом:
# Хэш %food_color определяется во Введении
%drlnk_color = ( galliano => "yellow", "Mat Tai" => "blue" );
%substance_color = ();
while (($k, $v) = each %food_color) { $substance_color{$k} = $v;
}
while (($k, $v) = each %drink_color) { $substance_color{$k} = $v;
}


Обратите внимание на повторяющийся код присваивания в циклах while. Проблема решается так:
foreach $substanceref (\%food_color, \%drink_color ) { while (($k, $v) = each v%substanceref) { $substance_color{$k} = $v;
}
}

vЕсли в объединяемых хэшах присутствуют одинаковые ключи, можно вставить код для обработки дубликатов:

foreach $substanceref (\%food_color, \%drink_color ) { while (($k, $v) = each
%substanceref) { if (exists $substance_color{$k}) {
print "Warning: $k seen twice. Using the first definition.\n";
next;
} $substance_color{$k} = $v;
}
}
В частном случае присоединения одного хэша к другому можно воспользоваться срезом для получения более элегантной записи:
@all_colors{keys %new_colors} = values %new_colors;
Потребуется память в объеме, достаточном для хранения списков всех ключей и значений %new_colors. Как и в первом варианте, расходы памяти при большом размере списков могут сделать эту методику неприемлемой.

Смотри также: Описание функции each в perlfunc(1); рецепт 4.9.


Назад
Вперед