Хэши (ассоциативные массивы) в Perl


Оригинал: http://www.perl.ru/

Хэш - ассоциативный массив, т.к. доступ к данным осуществляется при помощи ключа, ассоциированного со значением. Хэши начинаются с префикса %: %hash. Для работы с с хэш-массивами нужно, как и в обычных массивах, использовать разыменовывающий префикс $: $hash{шляпа} = серая;

Хэш можно определить несколькими способами:
%hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная', 'штаны' => 'широкие', 'пиво' => 'темное', 'игрушка' => 'любимая'); %hash = (); #можно не указывать пустой хеш для создания массива, perl может сам создавать пустой хэш. $hash{'шляпа'} = 'серая'; $hash{'водка'} = 'горькая'; $hash{'вобла'} = 'вкусная'; $hash{'штаны'} = 'широкие'; $hash{'пиво'} = 'темное'; $hash{'игрушка'} = 'любимая';

Если используется пробел при определении элемента хэша, то этот пробел лучше поставить в одинарные кавычки $hash{'дырявая шляпа'} = серая;

Добавить элементы в хеш можно так:
%hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); %hash = (%hash, 'кошка', 'мягкая'); foreach $k (keys %hash){ print "$k = $hash{$k}\n"; }
и так:
%hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); $hash{"дом"} = "большой"; $hash{"дым"} = "сизый"; foreach $k (keys %hash){ print "$k = $hash{$k}\n"; }
Проверка хэша на наличие элемента:
%hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); if(exists($hash{"дождь"})){ print "Элемент найден"; } else{ print "Элемент не найден"; }
Удалить элемент из хэша можно при помощи функции delete:
%hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); delete($hash{"шляпа"}); if(exists($hash{"шляпа"})){ print "Элемент найден"; } else{ print "Элемент не найден"; }

Функция delete может вызываться для среза хэша, что приводит к удалению всех указанных ключей: delete @hash{'шляпа','водка','вобла'}; см. perlfunc(1)

Для того чтобы организовать циклы по элементам хэша, нужно использовать функцию each:
%hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); while(($key,$value) = each %hash){ print "$key => $value\n"; };
Для перебора элементов не очень большого хеша можно воспользоваться foreach:
%hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); foreach $key(keys %hash){ print $key,"\n"; #возвращает список ключей хеша } %hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); foreach $value(values %hash){ print "$value\n"; #возвращает список значений хеша }

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

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

Содержимое хэша можно вывести и так: while (@Res = each %hash){ print "$Res[0] = $Res[1]\n" }

Вывод хэша одной строкой.

можно воспользоваться функцией map: print map {"$_ => $hash{$_}\n"} keys %hash;

функция map позволяет работать с элементами в произвольном порядке, в этом случае создается список строк(ключ => значение), передаваемый функции print. Если сохранить хэш во временном массиве, то он будет выведен как строка:

{ my @temp = %hash; print "@temp"; }
также можно вывести хэш и таким образом: print "@{[%hash]}\n";

в двух последних случаях мы интерполируем хэш как список, что не позволяет предсказать или управлять порядком вывода пар "ключ/значение". Данные в последних двух случаях выводятся в виде списка ключей и значений, элементы которого разделяются текущим содержимым переменной $", т.е. не удастся вывести каждую пару значений на новой строке или отделить ключи от значений каким-либо символом. Приведем программу, которая читает файл почтового ящика и выводит количество сообщений от каждого отправителя, отправитель определяется по строке From(цель программы только проиллюстрировать операции с хешами):

#!/usr/bin/perl -w $file = $ARGV[0] || "-"; open(FILE, "<$file") or die "$!"; while(<FILE>){ if(/^From: (.*)/) { $from{$1}++ } } foreach $people(sort keys %from){ print "$people: $from{$people}\n"; }
Запускаем программу чтения почтового ящика: bash-2.03$ ./1.pl /usr/home/vovka/mbox

Для перебора элементов хэша в порядке вставки, т.к. keys и each выводят элементы хеша неупорядоченно, можно воспользоваться модулем Tie::IxHash

use Tie::IxHash tie %hash, "Tie::IxHash"; #операции с %hash @keys = keys %hash;

Модуль Tie::IxHash заставляет функции keys, each и values возвращать элементы в порядке занесения их в хэш. Если у Вас нет такого модуля IxHash.pm то нужно зайти на CPAN/modules, найти его и установить, если у вас нет прав на установку библиотек, то в первой строчке скрипта нужно написать #!/put'/do/perl'a -wT -I/put'/do/nugnogo/modulia. Пример использования Tie::IxHash:

use Tie::IxHash tie %hash, "Tie::IxHash"; %hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); print "В упорядоченной вставке список хеша такой:\n"; foreach $qwerty (keys %hash){ print " $qwerty\n"; } print "Кроме того, предметы обладают некоторыми свойствами:\n"; while(($predmet, $opredelenie) = each %hash){ print $predmet $opredelenie,"\n"; }
см. тех документацию по модулю Tie::IxHash.

