[ ↑ INDEX ] [ ← PREV ] [ NEXT → ]

Lesson 9 ハッシュ


■1. ハッシュ:「配列」の要素を文字列で指定する

配列と同じように0個以上の値を持つことができるものにハッシュ (連想配列とも呼ばれる) がある。ハッシュは名前の先頭に % を付けて表わす。

   %ハッシュ名       cf. @配列名
%ハッシュ名」でハッシュを表すが,以下の説明では,「ハッシュ」・「%ハッシュ名」の代わりに「%ハッシュ」と書くことがある。

配列との違いは,配列が要素を,配列中の位置を表わすインデックスで指定するのに対し,ハッシュでは文字列で指定するという点である。個々の要素は次の形式で表わす。配列のときと同様,スカラーデータなので $ が付いている点に注意。

   $ハッシュ名{キー}    cf. $配列名[インデックス]

キーが文字列そのものなら引用符で囲む。変数や式で指定することも可能。

   $phone{'John'}     #%phone の,キーが 'John' の要素
   $phone{$name}      #%phone の,$name の値がキーの〃
   $phone{uc 'John'}  #%phone の,キーが 'JOHN' の要素

                       [その他の例]

ハッシュへの値の代入はリストを使って行う。

   %ハッシュ = (キー0, 値0, キー1, 値1, キー2, 値2, ...)

次のように,キーと値の間のコンマの代りに => を使ってもよい。

   %ハッシュ = (キー0 => 値0, キー1 => 値1, キー2 => 値2, ...)
次のように値を代入すると,ハッシュに該当するキーが存在しない場合は新たにキーと値のペアを作る。もし該当するハッシュが存在しない場合は,ハッシュそのものが自動的に作られる。
   $ハッシュ名{キー} = 

キーの重複は認められないので,指定したキーが既に存在した場合は,値が上書きされる。


◇ スクリプトの実行

次のスクリプトを実行し,結果を確認しなさい。なぜそのような結果になるのか,スクリプトの各行の意味を確認しなさい。

   script/price.pl
   --------------------------------------------------
   #ハッシュ      キー        値
   %price   =  ('notebook',  200,
                'pencil',     50,
                'eraser',    150,
                'folder',    100);

   print  "Type one of the 4 items:  ",
          "notebook, pencil, eraser, folder\n",
          "([ENTER] to exit)\n\n";

   while (<STDIN>) {
      exit if (/^\s*$/);
      chomp;
      print "$price{$_} yen\n\n";
   }

   exit;
   --------------------------------------------------

■2. 要素の取り出し

・キーを取り出す:keys

ハッシュのすべてのキーを取り出すには keys 関数を使う。次のようにすると,ハッシュ中のすべてのキーが配列に代入される。

   @配列 = keys %ハッシュ

・キーと値の組を取り出す:each

ハッシュのキーと値の組を取り出すには each 関数を使う。下のようにすると,$変数1にキーが,$変数2に値が代入される。

   ($変数1, $変数2) = each %ハッシュ

each 関数は一度に一組のキーと値しか取り出さないので,全要素を取り出すには,while などと組み合わせて使う。

   while (($変数1, $変数2) = each %ハッシュ) { 処理 }

keyseach も引数のハッシュ自体を変更するものではない。キーや要素を取り出しても,ハッシュの内容は元のままである。(cf. shift, pop)


◇ スクリプトの実行:各語の頻度を出す

次のスクリプトのうち一つを実行し,結果を確認しなさい。なぜそのような結果になるのか,スクリプトの各行の意味を確認しなさい。

   script/wordfreq2a.pl
   --------------------------------------------------
   while (<>) {
      while (/\b\S+\b/g) {
         $freq{$&}++;    #$& は直前の正規表現にマッチした文字列
      }
   }

   foreach $i (keys %freq) {
      printf "%4d %s\n", $freq{$i}, $i;
   }

   exit;
   --------------------------------------------------
   script/wordfreq2b.pl
   --------------------------------------------------
   while (<>) {
       s/^\s+//; s/\s+$//;
       @words = split;
       foreach $i (@words) {
          $freq{$i}++;
       }
   }

   foreach $j (keys %freq) {
      printf "%4d %s\n", $freq{$j}, $j;
   }

   exit;
   --------------------------------------------------
   script/wordfreq2c.pl
   --------------------------------------------------
   while (<>) {
       s/^\s+//; s/\s+$//;
       @words = split;
       foreach $i (@words) {
          $freq{$i}++;
       }
   }

   while (($key, $value) = each %freq) {
      printf "%4d %s\n", $value, $key;
   }

   exit;
   --------------------------------------------------
