UnixMountainSkiFun

Unix Горы Лыжи

07-06-2008 12:13

Глава 3. Полезные Perl-идиомы


В этой главе рассматриваются:

Имеется некоторое количество Perl-идиом, которые будут полезны во многих программах обработки данных. Давайте обсудим их в данной главе, прежде чем они встретятся ниже по тексту.

3.1 Сортировка

Сортировка это одна из наиболее общих задач, с которыми вы можете столкнуться когда будете заниматься обработкой данных. Как можно ожидать, Perl выполняет сортировку весьма просто (вместо вас), однако есть несколько пунктов, к которым мы вернемся чуть позже в этом разделе.

3.1.1 Простые сортировки

Perl имеет встроенную функцию sort, которая очень легко работает с простыми сортировками. Синтаксис функции sort следующий:

 @out = sort @in;

Она принимает элементы списка @in, лексически сортирует, и возвращает их в массив @@@out@. Это простейший сценарий. Обычно вам хочется чего-то более сложного, поэтому sort принимает и другой аргумент, который позволит вам определить именно ту сортировку, которую вы хотите получить. Этот аргумент либо имя процедуры, либо блок Perl-кода (заключенный в фигурные скобки). Например, чтобы отсортировать данные в числовом порядке (а точнее в лексическом порядке, когда 100 идет до 2), вам потребуется написать код похожий на такой:

 @out = sort numerically @in;

и процедуру, с названием numerically, которая может выглядеть так:

 sub numerically {
    return $a <=> $b;
 }

Необходимо сделать пару замечаний относительно этой процедуры. Во-первых, в ней используются две особых переменных $a и $b. Каждый раз когда Perk вызывает эту процедуру, эти переменные инициализируются в два значения из исходного массива. Ваша поцедура должна сравнить эти два значения и вернуть значение, которое будет показывать, какой из двух элементов должен идти первым в отсортированном списке. Вы должны вернуть -1, если $a идет перед $b, 1, если $b идет перед $a, и 0, если они одинаковы. Во-вторых, отметим оператор <=>, который принимает два значние и возвращает -1, 0 или 1, в зависимости от того, какое значние численно больше. Другими словами, эта функция сравнивает два значения и возвращает значения требуемые функцией sort. Если вам потребуется сортировка списка в реверсированном численном порядке, то вам всего лишь потребуется поменять порядок сравнения $a и $b, например, таким образом:

 sub desc_numerically {
    return $b <=> $a;
 }

Другой способ выполнить сортировку данных такого рода, а затем реверсировать список, будет состоять в использовании встроенной Perl-функции reverse, следующим образом:

 @out = reverse sort numerically @in;

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

 @out = sort lexically @in;

 sub lexically {
    return $a cmp $b;
 }

3.1.2 Сложные сортировки

Сортировки, которые мы только что обсуждали, обычно не пишутся, с использованием процедурного синтаксиса, вроде того, что приводился выше. В таких случаях используется блочный синтаксис. В блочном синтаксисе Perl-код помещается между функцией sort и обрабатываемым списком. Этот код, по прежнему должен работать с $a и $b, и должен подчиняться все тем же самым правилам, относительно возвращаемых значений. Таким образом, сортировки, которые мы обсуждали выше, могут быть переписаны таким образом:

 @out = sort { $a <=> $b } @in;
 @out = sort { $b <=> $a } @in; # либо @out = reverse sort { $a <=> $b } @in
 @out = sort { $a cmp $b } @in;

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

 my @out = sort namesort @in;

 sub namesort {
   return $a->{surname} cmp $b->{surname}
     || $a->{forename} cmp $b->{forename};
 }

Заметим, что мы пользуемся такой мощной функциональностью Perl, как "короткое замыкание" (прим.перев.:"short circuit") -- оператором ||. Второе сравнение будет выполнено только в том случае, если первое сравнение фамилий, вернет 0 (то есть, если фамилии одинаковы).

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

 my @out = sort namesort @in;

 sub namesort {
   return $a->{surname} cmp $b->{surname}
     || $a->{forename} cmp $b->{forename}
     || $b->{age} <=> $a->{age};
 }

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