Хэши с несколькими значениями, ассоциированными одним ключом. Т.к. скалярные величины, содержащиеся в хэше, могут быть ссылками(которые могут быть скалярами), то ассоциировать несколько значений одним ключом можно сохранив в $hash($key) ссылку на массив со значениями, ассоциированными с ключом $key. Операции с хэшами(вставка, удаление, перебор и проверка существования(undef)) переписываются для операций с массивами(push, splice и foreach). Пример, реализующий вставку в хэш(обрабатывает выходные данные команды who(1) и выводит краткий список пользователей с терминалами, на которых они зарегестрированы):

%ttys=(); open (WHO, "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}}.

Инвертирование хэша производится при помощи функции reverse, в котором ассоциированные значения исходного хэша являются ключами и наоборот. Воспользуемся списковой эквивалентностью хэшей. В списковом контексте reverse иетерпретирует %hash как список и меняет местами составляющие его элементов. Например: имеем хэш %Glavfish = ("seledka"=>"mokraia","skat"=>"elektricheskij"), если его интерпретировать как список, то получим следующее ("seledka","mokraia","skat","elektricheskij"), после инвертирования список выглядит так: ("elektricheskij","skat","mokraia","seledka"), интерпретация его как хэша дает следующее: ("elektricheskij"=>"skat","mokraia"=>"seledka"). Пример программы, которая на название предмета выдает его свойство и наоборот:

#!/usr/bin/perl -w $vziat = shift @ARGV or die $!; %svojstvo = ( "malina" => "vkusnaia", "morkva" => "v_uhi", "kozlodrom" => "rulit", "tupizna" => "tupit"); %predmet = reverse %svojstvo; if (exists $svojstvo{$vziat}){print "$vziat," ", $svojstvo{$vziat}\n";} elsif (exists $predmet{$vziat}){print "$vziat," ", $predmet{$vziat}\n";}
например если ввести в терминале: bash-2.03$ ./1.pl malina то скрипт выдаст: malina vkusnaia или bash-2.03$ ./1.pl vkusnaia vkusnaia malina
Сортировка хэша:
%hash = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); foreach $key(sort keys %hash){ print "$key => $hash{$key}\n"; #отсортирует в алфавитном порядке по значениям ключа } foreach $value(sort values %hash){ print "$value\n"; #сортировка по значению }
Сортировка ключей по алфавиту ассоциированных значений:
foreach $key(sort {$hash{$a} cmp $hash{$b}} keys %hash){ print $key, " => ", $hash{$key},"\n"; }
Сортировка по длинне ассоциированных значений:
@massive = sort{length($hash{$a}) <=> length($hash{$b})} keys %hash; foreach $key(@massive){ print $key, $hash{$key},"\n"; }
Пример из fido7.ru.perl: >> Есть хеш массив (слово, частота), необходимо вывести в файл пары >> (слово,частота) отсортированные по частоте. По ключю - просто, а вот >> по значению затрудняюсь. Подскахите как проще сделать?:-) my %hash=('for'=>1000,'to'=>1500,'in'=>2000,'do'=>500); foreach(sort {$hash{$a} <=> $hash{$b}} keys %hash) { print $_,'=',$hash{$_},"\n"; } Слияние хешей выполняется как и слияние массивов:
%hash1 = ( 'шляпа' => 'серая', 'водка' => 'горькая', 'вобла' => 'вкусная'); %hash2 = ( 'штаны' => 'широкие', 'пиво' => 'темное', 'игрушка' => 'любимая'); %allhash = (%hash1, %hash2);
Чтобы сэкономить память, можно воспользоваться таким кодом:
%hash=(); while (($key, $values) = each(%hash1)){ $hash{$key} = $values; } while (($key, $values) = each(%hash2)){ $hash{$key} = $values; }

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