注意 上のスクリプトはどれも正確な語の頻度を出すものとはなっていない。より正確に語の頻度が出せるように手を加える必要があるが,その作業は練習問題で行う。

■3. 要素を順序付けて取り出す

配列と違って,ハッシュ中の要素は順序付けられていない。(リストを使って代入しても,リスト中での順序は保持されない。)


             キー0 キー1 キー2  ... キーn
   %ハッシュ   ↓    ↓    ↓         ↓    ← 順序付けなし
              値0   値1   値2   ...  値n

                  インデックス
               0    1    2     ...    n
   @配列    ─────────────→
             (値0, 値1, 値2,   ...   値n) 

ハッシュの要素の間には順序がなく,sortreverse などで直接操作することはできないので,キーをリストとして取り出し,そのリストを操作することで行う。


  foreach $変数 (sort { 定義 } keys %ハッシュ) {    

    ... $変数 ... $ハッシュ名{$変数} ...

  }

wordfreq2a.pl などでは出力の順序はランダムだったが,次のようにすれば,キーの文字コード順で出力できる。

   --------------------------------------------------
   foreach $i (sort {$a cmp $b} keys %freq) {
      printf "%4d %s\n", $freq{$i}, $i;
   }
   --------------------------------------------------

ソートの定義の例をいくつか示す。(%hash というハッシュがあると仮定。)

   #キーの文字コード順で,大文字小文字を区別せずにソート
   sort {lc $a cmp lc $b} keys %hash

   #値を数値として比較してソート (昇順)
   sort {$hash{$a} <=> $hash{$b}} keys %hash

   #値でソート (降順),同じ値のものはキーの文字コード順 (昇順)
   sort {$hash{$b} <=> $hash{$a} || $a cmp $b} keys %hash

ソートの定義が複雑になった場合,サブルーチンを使うとすっきりする。

   --------------------------------------------------
   foreach $i (sort by_freq keys %hash) {
      ... 処理 ...
   }

   sub by_freq {
      $hash{$b} <=> $hash{$a}
         ||
      $a cmp $b
   }
   --------------------------------------------------
注意 $a, $b, %hashmy により局所変数として宣言してはならない。

サブルーチン名を変数で指定し,条件により変数の内容を変えることにより,ソートの順序を変えることもできる。

   --------------------------------------------------
   if (条件) {$order = 'by_abc'} else {$order = 'by_freq'};

   foreach $i (sort $order keys %hash) {
      ... 処理 ...
   }

   sub by_abc {
      lc $a cmp lc $b
         ||
      $a cmp $b
   }

   sub by_freq {
      $hash{$b} <=> $hash{$a}
         ||
      lc $a cmp lc $b
         ||
      $a cmp $b
   }
   --------------------------------------------------

◇ スクリプトの実行:語彙頻度表をアルファベット順・頻度順で出力する

次のスクリプトを実行し,結果を確認しなさい。なぜそのような結果になるのか,スクリプトの各行の意味を確認しなさい。意味が確認できたら,ソートの仕方を変えて実行しなさい。

   script/wordfreq3.pl
   --------------------------------------------------
   while (<>) {
      while (/\b\S+\b/g) {
         $freq{$&}++;
      }
   }

   foreach $i (sort {$a cmp $b} keys %freq) {
      printf "%4d %s\n", $freq{$i}, $i;
   }

   exit;
   --------------------------------------------------
   #大文字小文字を区別せずにソート,同じなら大文字のものが先
   {lc $a cmp lc $b  || $a cmp $b}

   #頻度順(降順)
   {$freq{$b} <=> $freq{$a}}

   #頻度順(降順)で,同頻度のものはアルファベット順(昇順)
   {$freq{$b} <=> $freq{$a} || lc $a cmp lc $b  || $a cmp $b}