3.1.3 Маневры орков

Простейший путь минимизировать эффект, от повторных вычислений сортировочного значения, состоит в кэшировании результатов каждого вычисления так, чтобы мы выполняли каждое из вычислений только один раз. Такой подход лежит в основе "маневров орков" (прим.перев.: Orcish Manoeuvre), этот каламбур придумал Joseph Hall. В этом методе, результаты предыдущих вычислений хранятся в хэше. Простейший код может выглядеть так:

 my %key_cache;

 my @out = sort orcish @in;

 sub orcish {

   return ($key_cache ||= get_sort_key($a))
     <=> ($key_cache ||= get_sort_key($b));
 }

 sub get_sort_key {

   # Код, который принимает элемент списка и возвращает
   # ту часть, которую вы желаете сортировать
 }

Здесь есть кое-что, заслуживающее более пристального рассмотрения. Хэш %key_cache используется для хранения вычисляемых ключей сортировки. Функция orcish выполняет сортировку, однако перед тем как вычислять сортировочный ключ, она, для каждого элемента, проверяет, не вычислялся ли такой ключ ранее (в этом случае он хранится в %key_cache). Она использует Perl-оператор ||=, чтобы сделать код более скоростным. Код

 $key_cache ||= get_sort_key($a)

может быть развернут в

 $key_cache = $key_cache || get_sort_key($a)

В результате работы такого кода, если $key_cache еще не существует, то вызов get_sort_key вычислит его, и результат будет сохранен в $key_cache. Точно такая же процедура используется для $b, а затем два результата сравниваются с использованием <=> (здесь, с легкостью, может быть использовано лексическое сравнение, с помощью cmp). В зависимости от того, насколько дорого стоит функция get_sort_key, этот метод может значительно увеличить производительность вашего кода, сортирующего большие списки.

3.1.4 Преобразование Шварца (Schwartzian transform)

Другой метод, позволяющий избежать повторного вычисления сортировочных ключей, состоит в использовании преобразования Шварца. Он был назван так после того, как Randal L.Schwartz, знаменитый член Perl-сообщества, автор многочисленных книг о Perl, был первым, кто отправил сообщение в новостную группу comp.lang.perl.misc, о использовании такой техники. В преобразовании Шварца, перед тем как выполнить сортировку, выполняется предварительное вычисление всех ключей сортировки.

В качестве примера вернемся к нашему списку компак-дисков. Если вы помните, в конце концов мы решили, что будем считывать данные из файла в массив хэшей, где каждый хэш содержит детальную информацию по каждому компакт-диску. Рисунок 3.1 показывает слегка упрощенную диаграмму того, что содержится в массиве @CD (каждый хэш имеет всего лишь два поля).

Рисунок 3.1 Несортированный массив хэшей компакт-дисков

Предположим, что теперь нам требуется построить список компакт-дисков, упорядоченный по дате выпуска. Методом "в-лоб" можно решить эту задачу, с использованием sort, следующим образом:

 my @CDs_sorted_by_year = sort { $a->{year} <=> $b->{year} } @CDs;

После этого можно перебирать отсортированный массив и распечатать интересующие нас поля хэша.

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

Для этого введем промежуточный массив. Каждый элемент массива будет ссылкой на двухэлементный массив. Первым элементом будет год, а вторым -- ссылка на наш прежний хэш. Такой список можно создать довольно просто с использованием map.

 my @CD_and_year = map { [$_->{year}, $_] } @CDs;

Рисунок 3.2 показывает как может выглядеть этот наш новый массив.

Рисунок 3.2 @CD_and_year содержит ссылки на двухэлементный массив

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

 my @sorted_CD_and_year = sort { $a->[0] <=> $b->[0] } @CD_and_year;

Рисунок 3.3 показывает этот наш новый массив.

