HOME Python 書き込む

Perl, Python, Ruby の比較


1. はじめに

この文書は旧版 を少し手直ししたものです。元の文書に載せてあるスクリプトはいまいちなので書き換えました。 それに伴い、本文のほうも替わっています。 また、実行環境の OS が Win32 から Linux に替わりました。 (今は家族も Linux を使っています。)

紫藤は長年スクリプト言語として awk と Lisp を使ってきました。 Perl というものがあり、一時はブームになっていたのも知っていたのですが、 どうもなじめなかったので、ほとんど使いませんでした。 最近、Python や Ruby といった新世代のスクリプト言語が広く使われるようになったので、 それらを試してみることにしました。

現在は Python を使っているので、Python よりの比較になっていることを念頭において読んでください。 また、この文書に批判的な意見もあるので、 そちらも参考にしてください。

お題は以前紹介した メディアにある画像ファイルをハードディスクに保存するスクリプトです。 実は我が家のパソコンはほとんどアルバムと化しており、メディアからハードディスクに移すスクリプトは FireFox 以外では私の家族に一番利用されているプログラムです。

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

仕様は以下の通りです。
  1. 今月のディレクトリの下に写真用ディレクトリを作り、そこにメディアから写真を移動する。
  2. 今月のディレクトリの名前は年の下2桁と月をハイフンでつなげたものである。(例: 2007 年 6 月 → 07-06)
  3. 今月のディレクトリの下に複数の写真用ディレクトリがあり、それらのの名前は photoNN (N=0-9) である。
    通し番号 NN は 01 からはじまり、1 づつ増加する。
    新しく作る写真用ディレクトリの通し番号 NN は、現在ある写真用ディレクトリの通し番号の最大値に 1 を加えたもの からはじめる。
以下のように実装します。
  1. メディアのディレクトリを走査して、ディレクトリ名をキーとし、そのディレクトリにある画像ファイルの リストを値とするハッシュ表を作る。
  2. 今月のディレクトリがなければ作る。
  3. 今月のディレクトをを調べ、写真用ディレクトリの通し番号を何番から始めればいいか決める。
  4. メディアにある写真をディレクトリごとに HDD にコピーする。
  5. HDD にコピーした画像ファイルと、メディアにあるもともとの画像ファイルを比較して、 等しければメディアのファイルを削除する。
例えば、
メディアに imag1, imag2 という画像ファイルの入ったフォルダーがあり、 今月のディレクトリに photo01, photo02 というディレクトリがあった場合、 photo03, photo04 というディレクトリを作り、imag1, imag2 にある画像ファイルを それぞれ photo03, photo04 に保存します。 (imag1 と imag2 の順序 (どちらが旧いかなど) は気にしません。)

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

2. で述べた仕様に沿って Perl, Python, Ruby で書いてみました。 現在、Python を使っているので、Python のやつが他のよりうまく書けていると思います。 初心者が一夜漬けの学習で書くとどうなるかは旧文書を見てください。

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 を混ぜたものをその適応範囲外まで拡張しすぎたという感じです。 崩壊寸前のダムみたいな感じで、いまいち。 ただし、ライブラリは優秀で、実行速度は速いです。

今は python や ruby があるので、いまさら新たに perl を学ぶ必要は無いと思います。

3.2. 次に Python