■4. 要素の存在の確認,削除

・ハッシュに存在するかどうかの確認: exists

exists は,指定の要素が存在すれば真を,しなければ偽を返す。

  exists  $ハッシュ{キー}

使用例

   script/price2.pl
   --------------------------------------------------
   #ハッシュ      キー        値
   %price   =  ('notebook',  200,
                'pencil',     50,
                'eraser',    150,
                'folder',    100);

   print  "Type one of the 4 items:  ",
          "notebook, pencil, eraser, folder\n",
          "([ENTER] to exit)\n\n";

   while (<STDIN>) {
      exit if (/^\s*$/);
      chomp;
       if (exists $price{$_}) {
         print "$price{$_} yen\n\n";
      } else {
         print "not registered\n\n";
      }
   }

   exit;
   --------------------------------------------------

exists と異なり,defined は,指定の要素が定義されていれば真を,されていなければ偽を返す。

   defined $ハッシュ{キー}

上の例では,exists を使っても defined を使っても同じ結果になるが,常に交換可能というわけではない。$ハッシュ{キー} は存在するが,その値が未定義である場合,exists は真を,defined は偽を返すので,違いが生じることになる。次の例では,その違いを基に,表示するメッセージを切り替えている。

   --------------------------------------------------
   #ハッシュ      キー        値
   %price   =  ('notebook',  undef,  #←値が未定義値
                'pencil',       50,
                'eraser',      150,
                'folder',      100);

   print  "Type one of the 4 items:  ",
          "notebook, pencil, eraser, folder\n",
          "([ENTER] to exit)\n\n";

   while (<STDIN>) {
      exit if (/^\s*$/);
      chomp;
      if (defined $price{$_}) {
         print "$price{$_}円\n\n";
      } elsif (exists $price{$_}) {
         print "価格は未定です\n\n";
      } else {
         print "登録されていません\n\n";
      }
   }

   exit;
   --------------------------------------------------

・要素の削除: delete

   delete $ハッシュ{キー}

□ 練習問題

練習問題A:必ずやること。

  1. wordfreq3.pl に手を加え,単語の認定をより正確に行えるようにしなさい。(wordfreq2b.pl を基にしてもよい。)

  2. wordfreq3.pl にさらに手を加え,大文字・小文字を区別せずに単語を数えられるようにしなさい。

練習問題B:余裕があったらやってみよう。

  1. 次のスクリプトを実行し,ランダムな順序で入力された曜日名 (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday) が曜日の順番でソートされて出力されることを確認した後,なぜそのような結果になるのか,スクリプトの内容を確認しなさい。

       script/sort_wdays.pl
       ------------------------------------------------------------
       %order = ('Sunday',    0,
                 'Monday',    1,
                 'Tuesday',   2,
                 'Wednesday', 3,
                 'Thursday',  4,
                 'Friday',    5,
                 'Saturday',  6);
    
       print STDERR "英語の曜日名を2つ以上入力してください:\n";
       @days = split (/\s+/, <STDIN>);
    
       print STDERR "ソート結果:\n";
       print ((join " ", sort {$order{$a} <=> $order{$b}} @days), "\n");
    
       exit;
       ------------------------------------------------------------
    
  2. 上のスクリプトに手を加え,省略された表記 (Sun, Mon, Tue, Wed, Thu, Fri, Sat) も一緒に扱えるようにしなさい。省略のない表記と省略された表記 (e.g. Sunday と Sun) では,前者が先に来るようにすること。

  3. 以下のような形式で,大文字小文字を区別しない数と,区別した場合の内訳を表示するスクリプトを考えなさい。

       ----------------------------------------
          8 LATER
                  1 Later
                  7 later
       
          4 LATEST
                  1 LATEST
                  3 latest
       
         14 LATIN
                 14 Latin
       
          1 LAUGH
                  1 laugh
       ----------------------------------------
    
  4. 練習問題Aで作ったスクリプトに手を加え,コマンドラインからソートの仕方が選べるようにしなさい。


[ ↑ INDEX ] [ ← PREV ] [ NEXT → ]