Рисунок 3.3 @sorted_CD_and_year это @CD_and_year, отсортированный по первому элементу массива

Теперь в @sorted_CD_and_year содержится массив ссылок на массивы. Здесь, важно отметить, что этот массив отсортирован по году. В действительности, все, что нам нужно, это лишь второй элемент каждого из этих массивов потому, что это ссылка на наш исходный хэш. Использованием map довольно просто отбросить зерна от плевел.

 my @CDs_sorted_by_year = map { $_->[1] } @sorted_CD_and_year;

Рисунок 3.4 показывает как может выглядеть такой массив.

Рисунок 3.4 @CDs_sorted_by_year содержит только ссылки на хэш

Соберем все эти три кусочка вместе.

 my @CD_and_year = map { [$_, $_->{year}] } @CDs;
 my @sorted_CD_and_year = sort { $a->[1] <=> $b->[1] } @CD_and_year;
 my @CDs_sorted_by_year = map { $_->[0] } @sorted_CD_and_year;

Вкратце, это и есть преобразование Шварца -- sort, окруженный двумя map-ами. Однако тут есть еще кое-что, что требуется улучшить, прежде чем продолжить. Каждый из этих map-ов и sort-ов, получает массив, в качестве входного, и возвращает массив. Таким образом, мы можем сцепить все эти преобразования воедино, в один оператор, и избавиться от промежуточных массивов.

 my @CDs_sorted_by_year = map { $_->[0] }
                          sort { $a->[1] <=> $b->[1] }
                          map { [$_, $_->{year}] } @CDs;

Если это не похоже на то, что у нас было до того, попробуйте отследить все в обратном порядке. Исходный массив (CD) поступает к нам внизу. Он проходит через map, который разыменовывает хэш, затем выполняет сортировку, и, наконец, выполняется последний map.

Соединение воедино нескольких функций обработки списков, когда вывод первого map, становится вводом sort и т.д., очень напоминает программные каналы, которые мы видели, когда ранее рассматривали модель "UNIX-фильтра".

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

print map { $_->[0] }

      sort { $a->[1] cmp $b->[1] }
      map { [$_, (split /\t/)[2]] } <STDIN>;

3.1.5 Преобразование Гуттмана-Рослера (Guttman-Rosler transform)

В ходе Perl Conference 1999 года, Ури Гуттман и Ларри Рослер представили статью о сортировке в Perl. Она рассматривала все, обсуждаемые нами, методики, а также продвинулась дальше, введением концепции "упакованная по-умолчанию сортировка" (прим.перев.: packed-default sort). Они начали с того, что сделали два утверждения:

  1. Исключение операций разыменования хэша или массива приведет к ускорению сортировки.
  2. Лексическая сортировка, используемая по-умолчанию (без каких бы то ни было процедур или блоков сортировки), наискорейший вариант.

Получившийся метод является интересной вариацией преобразования Шварца. Вместо преобразования каждого элемента списка в двухэлементный список (ключ сортировки и исходные данные), и последующей сортировке по первому элементу такого списка, Гуттман и Рослер предложили преобразовать каждый элемент исходного списка в строку, с ключем в ее начале, и исходным элементом в ее конце. После этого список, содержащий такие строки, может быть лексически отсортирован, а исходные данные извлечены из отсортированного списка.

Пример, который они использовали в своей статье, явлется сортировкой IP-адресов. Во-первых они преобразовали каждый элемент в строку, в которой каждая часть IP-адреса кодировалась (с использованием pack) значением ASCII-символа. Исходные данные добавлялись в конец такой четырех-символьной строки:

 my @strings = map { pack('C4', /(\d+)\.(\d+)\.(\d+)\.(\d+)/) . $_ } @IPs;

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

 my @sorted_strings = sort @strings

и, наконец, исходные данные изволекались.

 my @sorted @IPs = map { substr($_, 4) } @sorted_strings;

Переписывая сказанное выше, чтобы получить нечто более похожее на преобразование Шварца, получаем следующее:

 my @sorted_IPs = map { substr($_, 4) }
                  sort
                  map { pack('C4', /(\d+)\.(\d+)\.(\d+)\.(\d+)/) . $_ } @IPs;

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