my @common = (); foreach(keys %hash1){ push(@common, $_) if exists $hash2{$_}; }
Поиск ключей, отсутствующих в другм хэше:
my @test = (); foreach(keys %hash1){ push(@test, $_) unless exists $hash2{$_}; }

Если keys вызывается для хэша, ключи которого представляют собой ссылки, то возвращаемые ей ссылки не работают. Ключи преобразуются в строки, т.е. интерпретируются так, словно они заключены в кавычки, при работе со ссылками они теряют свои свойства. После преобразования в строку ссылка имеет вид

Class::Somewhere=HASH(0x72048) ARRAY(0x72048)

Преобразованную ссылку нельзя вернуть к прежнему виду, т.к. она из ссылки превратилась в строку. Нужно создать специальный хэш, ключами которого являются ссылки, преобразованные в строки, и значениями - настоящие ссылки.Можно воспользоваться модулем Tie::RefHash. Пример показывает использование объектов ввода/вывода для работы с файловыми манипуляторами.

use Tie::RefHash; use IO::File; tie %name, "Tie::RefHash"; foreach $filename("/etc/termcamp/", "/vminux", "/bin/cat"){ $fh = IO::File->("<$filename") or next; $name{$fh} = $filename; } print "open files: ", join(", values %name", "\n"); foreach $file(keys %name){ seek($file, 0, 2); printf("%s is %d bytes long.\n", $name{$file}, $tell{$file}); }

Если в качестве ключа использована неопределенная величина undef, то она преобразуется в пустую строку. undef является вполне допустимым значением в хэше. Но при выборке значения для ключа, отсутствующего в хэше perl выдаст undef. Проверить наличие ключа можно так: exist($hash{$key}); определенность ассоциированного значения: defined($hash{$key}); истинность: if($hash{$key});. Иногда undef нужно сохранять в кэше, т.е. ключ есть, но с ним не связано ничего полезного, например программа, определяющая размер файлов из переданного списка:

%name =(); while(<>){ chomp; next if exist $name{$_}; $name{$_} = -s $_; }
Этот код позволяет пропустить несуществующие и нулевые файлы, но записанные в исходном списке.


Как можно хеш положить в строку? Например проблема:

tnc> Спасибо, товарищи :-)
tnc> До этого я уже допер. Второй вопрос: имеются логи http-gw. Пакостность их
tnc> заключается в том, что на каждый запрос в логе появляется 4 строки,
tnc> совершенно не обязательно идущие подряд:

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

