Perl, Python, Ruby の比較


1. はじめに

紫藤はスクリプト言語として awk と xyzzy(emacs)-Lisp を使ってきましたが、最近は (といっても全然”最近”ではなく、陸の孤島に住んでいると思われかねませんが。。) Perl, Python, Ruby などの便利なスクリプト言語があるそうなのでちょっとかじってみました。 これらの言語の特徴について初心者なりの感想を述べてみたいと思います。 あくまで、初心者の私見ですので気軽に聞き流してください。

お題は以前紹介した メディアにある画像ファイルをハードディスクに保存するスクリプトです。 実は我が家のパソコンはほとんどアルバムと化しており、メディアからハードディスクに移すスクリプトは FireFoxLinar 以外では私の家族に一番利用されているプログラムです。 画像ファイルをコピーするのにいちいち変なロゴのエディター (xyzzy のこと) が立ち上がるのは鈍くさいという家族の意見を容れ、普通のスクリプト言語で書くことにしました。

2. 画像ファイル保存スクリプトの仕様

仕様は以下の通りです。
  1. メディアのディレクトリを走査して、ディレクトリ名をキーとし、そのディレクトリにある画像ファイルの リストを値とするハッシュ表を作る。
  2. ハードディスクは月ごとのディレクトリに分かれていて、 ある月に作成されたデータはその月のディレクトリに入っている。ディレクトリの名前は 2桁の年、月をハイフンでつなげたものである。(例えば 2005 年 1 月は 05-01 となる) 以降このディレクトリを”今月のディレクトリ”という。
  3. 今月のディレクトリの下に photo[0-9][0-9] というという名のディレクトリを作り、 メディアにある画像ファイルをディレクトリ毎に photo[0-9][0-9] にコピーする。
  4. ハードディスクにコピーした画像ファイルとメディアにあるファイルを比較して、 等しければメディアにあるファイルを消去する。
例えば、
メディアに imag1, imag2 という画像ファイルの入ったフォルダーがあり、 今月のディレクトリに photo01, photo02 というディレクトリがあった場合、 photo03, photo04 というディレクトリを作り、imag1, imag2 にある画像ファイルを それぞれ photo03, photo04 に保存します。

3. Perl, Python, Ruby で書いてみて

2. で述べた仕様に沿って Perl, Python, Ruby で書いてみました。 一夜漬けで書いたので改善の余地はあるとは思いますが、 Perl, Python, Ruby の3つの技量が等しい(残念ながら全部に初心者、本当は 全部に達人が書いたほうが参考になる)人間が 書いたコードを比較するのは悪くないアイデアだと思います。

3.1. まず、Perl で