3.1.6 Выбор метода сортировки

Если вы испытываете проблемы с производительностью, в своей программе, использующей сложные сортировки, то вполне возможно, что использование одной из методик, описанную в данном разделе, позволит ускорить ваш скрипт. Однако, весьма вероятно, что ваш скрипт может и замедлиться. Каждый из методов будет улучшать время сортировки, но это также имеет и оборотную сторону в том смысле, что исходные данные должны быть весьма объемными, чтобы улучшение стало заметным. При выборе метода сортировки, чтобы выбрать наиболее приемлимый, важно воспользоваться методами определения быстродействия (прим.перев.: benchmarking), обсуждаемыми в разделе 3.4. Конечно, если предполагается, что ваш скрипт будет выполняться только один раз, то тратить половину дня на определение наиболее быстрой сортировки, чтобы в результате получить выигрыш в скорости в пять секунд, будет "Пирровой победой".

Этот раздел, в действительности, только-только касается обширной темы сортировки в Perl. Если вы заинтересованы узнать больше, то статья Гуттмана и Рослера, то самое место с которого надо начать исследование. Вы можете найти ее online по данной ссылке -- http://www.hpl.hp.com/personal/Larry_Rosler/sort/.

3.2 DBI -- интерфейс к базам данных.

Как обсуждалось в главе 1, одним из общих источников или приемников данных может быть база данных. Perl уже давно имеет механизмы, позволяющие ему общаться с системами управления базами данных. Например, если требуется обменяться данными с базой данных Oracle, то вы должны использовать oraperl, а если с Sybase, то sybperl. Доступны и модули и для многих других популярных систем управления базами данных.

Большинство из таких модулей, предоставляющих доступ к базам данных, являются "обертками" (прим.перев.: wrapper) вокруг программного интерфейса (API), который предоставляют разрабочики баз данных. Механизмы, для обмена информацией с различными системами управления базами данных, по большей части выполняют одни и те же вещи, однако все это делается совершенно неодинаковым образом.

В последние годы, все изменилось, с появлением обощающего модуля Perl Database Interface -- DBI. Этот модуль был разработан и реализован Тимом Бансом (Tim Bunce), автором и мэйнтейнером oraperl. Этот модуль позволяет программе соединяться с любой, поддерживаемой, системой управления базой данных, считывать и записывать данные, используя в точности один и тот же синтаксис. Единственное изменение, которое потребуется соделать при переходе к другой системе управления базой данных, будет изменение одной строки, которая передается DBI-функции connect. Это достигается использованием различных модулей-драйверов баз данных (DBD). Все эти модули именуются таким образом -- DBD::<db_name>. Если вы планируете работать с какой-либо базой данных, то потребуется раздобыть соответствующий DBD-модуль, совершенно независимо от основного DBI-модуля.

3.2.1 Простейшая DBI-программа

Простейшая DBI-программа, считывающая данные из базы данных, может выглядеть следующим образом:

 1: #!/usr/local/bin/perl -w
 2:
 3: use strict;
 4: use DBI;
 5:
 6: my $user = 'dave';
 7: my $pass = 'secret';
 8: my $dbh = DBI->connect('dbi:mysql:testdb', $user, $pass,
 9: {RaiseError => 1})
 10: || die "Connect failed: $DBI::errstr";
 11:
 12: my $sth = $dbh->prepare('select col1, col2, col3 from my_table')
 13:
 14: $sth->execute;
 15:
 16: my @row;
 17: while (@row = $sth->fetchrow_array) {
 18: print join("\t", @row), "\n";
 19: }
 20:
 21: $sth->finish;
 22: $dbh->disconnect;

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

Строка 1 указание интерпретатору Perl. Заметьте, что мы используем флаг -w.

Строка 3 включает прагму strict.

Строка 4 включает модуль DBI.pm. Это позволяет нам использовать DBI-функции.