Python で書くと次のようになります。旧版のはいけてないので書き直しました。
001:   #! /usr/bin/env python
002:   
003:   r"""
004:   script to achive photos in the removal media into HD
005:   by T.Shido
006:   June 26, 2007
007:   """
008:   
009:   import os, os.path, filecmp, shutil, re, sys, operator
010:   from datetime import date
011:   
012:   # global parameters
013:   HD    = '/home/pub/photos/'
014:   MEDIA = '/media/usbdisk/'
015:   
016:   REG_FILE = re.compile(r"\.(gif|bmp|jpe?g|tiff?|wav|mov)$", re.I)
017:   REG_DIR = re.compile(r'^photo[0-9][0-9]$')
018:   
019:   def n_photo_dir(d):
020:       """return the max NN of photoNN in directory of the month"""
021:       
022:       return \
023:         reduce(max, \
024:                [ int(x[-2:]) for x in os.listdir(d) \
025:                  if (os.path.isdir(os.path.join(d,x)) and REG_DIR.match(x))], \
026:                0)
027:   
028:   def search_media(d):
029:       """search Media and returns a hash
030:       whose keys are directory name and the values are lists of photo files."""
031:       def search_sub(d):
032:           os.chdir(d)
033:           ls_d = os.listdir(d)
034:           ls_f = [x for x in ls_d if os.path.isfile(x) and REG_FILE.search(x)]
035:           if ls_f:
036:               h[d] = ls_f
037:           for d1 in [os.path.join(d,x) for x in ls_d if os.path.isdir(x)]:
038:               search_sub(d1)
039:               
040:       h={}
041:       search_sub(d)
042:       return h
043:           
044:   def move_photos(d0, d1):
045:       r"""
046:       Moveing photo files from media into HD,
047:       """
048:       dir_of_month = os.path.join(d1, date.today().strftime("%y-%m"))
049:       h = search_media(d0)
050:       total_files = reduce(operator.__add__, [len(v) for v in h.itervalues()], 0)
051:   
052:       if total_files==0:
053:           print "No photos in the media: give return"
054:           sys.stdin.readline()
055:           sys.exit()
056:               
057:       if not os.path.isdir(dir_of_month):
058:           os.mkdir(dir_of_month)
059:                   
060:       i_dir = n_photo_dir(dir_of_month)
061:       count=0
062:   
063:       for d, ls_files in h.iteritems():
064:           i_dir += 1
065:           d_to = os.path.join(dir_of_month, "photo%02d" % i_dir)
066:           os.mkdir(d_to)
067:           
068:           for f in ls_files:
069:               f_from=os.path.join(d,f)
070:               f_to=os.path.join(d_to, f)
071:               shutil.copyfile(f_from, f_to)
072:               if not filecmp.cmp(f_from, f_to):
073:                   print f_from + " and " + f_to + " are not same!"
074:                   sys.exit()
075:               os.remove(f_from)
076:               count+=1
077:               print "%d/%d\n" % (count, total_files),                
078:   
079:   if __name__=='__main__':
080:       move_photos(MEDIA, HD)
大体 80 行になります。長さは大体 Perl で書いたものと同じです。 ソースの見栄えはとても良く、 インデントでブロックを表現するというアイデアは成功していると思います。 煉瓦のような雰囲気できっちりとしています。コーディングの自由度が少ないので、 (1年前の自分も含む)誰が書いても同じようなコードになり、読み取るのは容易です。 そのため、コメントの量も少なくて済み、変数名をコメント代わりに使う必要もありません。 変数はデフォルトで局所変数となるので、 Perl のように my で宣言する必要はありません。 また、リストの内包表現は Lisp の mapcar と remove-if-not が同時に出来るので便利です。 ライブラリは優秀で、実行速度は Perl より速い気がします。

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

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

それから、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 ではどうなるでしょうか? (これも書き換えました)
001:   #! /usr/bin/env ruby
002:   
003:   require "fileutils"
004:   
005:   # global parameters
006:   HD    = '/home/pub/photos/'
007:   MEDIA = '/media/usbdisk/'
008:   
009:   REG_FILE = Regexp.compile("\\.(gif|bmp|jpe?g|tiff?|wav|mov)$",Regexp::IGNORECASE)
010:   REG_DIR = Regexp.compile('^photo[0-9][0-9]$')
011:   
012:   H=Hash.new    
013:   # HD 側に何個の photoNN フォルダーがあるかを返します。
014:   def n_photo_dir(d)
015:     Dir.entries(d).select{|x| FileTest.directory?(File.join(d,x)) and REG_DIR =~ x
016:      }.map{|x| x[5..6].to_i}.inject(0){|x,y| max(x,y)}
017:   end
018:   
019:   # メディアに保存されている写真ファイルの一覧をフォルダーごとにまとめて返します
020:   # 再帰的にファルダーのツリーを下っていき、写真ファイルの一覧をフォルダー名をキーとしてハッシュ表に登録します
021:   def search_media(d)
022:   
023:     Dir.chdir(d)
024:     ls=Dir.entries(d).reject{|x| x=='.' or x=='..' }
025:   
026:     ls_f=ls.select{|x| FileTest.file?(File.join(d, x)) and REG_FILE =~ x}
027:     
028:     H[d] = ls_f unless ls_f.empty? 
029:     ls.map{|x| File.join(d,x)}.select{|x| FileTest.directory?(x)}.each{|x| search_media(x)}
030:   end
031:   
032:   # 写真ファイルをメディアから HD にコピーします
033:   def move_photos(d0, d1)
034:       dir_of_month=File.join(d1, Time.now.strftime("%y-%m"))
035:       search_media(d0)
036:       total_files = H.values.map{|x| x.size}.inject(0){|result, item| result + item }
037:       
038:       if total_files==0
039:       then
040:           p "no photos, give return"
041:           STDIN.readline
042:           abort
043:       end
044:       
045:       FileTest.directory?(dir_of_month) or Dir.mkdir(dir_of_month)
046:       
047:       i_dir = n_photo_dir(dir_of_month)
048:       count=0
049:       
050:       H.each{|d, ls_files|
051:           i_dir += 1
052:           d_to = File.join(dir_of_month, sprintf("photo%02d", i_dir))
053:           Dir.mkdir(d_to)
054:           
055:           ls_files.each{|f|
056:   
057:               f_from=File.join(d,f)
058:               f_to=File.join(d_to, f)
059:               FileUtils.cp(f_from, f_to)
060:               
061:               if not FileUtils.cmp(f_from, f_to)
062:               then
063:                   printf("Copy failed: %s => %s\n", f_from, f_tp)
064:                   abort
065:               end
066:               File.delete(f_from)
067:               count+=1
068:               printf("%d/%d\n", count, total_files)
069:           }
070:       }
071:   end
072:   
073:   #main
074:   move_photos(MEDIA, HD)
長さは 74 行となり、3つの中で一番短くなります。 ソースの見栄えも悪くなく、データがピリオドの前から、後ろに 流れていくような感じです。ちょうど、Lisp コードが括弧の中から外にデータが流れるように 見えるのと同じ雰囲気です。 また、全てがメソッドなのですっきりしています。