Perl で書くと次のようになります。
01:     #! perl
02:     
03:     use strict;
04:     use File::Copy;
05:     use File::Compare;
06:     use File::Find;
07:     use Cwd;
08:     
09:     ## global parameters
10:     my $DOC_DIR = 'D:/doc';
11:     my $MEDIA ='G:/';
12:     my $PHOTO_VIEWER = 'D:/WBIN/linar160/linar.exe';
13:     
14:     # getting the string of "year(NN)-month(NN)" 
15:     sub get_year_month{
16:     my ($m, $y) = (localtime)[4,5];
17:     sprintf ("%02d-%02d", $y-100, $m + 1);
18:     }
19:     
20:     # getting the starting number of photoNN, the directory in which photos should be saved
21:     # This function should be called when the program is in the month directory.
22:     sub get_first_photo_dir_number{
23:       my  @pdirs = (glob "photo[0-9][0-9]");
24:       @pdirs ? 1+ substr($pdirs[-1], -2) : 1;
25:     }
26:     
27:     #move into the directory "$DOC_DIR/y-m"
28:     sub move_into_dir_of_month{
29:       my $dir_of_month =  &get_year_month;
30:       unless ($DOC_DIR eq cwd){
31:         chdir $DOC_DIR or die "Cannot move to $DOC_DIR: $!";
32:       }
33:       unless (-d $dir_of_month){
34:         mkdir $dir_of_month or die "cannot create $dir_of_month: $!";
35:       }
36:       chdir $dir_of_month or die "Cannot move to $dir_of_month: $!";
37:       "$DOC_DIR/$dir_of_month" ;
38:     }
39:     
40:     #archive photos in the media into the HD
41:     # This function should be called when the program is in the month directory.
42:     sub archive_photos{
43:       my $photo_dir_number = shift;
44:       my %dhash;
45:       find({
46:         wanted      => sub{push @{$dhash{$File::Find::dir}}, $_ if -f},
47:       }, $MEDIA);
48:       for my $dir_from (sort keys %dhash){
49:         my $n = @{$dhash{$dir_from}};
50:         my $i = 0;
51:         my $dir_to = sprintf("photo%02d", $photo_dir_number++);
52:         mkdir $dir_to or die "cannot create $dir_to: $!";
53:         print "\n$dir_from ==> $dir_to\n";
54:         for my $fname (@{$dhash{$dir_from}}){
55:           my $copy_from = "$dir_from/$fname";
56:           my $copy_to   = "$dir_to/$fname";
57:           copy($copy_from, $copy_to) or die "cannot make a copy for $copy_from: $!";
58:           if(0 == compare($copy_from, $copy_to)){
59:             unlink $copy_from;
60:             print ++$i, "/$n\r";
61:           }else{
62:             die "an error occurs during coping $copy_from";
63:           }
64:         }
65:       }
66:       %dhash;
67:     }
68:     
69:     #main
70:     my $dir_of_month = &move_into_dir_of_month;
71:     my $first_photo_dir_number =  &get_first_photo_dir_number;
72:     if(&archive_photos($first_photo_dir_number)){
73:       exec (sprintf "%s %s/photo%02d", $PHOTO_VIEWER, $dir_of_month, $first_photo_dir_number);
74:     }else{
75:       print "No photos in the media!\nGive Return:";
76:       < STDIN>
77:     }
大体 80 行くらいのコードになります。 リストは Perl のハッシュ表の値にはなれないのでリストを値としたければ、 リストのリファレンスを使う必要があります。 印象としては、awk と sed を混ぜたものをその適応範囲外まで拡張しすぎたという感じです。 崩壊寸前のダムみたいな感じで、いまいち。 ただし、ライブラリは優秀で、実行速度は速いです。

3.2. 次に Python

Python で書くと次のようになります。
01:     #! usr/bin/env python
02:     
03:     import glob, string, os, os.path, shutil, filecmp, re, sys
04:     from datetime import date
05:     
06:     #global parameters
07:     HD    = 'D:/doc/'
08:     MEDIA = 'G:/'
09:     PHOTO_VIEWER = 'D:/WBIN/linar160/linar.exe'
10:     PREGEXP = re.compile(".(gif|bmp|jpe?g|tiff?)$", re.I)
11:     PHASH = {}
12:     
13:     def sum(ls):
14:         total = 0
15:         for x in ls:
16:             total += x
17:         return total
18:     
19:     def nPdir(dir):
20:         lst = [ x for x in glob.glob(dir + "photo[0-9][0-9]") if os.path.isdir(x)]
21:         return lst and int(lst[-1][-2:]) or 0
22:     
23:     def Search_Media(dir):
24:         os.chdir(dir)
25:         items = os.listdir(dir)
26:         ls = [x for x in items if PREGEXP.search(x)]
27:         if ls:
28:             PHASH[dir] = ls
29:         for d in [dir + x + '/' for x in items if os.path.isdir(x)]:
30:             Search_Media(d)
31:     
32:     def Move_Photos():
33:         md = HD + date.today().strftime("%y-%m/")
34:         np = nPdir(md)
35:         pt = sum([len(val) for val in PHASH.itervalues()])
36:         sf = 0
37:         i0 = True
38:     
39:         if not pt:
40:             print "No photos in the media: give return"
41:             sys.stdin.readline()
42:             sys.exit()
43:             
44:         os.chdir(md)
45:     
46:         for d, fs in PHASH.iteritems():
47:             np += 1
48:             pd = md + "photo%02d" % np
49:             if i0:
50:                 pd0 = pd
51:                 i0 = False
52:             os.mkdir(pd)
53:             for f in fs:
54:                 f1 = d + f
55:                 f2 = pd + '/' + f
56:                 shutil.copyfile(f1, f2)
57:                 if filecmp.cmp(f1, f2):
58:                     os.remove(f1)
59:                 else:
60:                     print "copy failed: %s => %s\n" % (f1, f2)
61:                 sf += 1
62:                 print "%d/%d\r" % (sf, pt),
63:         return [' ', pd0]
64:     
65:     if __name__=='__main__':
66:         Search_Media(MEDIA)
67:         os.execv(PHOTO_VIEWER, Move_Photos())
大体 70 行になります。長さは大体 Perl で書いたものと同じです。 (このコードは一夜漬けで書いたのでかなりいけてないです。改良版は
ここにあります。) ソースの見栄えはとても良く、 インデントでブロックを表現するというアイデアは成功していると思います。 煉瓦のような雰囲気できっちりとしています。コーディングの自由度が少ないので、 (1年前の自分も含む)誰が書いても同じようなコードになり、読み取るのは容易です。 そのため、コメントの量も少なくて済み、変数名をコメント代わりに使う必要もありません。 変数はデフォルトで局所変数となるので、 Perl のように my で宣言する必要はありません。 また、リストの内包表現は Lisp の mapcar と remove-if-not が同時に出来るので便利です。 ライブラリは優秀で、実行速度は Perl より速い気がします。