Строки 6 и 7 определяют имя пользователя и пароль, которые мы будем использовать при подключении к базе данных. Очевидно, что в настоящей программе мы вряд ли захотим использовать, явным образом написанный, пароль.

Строка 8 соединяет нас с базой данных. В этом случае мы соединяемся с базой данных под управлением MySQL. Эта, свободная программа управления базами данных, весьма популярна для web-систем. Это единственная строка, которую потребуется изменить, если мы захотим соединиться с другой системой управления базами данных. Функция connect имеет некоторое количество параметров, которые могут варьироваться в зависимости от базы данных, к которой мы присоединяемся. Первый параметр -- строка подключения. Она меняется в зависимости от базы к базе данных, однако всегда используется строка, разделенная двоеточиями. Первая часть такой строки -- dbi, вторая часть -- всегда имя системы управления базами данных, к которой мы присоединяемся (или, если говорить более точно, то имя DBD-модуля, который мы используем, чтобы присоединиться к базе данных). В нашем случае строка mysql говорит DBI, что мы будем общаться с базой данных MySQL, и таким образом ему потребуется загрузить модуль DBD::mysql. Третья часть строки подключения, в нашем случае, -- это конкретная база данных, с которой мы желаем соединиться. Многие системы управления базами данных (включая MySQL) могут хранить несколько различных баз данных на одном и том же сервере баз данных. В данном случае мы желаем соединиться с базой данных под названием testdb. Второй и третий параметры -- это существующие имя и пароль, для соединения с данной конкретной базой данных. Четвертый параметр DBI->connect -- это ссылка на хэш, содержащий различные конфигурационные опции. В этом примере мы включаем опцию RaiseError, которая будет автоматически генерировать фатальную ошибку времени исполнения (прим.перев.: fatal run-time error), если случится соответствующая ошибка базы данных.

Функция DBI->connect возвращает хэндл базы данных, который позже может быть использован для доступа, в остальных DBI-функциях. Если произойдет ошибка, то функция возвращает undef. В простейшей программе мы проверяем на это значение, и если случается проблема, то программа оканчивает свое выполнение после того, как напечатает значение переменной $DBI::errstr, которая содержит самое последнее сообщение об ошибке базы данных.

Строка 12 подготавливает SQL-оператор для выполнения в базе данных. Это достигается вызовом DBI-функции prepare. Эта функция возвращает хэндл оператора, который может быть использован для доступа к другому набору DBI-функций, -- которые связаны с выполнением запросов к базе данных, чтению и записи данных. Этот хэндл неопределен (undef) если происходит ошибка подготовки SQL-оператора.

Строка 14 выполняет оператор или "умирает", если случается ошибка.

Строка 16 определяет переменную-массив, который будет содержать все строки данных, выбранные из базы данных.

Строки с 17 по 19 определяют цикл, который получает каждую строку из запроса к базе данных и печатает ее. В строке 17 вызывается fetchrow_array, которая возвращает список, содержащий по одному элементу на каждый из стобцов, следующей записи результирующего набор (прим.перев.: result set). Когда весь результирующий набор будет выбран, следующий вызов fetchrow_array вернет значение undef.

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

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

Это было очень быстрое введение в использование DBI. Имеются и другие функции, и наиболее полезные из них перечислены в Приложении A. Более детальная документация поставляется с модулем DBI и избранными вами модулями DBD.

3.3 Data::Dumper

Когда ваши структуры данных становятся все более и более сложными, тогда все более важным становится иметь простой способ увидеть, что они собой представляют. Наиболее удобным способом сделать это, -- воспользоваться модулем Data::Dumper, который поставляется, как стандартная часть Perl-дистрибутива. Data::Dumper получает одну или более переменную, и производит символьную версию данных, содержащихся в переменных.

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

 my @CDs;
 my @attrs = qw(artist title label year);
   while (<STDIN>) {
     chomp;
     my %rec;
     @rec{@attrs} = split /\t/;
     push @CDs, \%rec;
   }

