2010年9月6日月曜日

Perl best practices[Perlベストプラクティス] 8章 組み込み関数


このエントリーをはてなブックマークに追加


ソート中にソートキーを再計算しない



  • perlのソートはマージソートなので、sortの度にsortブロックが¥mathrm{O}(N¥log N)回呼び出される。

以下に記載されていた方法を紹介するが、それぞれ得手不得手があるので、使うときにベンチマークをすることが重要。
(8.8節では、ソートを最適化するための方法をもう一つ紹介されている。)

一度計算した結果をハッシュに登録しておく

Orcishと呼ばれる手法(キャッシュをOrするが名前の由来)

#スクリプトのSHA-512によるソート
use Digest::SHA qw( sha512 );

@sorted_scripts
= do {
my %sha512_of;

#ハッシュを参照して、ハッシュに登録されていなければ、$sha512($a)を計算して、結果をハッシュに登録
sort { ($sha512_of{$a} ||= ($sha512($a))
cmp
($sha512_of{$b} ||= ($sha512($b))
}
@scripts;
};



他でも計算結果を使い回す場合は%sha512_of宣言をソートの外に出して、スコープを変える。またその場合はdoブロックは要らない。

予めダイジェストを計算しておき、ソートする

この場合、ハッシュスライスが使える!

@sha512_of{@script} = map { sha512($_) } @scripts;



ただし、ソートから例外が出されて、ソートが途中で終わる可能性があるときは、予め計算したSHA-512ダイジェストが無駄になる。

schwartzian変換

このような方法もあるが、若干低速らしい。

#sortした値自体が欲しくないときに使う。
@sorted_scripts
= map  { $_->[0] }               #スクリプトを抽出
sort { $a->[1] cmp $b->[1] }   #ダイジェストでソート
map  { [$_, sha512($_)] }      #予め対応するスクリプトとダイジェストの結果を格納する。
@scripts;



サブルーチンの結果を記憶する

一番分かりやすいのは、サブルーチンの結果を記憶すること。(ただし前述のものよりは若干低速。schwartzianとはどうだろう・・・)
どういうことかと言うと、一度サブルーチンを呼び出すと、そのときの引数と結果を記憶していて、同じ引数で再度サブルーチンが呼び出された場合は、記憶している結果をそのまま使用するというもの。
PerlではMemoizeモジュールで簡単に実現できる。

use Digest::SHA qw( sha512 );
#SHA-512ダイジェスト関数の自己キャッシュ
use Memoize;
memoize('sha512');

#自動的にキャッシュされたSHA-512ダイジェストによるソート
@sorted_scripts = sort { sha512($a) cmp cha512($b) } @scripts;



リストの反転にはreverseを使う


分かりやすいし、早い。
@sorted_results = sort {$b cmp $ a} @unsorted_results;
より
@sorted_results = reverse sort @unsorted_results;
とした方が早いことすらある。


また、forループを逆順にループするときに使用すると、簡潔に書ける。

for my $remaining (reverse $MIN..$MAX) {
print "T minus $remaining, and counting...\n";
sleep $INTERVAL;
}



これをCスタイルループのデクリメントで実現しようとすると、前にも紹介した
Cスタイルの弱点が現れてしまう。
1、比較演算子の選択を誤る(<と<=を間違えるとか)
2、反復変数の更新をしくじる

スカラーの反転にもreverse


スカラーにreverseを適用すると文字列の文字を並べかえることもできる。
ただし、スカラーであることを明記すること。

my  $visible_email_address = scalar reverse $actual_email_address;


明記しないと、以下のようにサブルーチンの引数として使用した場合、
リストに作用して、ひとつしかないリスト要素を反転してしまう。

add_email_addr(reverse $email_address);#add_email_address(scalar reverse $email_address)とする。



固定幅のデータにはunpackを使用する


X123-S000001324700000199のような固定長のテキストデータの塊が使われていることがよくある。
ここから、特定文字列を取得することを考えると、


  1. substrはコードが汚くなるし、処理が遅い。

  2. 正規表現を使っても処理が遅い。




組み込み関数unpackはこのような時のために最適化された関数。'A'指定子を使用することで、文字列から文字を取り出すことができる。

Readonly my $RECORD_LAYOUT => 'A6 A10 A8'; #6文字 10文字 8文字の並びで指定

while (my $record = <$sales_data>) {
my ($ident, $sales, $price)#文字列右から6文字,10文字,8文字を、リストに格納
= unpack $RECORD_LAYOUT, $record;

#それぞれハッシュ登録してpush
push @sales, {
ident => translate_ID($ident),
sales => $sales * 1000,
price => $price,
};
}


データがX123-S 0000013247 00000199のように、何かで区切られている場合は@指定子を使用して、
文字列の何文字目がそれぞれのフィールドの先頭なのかを教える。

Readonly my $RECORD_LAYOUT
=> '@0 A6 @8 A10 @20 A8'
#@は昇順である必要はないので、好きな順序で文字列を取得することができる。



可変幅のデータにはsplitを使用


一般的に使用されていないが、splitの第3引数の使用を推奨する。
この第3引数はsplitから返されるフィールドの最大数を指定する。
予め返されるフィールドの数がわかっている場合は、数を指定することで、無駄な分割を防ぐことができる。
(例えば、splitの戻り値を受け取る変数が3つしかない場合、それ以上分割する必要はない)

ただし、注意するべきことは、
指定する第3引数は、取得したいフィールドの数+1を指定すること。
レコードから、「最初の3つ」のフィールドを取得するためには、レコードを4つに分割する必要がある。

複雑な可変幅のデータ


Text::CSV_XS、Text::CSV::Simple,Text::CSV

単純な文字列の並びならsplitで良いが、フォーマットルールは気まぐれに変化する。
これに対応するのは苦痛なので、モジュールを使用する。

Text::CSV_XSでは、フィールドセパレータ、エスケープ文字、フィールド引用符デリミタとして
使用される文字を指定し、フィールドから文字列を取得する。
Text::CSV::Simpleは、Text::CSV_XSのラッパーで、よりシンプルにモジュールを使用できる。

use Text::CSV::Simple;

my $csv_format
= Text::CSV::Simple->new({
sep_char    => q{,}, #フィールドセパレータ
escape_char => q{\\},#バックスラッシュのついた文字は常にデータ
quote_char  => q{"}, #フィールドは2重引用符で囲むことが可能
});

$csv_format->field_map( qw( ident sales price) ); #フィールドの順に名前を指定

#一度の呼び出しで、ファイル全体を読み込みハッシュリストに変換
#その後forでハッシュを使用してフィールド値を取得している。
for my $record_ref ($csv_format->read_file($sales_data)){
push @sales, {
ident => translate_ID($record_ref->{ident}),
sales => $record_ref->{sales} * 1000,
price => $record_ref->{price},
};
}




Text::CSVはピュアPerlで実装されているので、コンパイル済みのモジュールを使用できない環境なら
こちらを使用する。

文字列の評価


eval


  1. 処理に時間がかかる

  2. コンパイル時に警告が生成されない




evalを使用したい状況の代表は、
「ユーザから渡された式に基づいて、新しいサブルーチンを作成する」こと。

匿名クロージャによるサブルーチンの作成

作成予定のサブルーチンの名前と、作成するサブルーチンの機能を選択するオプションとなる引数をサブルーチン作成用のサブルーチンに渡して、
そこで、オプションで指定した匿名サブルーチンを作成する。
そして、Sub::Installerを用いて、名前と匿名サブルーチンを呼び出し元の名前空間にインストールする。
これは構文に誤りがあればコンパイルエラーになる。

ソートの自動化


Sort::Makerによるソートルーチンの作成
いろいろなソート方法を実現できる。
引数を降順でソートするようなよく使う単純なソートも名前付き引数で指定できるので、
積極的に使うようにする。

部分文字列


以下の組み込み関数に値を代入するという奇妙な構文は、
substrで指定した部分文字列を右辺の文字列で置換する機能をもつ。
だた、これは理解しにくく、かつ処理が遅い。

substr($addr, $country_pos, $COUNTRY_LEN)
= $country_name{$country_code};



perl5.6.1からはsubstrに第4引数が指定できるようになり、この引数で部分文字列の置換ができるようになった。
こちらの方が構文も自然で高速であるので、常にこちらを使用する。

ハッシュ値


ハッシュのvaluesはエイリアスなので、値を直接利用することができる。
ただし、perl5.6以前はできないので、インデックスキーを指定してアクセスする必要がある。

グロブ


入力演算(IOなど)ではない場合は、<>ではなく、グロブであることを明示するglob()を使用すること

my @files = <*.pl>;
はreadline処理と勘違いされやすいが、グロブである。


山括弧<>が入力演算の働きをするのは


  1. 空である場合<>

  2. 裸のワードだけ<DATA>

  3. 単純なスカラー変数<$input_file>



山括弧にそれ以外のものが含まれる場合は、ディレクトリ参照が実行される。
つまり、拡張子が*.plのファイル名のリストが返されることになる。

また、最悪なパターンとして、'*.pl'が定数として定義されてしまった場合、
スカラー変数を<>で囲むことになるので、グロブではなくなってしまう。


常に、
Readline my $FILE_PATTERN => '*.pl';
my @files = glob($FILE_PATTERN);
のようにする。

スリープ


組み込み関数sleepは整数の値しか対応していないが、selectの副作用を使ったスリープで1秒未満のスリープを実現しないこと。代わりにTime::HiRes::usleep関数を使用すること。
Time::HiRes::usleep関数を使用できない場合に、selectを使用する場合はサブルーチンでカプセル化すること。
ただし、サブルーチン呼び出しのオーバヘッドでわずかだか正確さに影響がでる。(気にしなくていい程度らしいが)

mapとgrep


mapとgrepの式は、常にブロック{}で囲むこと。そうしないと、後に続くリストと区別がつきにくくなる。

ユーティリティ 組み込みでない関数を使用する


次の3つのモジュールには便利な関数が複数存在する。


  1. Scalar::Util

  2. List::Util

  3. List::MoreUtils



詳しくは紹介しないけれど、first,reduce,apply,uniqなどがよく使いそう。




0 件のコメント:

コメントを投稿