スクリプト言語としての欠点を挙げると、

  1. 関数とメソッドが入り混じっているのですっきりしない。
  2. 多くのモジュールを import しなければならない。(70 行で 9 個もある)
があります。

それから、Python には 対話モードがあり、個々の関数をテストできます。(注1) この機能は大きめのプログラムを書くときに便利です。 main に相当する部分を、 if __name__=='__main__': のブロックに入れることによって、 Python によって直接読み込まれたとき以外は動作しないようにすることが出来ます。(注2)
個々の関数のテストは次のようにします。

  1. プロンプトから python とだけ打ち込んで対話モードに入ります。
  2. import hoge としてスクリプトを読み込ませます。 (例えば hoge.py を読み込む場合)
  3. あとは hoge.foo([1,2,3]) などとして、個々の関数をテストします。(hoge.py に foo という関数が 定義されている場合)

3.3. 最後に Ruby

それでは、 Ruby ではどうなるでしょうか?
01:     #! ruby
02:     
03:     require "FileUtils"
04:     
05:     #global parameters
06:     Doc_dir = "D:/doc/"
07:     Media = "G:/"
08:     Viewer = "D:/WBIN/linar160/linar.exe"
09:     PEXP =  /\.(jpe?g|JPE?G|bmp|BMP|tiff?|TIFF?)$/
10:     $photo_hash = Hash.new
11:     
12:     def photo_dir_max(dir)
13:         Dir.chdir(dir)
14:         d = Dir.glob("photo[0-9][0-9]").select{|f| File.directory?(f)}.last
15:         d ? d.slice(5..6).to_i : 0 
16:     end
17:     
18:     def search_media(dir)
19:         Dir.chdir(dir)
20:         files = Dir.glob("*.*").select{|f| f =~ PEXP}
21:         $photo_hash[dir] = files unless files == []
22:         Dir.glob("*").select{|f| File.directory?(f)}.each{|d| search_media(dir + d + "/")}
23:     end
24:     
25:     def move_photos ()
26:         mon_dir = Doc_dir + Time.new.strftime("%y-%m/")
27:         p_d_num = photo_dir_max(mon_dir)
28:         i = count = 0; p_dir0 = ''
29:         n_p_files = lambda{|h| n=0; h.each_key{|k| n += h[k].size}; n}.call($photo_hash)
30:         if n_p_files == 0
31:         then
32:             puts "NO photo files in the media."
33:             STDIN.readline
34:             exit(0)
35:         end
36:         $photo_hash.each_key{|d| 
37:             p_d_num += 1
38:             p_dir = mon_dir + sprintf("photo%02d/", p_d_num)
39:             if i == 0 then p_dir0 = p_dir; i += 1 end
40:             FileUtils.mkdir(p_dir)
41:             $photo_hash[d].each{|f|
42:                 f1, f2 = d + f, p_dir + f
43:                 FileUtils.cp(f1, f2)
44:                 FileUtils.cmp(f1, f2) ?  File.delete(f1) : printf("Copy failed: %s => %s\n", f1, f2)
45:                 count += 1
46:                 printf("%d/%d\r", count, n_p_files) 
47:             }
48:         }
49:         p_dir0
50:     end
51:     
52:     search_media(Media)
53:     exec(Viewer, move_photos())
長さは大体 50 行となり、3つの中で一番短くなります。 ソースの見栄えも悪くなく、データがピリオドの前から、後ろに 流れていくような感じです。ちょうど、Lisp コードが括弧の中から外にデータが流れるように 見えるのと同じ雰囲気です。 また、全てがメソッドなのですっきりしています。
問題は、遅いことです。 このプログラムではメディアからの読み出しが律速のはずなのに、ほかの2つに比べて随分遅いです。 これは、perl や python がバイトコンパイルしてから実行するのに対し、ruby が純粋なインタープリタで、 プログラムを1つ1つ解釈しながら実行することによると思います。 (これについてはお前の書き方が悪いとの指摘を受けました。 詳しくは
ここを見てください。 また、改良版はここにあります。 しかし、紫藤のプログラムのいけてない点は律速ではなく、改良版も perl や python に比べて遅いです。) 言語そのものは悪くないのに残念です。 遅いとストレスが溜まるので今回は見送り。