Чтобы использовать Data::Dumper, необходимо добавить оператор use Data::Dumper и сделать вызов Dumper-функции, например таким образом:

 use Data::Dumper;
 my @CDs;
 my @attrs = qw(artist title label year);
 while (<STDIN>) {
   chomp;
   my %rec;
   @rec{@attrs} = split /\t/;
   push @CDs, \%rec;
 }

 print Dumper(\@CDs);

Запустив на выполнение эту программу, использующую, в качестве входных, наши CD-файлы, получим следующий результат:

$VAR1 = [

           {
            'artist' => 'Bragg, Billy',
            'title' => 'Workers\' Playtime',
            'year' => '1987',
            'label' => 'Cooking Vinyl'
           },
           {
            'artist' => 'Bragg, Billy',
            'title' => 'Mermaid Avenue',
            'year' => '1998',
            'label' => 'EMI'
           },
           {
            'artist' => 'Black, Mary',
            'title' => 'The Holy Ground',
            'year' => '1993',
            'label' => 'Grapevine'
           },
           {
            'artist' => 'Black, Mary',
            'title' => 'Circus',
            'year' => '1996',
            'label' => 'Grapevine'
           },
           {
            'artist' => 'Bowie, David',
            'title' => 'Hunky Dory',
            'year' => '1971',
            'label' => 'RCA'
           },
           {
            'artist' => 'Bowie, David',
            'title' => 'Earthling',
            'year' => '1998',
            'label' => 'EMI'
           }
        ];

Это весьма понятное представление нашей структуры данных.

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

3.4 Определение быстродействия

Когда мы выбираем среди различных способов реализации некоторой задачи в Perl, часто бывает полезным узнать какой из вариантов быстрейший. Perl предоставляет модуль Benchmark, который позволяет с легкостью решить задачу, получения таких сведений. Этот модуль содержит несколько функций (для дополнительной информации смотри документацию), однако наиболее полезной, для сравнительной оценки производительности различных участков кода, является timethese. Эта функция использует некоторое количество частей кода, несколько раз запускает каждый из них на выполнение, и возвращает время исполнения каждого из их. Другими словами, -- требуется разбить каждый из ваших вариантов, на отдельные функции, каждая из которых выполняет одну и ту же вещь, но разными способами, и передать эти функции в timethese. Наприме, имеется четыре способа поместить значение переменной в середину некоторой постоянной строки. Вы можете интерполировать переменную непосредственно внутри строки

 $str = "Значением будет $x (ну, или что-то типа того)";

или выполнить join над значениями

 $str = join '', 'Значением будет ', $x, ' (ну, или что-то типа того)';

или выполнить конкатенацию значений

 $s = 'Значением будет ' . $x . ' (ну, или что-то типа того)';

или, в конце концов, использовать sprintf.

 $str = sprintf 'Значением будет %s (ну, или что-то типа того)', $x;

Чтобы вычислить какой из этих методов быстрейший, вы можете написать скрипт, похожий на этот:

 #!/usr/bin/perl -w
 use strict;
 use Benchmark qw(timethese);

 my $x = 'x' x 100;

 sub using_concat {
   my $s = 'Значением будет ' . $x . ' (ну, или что-то типа того)';
 }

 sub using_join {
   join '', 'Значением будет ', $x, ' (ну, или что-то типа того)';
 }

 sub using_interp {
   my $str = "Значением будет $x (ну, или что-то типа того)";
 }

 sub using_sprintf {
   $str = sprintf 'Значением будет %s (ну, или что-то типа того)', $x;
 }

 timethese (1E6, {
   'concat' => \&using_concat,
   'join' => \&using_join,
   'interp' => \&using_interp,
   'sprintf' => \&using_sprintf,
 });