Win32 で試したときはすごく遅かったのですが、Linux 上ではそんなに遅く感じませんでした。 Ruby は Linux と相性がいいのかもしれません。

4. というわけで Python

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

現在の人気を無視して、言語そのもののよしあしを考えると、 Python と Ruby はほぼ互角でしょう。しかし、今のところ Python の方が、 実行速度が速く、ライブラリが豊富なので、とりあえず Python を使うことにします。 また、Python には対話モードがあるのも Lisp に慣れた人間にとってはありがたいです。 Ruby は今後の健闘に期待します。

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. 高階関数

ディレクトリを再帰的にたどって、ファイルなら関数を適用する関数 walk_dir を python と ruby で書くと以下のようになります。

[walk_dir.py]

001:   #!/usr/bin/env python
002:   # coding:shift_jis
003:   
004:   from __future__ import with_statement
005:   import os, os.path, sys
006:   
007:   
008:   def walk_dir(f, d, exp=''):
009:       u'''ディレクトリを再帰的にたどって全てのファイルに関数 f を適用します'''
010:       
011:       for p0 in os.listdir(d):
012:           p1 = os.path.join(d, p0)
013:           if os.path.isfile(p1) and p1.endswith(exp):
014:               f(p1)
015:           elif os.path.isdir(p1):
016:               walk_dir(f, p1, exp)
017:   
018:               
019:   def head(fname):
020:       u'''最初の 5 行を表示する'''
021:       print '====  %s  ====' % (fname,)
022:       with file(fname) as f:
023:           for i, line in enumerate(f):
024:               if i==5: break
025:               print line
026:   
027:   
028:   # スクリプトが直接呼ばれたときに以下のブロックが実行されます。
029:   if __name__=='__main__':
030:       walk_dir(head, os.getcwd(), '.py')

[walk_dir.rb]

001:   #! ruby
002:   
003:   
004:   # walk directories recursively
005:   # and apply proc if the element is a file
006:   def walk_dir(proc, d=nil, exp='')
007:     Dir.foreach(d){|s|
008:       if s != '.' and s != '..' then
009:         p=File.join(d,s)
010:         case File.ftype(p)
011:         when "file"
012:           if(exp.length==0 or File.extname(p) == exp) then
013:             proc.call(p)
014:           end
015:         when "directory"
016:           walk_dir(proc, p, exp)
017:         end
018:       end
019:     }
020:   end
021:   
022:   # show first 5 lines of a file
023:   def head(fname)
024:     print "\n\n====  " + fname + "  ===\n"
025:     i=0
026:     open(fname) do |f|
027:       f.each{|line|
028:         if i==10 then break end
029:         print line
030:         i+=1
031:       }
032:     end
033:   end
034:   
035:   #-----
036:   
037:   walk_dir(lambda{|fname| head(fname)}, Dir.pwd, '.rb')    

6.7. その他

注1:
Python は言語の機能として対話環境をサポートしています。 perl では Term::Readline::Gnu にある perlsh を使えば Python と同じことができるそうです。 ruby では irb で同じことができます。

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

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

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