4. というわけで Python

Perl, Python, Ruby の比較をまとめるとつぎの表のようになります。 あくまで、初心者の私見です。
項目 Perl Python Ruby
書きやすさ
読みやすさ
ライブラリー
実行速度
ドキュメント
ユーザー数
コードの書きやすさは3つともそれほど違わないように思えます。ただ、読みやすさはダントツで Python が 優れています。Ruby はまずまずで、Perl は書き手の技量による部分が大きいのですが、 一般的には可読性の低いコードになりがちです。

現在の人気を無視して、言語そのもののよしあしを考えると、 Python と Ruby はほぼ互角でしょう。しかし、今のところ Python の方が、 実行速度が速く、ライブラリが豊富なので、とりあえず Python を使うことにします。 また、Python には対話モードがあるのも Lisp に慣れた人間にとってはありがたいです。 Ruby は今後の健闘に期待します。Ruby を使うと他の2つよりコードが2割以上短くなるので 素質は十分だと思います。 (上の3つの例は、コードの長さの目安を得るために通常の書き方より少しつめて書きました。)

5. おわりに

以上 Perl, Python, Ruby の比較をつれづれと書きましたがご参考になりましたでしょうか? 結論は、
  1. Python, Ruby は Perl に比べて明らかによい。(後発なので当然か)
  2. Python と Ruby はほぼ互角。
最後に主な Web docments を挙げておきます。

6. 追記(もう少しまじめな比較)

上の文章は小さなスクリプトを書いてみてその出来具合を比較しただけなので、 ここではもう少しまじめな比較をしたいと思います。現在は Python を使っているので Python よりの論評になっています。

Perl や Ruby は純粋なスクリプト言語で、(もちろんそれなりに大きなプログラムも書けるようにはなっているものの) プログラムを短くすることに主眼が置かれています。 一方、Python はスクリプト言語としてもつかえる大規模プログラム作成言語で、デバックの容易さに主眼が 置かれています。プログラミング言語は適応範囲が広いほど学ぶ価値があるので、 その意味で Python を学ぶことは Perl や Ruby を学ぶより有用だと思います。 (このことから google が Perl や Ruby でなく、 Python を使っている 理由が分かるような気がします。)

ここにある内容は一部Python 早めぐりと重複しています。 Python のポリシーについては A morality tale of Perl versus Python和訳) やThe Zen of Python和訳)を見てください。

6.1. 大規模プログラムの作成

Python は Perl や Ruby に比べて大規模プログラムが容易に作れるという利点があります。 また、小さなスクリプトを大きなプログラムに育てることがやりやすい ということも Python の特徴です。 Python で大規模プログラムが作りやすいのは次の理由に拠ります。
  1. ソースファイルごとに名前空間が割り当てられる。
  2. 対話モードで関数を一つずつをテストすることが出来る。
  3. if __name__=='__main__' 以下にファイルごとのテストコードを書くことが出来る。