На моем современном компьютере (скорее древнем, -- 200 MHz P6, RAM 64 MB, под управлением Microsoft Windows 98, и ActivePerl build 521), выполнение такого скрипта дало следующий результат:

 Benchmark: timing 1000000 iterations of concat, interp, join, sprintf :
    concat:  8 wallclock secs ( 7.36 usr + 0.00 sys  =  7.36 CPU) @ 135869.57/s (n=1000000)
    interp:  8 wallclock secs ( 6.92 usr + -0.00 sys =  6.92 CPU) @ 144508.67/s (n=1000000)
      join:  9 wallclock secs ( 8.38 usr + 0.03 sys  =  8.41 CPU) @ 118906.06/s (n=1000000)
   sprintf: 12 wallclock secs (11.14 usr + 0.02 sys  = 11.16 CPU) @ 89605.73/s  (n=1000000)

Что все это значит? Просматривая скрипт, мы можем заметить, что мы вызываем функцию timethese, передавая ей целое число и ссылку на хэш. Целочисленное значение -- это, желаемое вами, число тестовых запусков. Хэш содержит детализацию о тестируемом вами коде. Ключи такого хэша -- уникальные имена каждой из подпрограмм, а значения -- ссылки на сами функции. timethese выполнит каждую из ваших функций, указанное число, раз и отпечатает результаты. Из результатов, представленных выше, видно, что наши функции можно разбить на три множества. concat и interp потребляют примерно 8 секунд процессорного времени (прим.перев.: CPU time), на выполнение 1'000'000 прогонов; join выполняется несколько дольше -- 9 секунд; а sprintf выполняется 12 секунд процессорного времени.

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

3.5 Скрипты командной строки

Очень часто скрипты обработки данных разрабатываются, чтобы выполнять одну единственную задачу. Предположим, что вы получили файл данных. который необходимо подчистить, прежде чем загрузить в базу данных. Конечно вы можете написать полноценный Perl-скрипт, чтобы реализовать такую обработку, однако Perl предоставляет вам множество опций командной строки, которые позволят с легкостью решить задачи такого рода прямо в командной строке. Такой подход зачастую бывает более эффективным. Простейшей опцией для обработки из командной строки является -e. Текст, следующий за этой опцией, трактуется как Perl-код, и пропускается через Perl-интерпретатор. Таким образом, вы можете написать скрипты, наподобие такого:

 perl -e 'print "Hello world\n"'

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

Если код, который требуется выполнить, нуждается в модуле, который обычно подключается оператором use, вы можете использовать опцию -M, чтобы загрузить необходимый модуль. Например, такая возможность с легкостью позволяет выяснить версию модуля, инсталированного в вашей системе (считая, что модуль использует стандартную практику определения переменной $VERSION), используя код, похожий на этот:

 perl -MCGI -e 'print $CGI::VERSION'

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

 LINE:
 while (<>) {
   # ваш -e код выполняется в этом месте
 }

Такой подход может быть использован, например, чтобы написать простейший grep-скрипт, типа такого:

 perl -ne 'print if /искомый текст/' file.txt

который отпечатает каждую строку file.txt, которая содержит строку "искомый текст". Заметим, что наличие метки LINE позволяет вам написать код, с использованием next LINE. Если вы изменяете данные в файле, и желаете отпечатать результат для каждой строки, то необходимо использовать опцию -p, которая печатает содержимое $_ в конце каждой итерации цикла while. Код, который реализует такой подход, может выглядеть так:

 LINE:
 while (<>) {
   # ваш -e код выполняется в этом месте
   } continue {
     print
   }

В качестве примера использования этой опции представьте, что необходимо "сжать" несколько нулей в записи в один-единственный. Вы можете написать примерно такой код:

 perl -pe 's/0+/0/g' input.txt > output.txt

В примерах которые мы до сих пор видели, результат скрипта записывается на STDOUT (вот почему, в последнем примере, мы перенаправили STDOUT в другой файл). Есть и другая опция -- -i, кторая позволяет нам обработать файл "по-месту" (прим.перев.: in place), и опционально создать резервную копию (прим.перев.: backup) предыдущей версии нашего файла. За опцией -i следует строка, которая будет расширением, добавляемым к резервной копии файла, таким образом, можно переписать наш предыдущий пример так:

 perl -i.bak -pe 's/0+/0/g' input.txt