Что-нибудь типа while() { ($key, $no, $data) = parse($_); # расковыряли строчку $buf{$key}->[$no] = $data; # запихнули в хэш в виде массива next if $no < 3; # нумеруем, ессно с нуля analyze($buf{$key}); # обработали delete $buf{key}; # удалили } Пример работы с хэшем и базой данных из fido7.ru.perl (cм. ответ): VS> Скажите, можно ли ввести в dbm-базу данных уже заполненный значениями VS> хеш? Если да, то как? %hash1=("dfadf", "dfadfd"); dbmopen (%hash, "hash", 0666); %hash=%hash1; dbmclose (%hash); Как передать хэш в функцию? fido7.ru.perl (см. всю дискуссию) > PK> Вобщем то все передаеться, но использовать его нельзяю > PK> Hа %_[0] или $_[0]{??} компилер ругаеться. > %a; > &f(\%a); > sub f { > my $a = $($_[0]){'key'}; > } > кажется так, мог ошибится в написании. Смысл в передаче ссылки, а не значения. Это можно сделать также: 1. Неявной передачей ссылки (использование прототипов): sub f(\%) { my $a=$_[0]->{'key'}; } f(%a); 2. Передачей хэша как массива с четным числом элементов: sub f { my $a={@_}->{'key'}; } f(%a); Пример использования хеша для транслитерации fido7.ru.perl: RK> Как сочнить такой tr/../../ (и возможно ли), чтобы RK> "абея" -> "abyoya" RK> Т.е. транслитерацию сделать...

самый простой и топорный вариант - сделать хеш с соответствиями типа я => ya сплитить строчки и заменять элементы полученного массива на нужные. что-то на подобие:

@letters = split //, $str; for (@letters){$_ = $hash{$_}}; Переменные окружения, использующие встроенные хэши и массивы %INC, %SIG, %ENV, %FORM{}.

%INC содержит имена файлов, включаемых командами do и require. Ключ имя файла, значение путь до включаемого файла. Массив @INC содержит список дирректорий, которые просматривает perl для выполнения команд do, require или use.

%SIG - хэш, в котором хранятся обработчики различных ситуаций, возникающих в perl. Например строка local $SIG{__WARN__} = sub{}; отключает предупреждающие сообщения.

%ENV содержит значения переменных среды(окружения), заданных на момент запуска сценария(скрипта). Ключами обычно бывают имена переменных среды(но их состав зависит от операционной системы), изменение этих значений вызовет изменение окружения для процессов потомков.

#!/usr/bin/perl/ -w while (($key, $value) = each(%ENV)){ print "$key => $value\n"; } программа выдает: SERVER_SOFTWARE => Apache/1.3.11 (FreeBSD) mod_perl/1.21 PHP/3.0.14 GATEWAY_INTERFACE => CGI/1.1 DOCUMENT_ROOT => /usr/local/www/data UNIQUE_ID => OZaSFsHofQoAAEd@Cn8 REMOTE_ADDR => 195.202.122.14 SERVER_PROTOCOL => HTTP/1.0 SERVER_SIGNATURE => Apache/1.3.11 Server at www.mojdodir.ru Port 80 REQUEST_METHOD => GET REMOTE_HOST => www.mojdodir.ru QUERY_STRING => HTTP_USER_AGENT => Mozilla/4.73 [en] (Win98; I) PATH => /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin HTTP_ACCEPT => image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */* HTTP_CONNECTION => keep-alive REMOTE_PORT => 3633 SERVER_ADDR => 195.202.122.14 HTTP_ACCEPT_LANGUAGE => en,ru HTTP_CACHE_CONTROL => max-age=259200 SCRIPT_NAME => /cgi-bin/1.pl SCRIPT_FILENAME => /usr/local/www/cgi-bin/1.pl HTTP_ACCEPT_ENCODING => gzip SERVER_NAME => www.mojdodir.ru HTTP_PRAGMA => no-cache REQUEST_URI => /cgi-bin/1.pl HTTP_ACCEPT_CHARSET => iso-8859-1,*,utf-8 HTTP_X_FORWARDED_FOR => 15.0.0.23 SERVER_PORT => 30 HTTP_HOST => www.mojdodir.ru SERVER_ADMIN => webmaster@www.mojdodir.ru HTTP_VIA => 1.0 cache.www.mojdodir.ru:3363 (Squid/2.3.STABLE1)

Непосредственно из скрипта элементы хэша %ENV можно вызывать $ENV{'HTTP_CACHE_CONTROL'} или $ENV{'HTTP_USER_AGENT'}, смотря что нужно вызывать.

%FORM содержит данные, вводимые из формы методом POST: html форма такая:

<form action="/cgi-bin/1.pl" method="post">
<input type="text" name="name1" size=10 maxlength=10>
<input type="text" name="name2" size=10 maxlength=10>
<input type="text" name="name3" size=10 maxlength=10>
<input type="submit" value="send">
<input type="reset" value="reset"></form>

Если мы введем в поле name1 qwe, name2 rty, name3 asd и нажмем send, то через STDIN передаются данные в виде: name1=qwe&name2=rty&name3=asd и содержимое хэша

%FORM( name1 => qwe, name2 => rty, name3 => asd); Значения полей name* можно получать $FORM{'name1'}, $FORM{'name2'} и т.д.



Хэши slice (перевод, статья находится по адресу http://www.sysarch.com/perl/tutorials/hash_slices.txt)
Насколько хорошим является этот код?? @array = qw( a b c d ) ; @array{ @array } = ( [ @array ] ) x @array ; Основное определение хэша slice: @hash{ @array } = ( list ) что эквивалентно ( $hash{ $array[0] }, ... $hash{ $array[$#array] } ) = ( list ) Префиксный символ (@ ,%) используется только для обозначения контекста, но не типа данных. Если имеются два массива и мы ходим сопоставлять их данные, то мы поступаем так: @foo_array = qw( abc def ghi ) ; @bar_array = qw( jkl mno pqr ) ; @foo_to_bar{ @foo_array } = @bar_array теперь можно работать с массивами немного проще: $foo_value = 'def' ; $bar_value = $foo_to_bar{ $foo_value } ; Можно даже сразу преобразовывать целый массив переменных foo одной конструкцией: @bar_values = @foo_to_bar{ @foo_values }; Еще одно использование хэша slice, производится проверка: находится ли строка в данном массиве строк? Фактически мы не заботимся о том, что находится в хеше slice, но 1 используется для большей ясности. @foo_array = qw( abc def ghi ) ; @is_a_foo{ @foo_array } = (1) x @foo_array ; $input = 'def' ; if ( $is_a_foo{ $input } ) { ... if ( exists( $is_a_foo{ $input } ) ) { ... Оператор x называется оператором повторения, он дублирует левый операнд значением правого операнда. В скалярном контексте он копирует левый оперант как строку и возвращает его, т.е. мы имеем контекст списка и левый операнд списка. Это позволяет создать новый список с N копиями предыдущего списка. Для foo N = 3 и (скалярное значение @foo_array) и мы получаем список (1, 1, 1) который присваивается хешу slice. Программа, преобразующая строки в нумерованные индексные значения. Используется оператор диапазона (..), создающий список целых чисел, которые присваиваются хэшу slise: @foo_array = qw( abc def ghi ) ; @foo_to_index{ @foo_array } = ( 0 .. $#foo_array ) ; @foo_to_index{ @foo_array } = ( 1 .. @foo_array ) ; $i_am_a_foo = 'def' ; $foo_index = $foo_to_index{ $i_am_a_foo } ; Названия %foo_to_bar, %is_a_foo, $foo_to_index подобраны так, чтобы нагляднее показать принцип работы slise. Фактически была проведена индексация входных данных и выходных данных в хеш splice. Теперь рассмотрим хэш splice снова @array{ @array } = ( [ @array ] ) x @array ;. Заметим, что хэш и массив(и скаляр) называются массивами, но это различные переменные, в частности отличные по разделитеям. Левая часть выражения - присваивание хэшу slice, но что-же стоит справа? Оператор x и @array справа дублирует повторения счетчика и некоторый лист слева и мы имеем контекстный список. Хеш slice есть список контекстов, мы создаем скопированный список анонимного массива, который содержит значения в @array. Это значит что хэш %array напоминает такой массив: %array = ( 'a' => [ 'a', 'b', 'c', 'd' ], 'b' => [ 'a', 'b', 'c', 'd' ], 'c' => [ 'a', 'b', 'c', 'd' ], 'd' => [ 'a', 'b', 'c', 'd' ], ) ; Если имеется набор псевдонимов(alias), то можно, обращаясь к списку, вывести каждый из них в отдельности(ср. alias для сокращения часто употребляемой команды в unix). Т.е. вводится любой из отдельных элементов и мы получаем список данных. Если имеется список массивов, то их можно обработать таким образом: @foo_list = qw( a b c d ); @bar_list = qw( j k l m n o ); @baz_list = qw( w x ); @expand_aliases{ @foo_list } = ( [ @foo_list ] ) x @foo_list; @expand_aliases{ @bar_list } = ( [ @bar_list ] ) x @bar_list; @expand_aliases{ @baz_list } = ( [ @baz_list ] ) x @baz_list; Если есть лексемма неопределенного типа, то можно получить список псевдонимов за 1 шаг @aliases = @{ $expand_aliases{ $alias } } ; Окружение @{} используется, чтобы разыменовать сохраненный анонимный список в список для присвоения @aliases.