Perl や Ruby では名前空間を分けるための宣言をわざわざ書かなければいけません。 Python では1ファイル1名前空間と割り切り、 ファイル名を名前空間名とすることによって名前の衝突を上手に回避しています。 また、 Perl でも Ruby でもはじめからモジュール専用のコードを書かなければなりませんが、 Python でははじめの小さなスクリプトに __all__ などの 約束事を書き足せばモジュールとして機能します。また、そのスクリプト単体でもつかうことが 出来ます。例えば wxPython と Tkinter で Eight Queens を作る を見てください。

6.2. ファイルの読み込み

Perl では基本的に while(<>) を使って一行ずつファイルを読み込みますが、 Python や Ruby では read() を使ってファイルを一気に読み込むことも出来ます。 (注3) これは、Python や Ruby が登場した 1990 年代はメモリーが安くなったために贅沢な使い方が出来るようになったためだと 思われます。

6.3. リストの操作

Lisp にある mapcar, remove-if-not などのリストを操作してリストを返す関数は、 Perl, Python, Ruby では以下のようなっています。 このなかで Ruby が一番 Lisp に近い仕様になっています。Python は Lisp とは一線を画しているようです。

例)要素が非負の実数の場合、その平方根を返す。 [-3,-2,-1,0,1,2,3] ⇒ [0.0, 1.0, 1.4142135623731, 1.73205080756888]

01:     # Perl 5
02:     my @ls0=(-3,-2,-1,0,1,2,3);
03:     my @ls1=();
04:     for  (@ls0){
05:       push @ls1, sqrt($_) if $_ >= 0;
06:     }
07:     print "$_\n"  for (@ls1);
08:     
09:     # Ruby
10:     p [-3,-2,-1,0,1,2,3].select{|x| x>=0}.map{|x| Math.sqrt(x)}
11:     
12:     # Python
13:     import math
14:     print [math.sqrt(x) for x in [-3,-2,-1,0,1,2,3] if x>=0]

6.4. 引数の渡し方

6.5. 関数の生成

関数の生成は以下のようにします。Perl や Ruby では関数を返す関数が書けますが、 Python では普通はそういうことをしません(
)。その代わり、関数のクラスを定義します。 Python で書くと Perl や Ruby より長くなりますが、これはやり方は1つだけ のポリシーに従ったものです。また、より複雑な関数を生成する場合はむしろこの記法のほうがすっきりするでしょう。

例)累積機の生成。 (数nを取り、「数iを取ってnをiだけ増加させ、その増加した値を返す関数」を返すような関数)

01:     # Perl 5
02:     sub foo {
03:       my ($n) = @_;
04:       sub {$n += shift}
05:     }
06:     
07:     #  Python
08:     class foo:
09:         def __init__(self, n):
10:             self.n = n
11:         def __call__(self, i):
12:             self.n += i
13:             return self.n
14:                 
15:     # Ruby
16:     def foo (n)
17:         lambda {|i| n += i }
18:         end
使用例

01:     >>> a=foo(10)
02:     >>> a(3)
03:     13
04:     >>> a(5)
05:     18

6.6. 高階関数

6.7. その他

注1:
perl では Term::Readline::Gnu にある perlsh を使えば Python と同じことができるそうです。 ruby では irb で同じことができます。

注2:
perl や ruby でもできるそうですが、この機能は、モジュールと通常のスクリプトの書式が同じであるという python の特徴があって初めて威力を発揮すると思います。 perl や ruby でこの機能を有効に使った例をご存知の方はお教えください。

注3:
perl でファイルを一気に読み込めないといっているわけではありません。 perl で一気読みするには次のようにするそうです。

my $data = do{ local $/; <STDIN>};

注4:
こんな感じになります。
[greeting.rb]

#! /usr/bin/env ruby

def goodmorning(name)
    puts "good morning " + name + "."
end

def goodafternoon(name)
    puts "good afternoon " + name + "."
end

def greeting(fn, name)
    fn.call(name)
end

greeting(method(:goodmorning), "Lucy")
greeting(method(:goodafternoon), "Marcia")
$ ruby greeting.rb
good morning Lucy.
good afternoon Marcia.