Эта опция сделает так, что измененные данные будут в input.txt, а исходные данные в input.txt.bak. Если вы не укажите после -i расширение, то резервной копия не будет создана (так что будьте уверены, что вы знаете, что делаете!).

Имеются и другие опции, которые могут сделать вашу жизнь намного легче. Использование -a включает авторазбиение (прим.перев.: autosplit), который, в свою очередь, разбивает каждую входную строку в @F. По умолчанию, авторазбиение разбивает строку, используя любое количество "пробельных символов" (прим.перев.: white space), однако вы можете изменить символ-разделитель, использованием -F. Другими словами, чтобы напечатать набор имен пользователей из /etc/passwd, можно воспользоваться таким кодом:

 perl -a -F':' -ne 'print "$F[0]\n"' < /etc/passwd

Опция -l включает обработку окончания строки. В результате, когда используем опции -n или -p, то над каждой строкой автоматически выполняется chomp. Кроме того, можно указать опциональное восьмиричное число, которое изменит разделитель записей $\ (специальные переменные, вроде $\, более детально рассматриваются в Главе 6). Это значение добавляется в конце каждой строки, выдаваемой на печать. Если не указать восьмеричное число, то $\ установлено в то же самое значение как и в разделителе записей поступающих на ввод ($/). Значением по-умолчанию будет являться новая строка (прим.перев.: newline). Вы можете изменить значние $/, используя опцию -0 (это перечеркнутый ноль, а не перечеркнутое О).

Чтобы символ новой строки автоматически удалялся из ваших входных строк, и автоматически добавлялся к результирующим строкам, необходимо воспользоваться опцией -l. Например, предыдущий пример с /etc/passwd может быть переписан следующим образом:

 perl -a -F':' -nle 'print $F[0]' < /etc/passwd

Больше информации об опциях командной строки можно получить на странице руководства prelrun, которая устанавливается, когда вы инсталлируете Perl.

3.6 Дополнительная информация

Дополнительно, обсуждение преобразования Шварца, Маневра Орков, а также других трюков Perl'а могут быть найдены в книгах Effective Perl Programming, изд.Addison-Wesley, авторы Joseph Hall и Randal Schwartz, и The Perl Cookbook, изд.O'Reilly, авторы Tom Christiansen и Nathan Torkington.

Более академическое рассмотрение сортировки в Perl, обсуждается в Mastering Algorithms with Perl, изд.O'Reilly, авторы Jon Orwant, Jarkko Hietaniemi и John Macdonald. Дополнительная информация об определении быстродействия может быть найдена в документации модуля Benchmark.pm.

Дополнительная информация о модулях DBI и DBD может быть обнаружена в Programming the Perl DBI, изд.O'Reilly, авторы Tim Bunce и Alligator Descartes, а также в документации, которая инсталируется вместе с данными модулями. Когда вы установите DBI-модуль, то можете прочитать документацию, используя команду

 perldoc DBI

набирая ее в своей командной строке. Сходным образом, можно прочитать документацию о любом, установленном DBD-модуле

 perldoc DBD::<name>

набирая это в командной строке. Вы должны заменить <name> на имя того DBD-модуля, который вы установили, например "Sybase" or "mysql".

3.7 Резюме

* Сортировка в Perl может быть весьма простой, однако существуют и более сложные методы сортировки, которые могут выполняться более эффективно.

  • В Perl реализован весьма удобный доступ к базам данных, с использованием DBI.
  • Data::Dumper очень полезный инструмент, чтобы увидеть что из себя представляют ваши внутренние структуры данных.
  • Определение быстродействия весьма важно, однако чтобы правильно его оценить, придется попотеть.
  • Скрипты командной строки могут быть, на удивление, мощными.

<< Глава 2. Наиболее общие приемы обработки данных | Data Munging With Perl | Глава 4. Поиск по шаблону >>


edit RightSideBar