Monday, February 16, 2009

python perl and ruby

http://www.shido.info/py/python1.html

HOME Python 書き込む

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

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

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

お題は以前紹介した メディアにある画像ファイルをハードディスクに保存するスクリプトです。 実は我が家のパソコンはほとんどアルバムと化しており、メディアからハードディスクに移すスクリプトは FireFox 以外では私の家族に一番利用されているプログラムです。
2. 画像ファイル保存スクリプトの仕様
仕様は以下の通りです。
今月のディレクトリの下に写真用ディレクトリを作り、そこにメディアから写真を移動する。
今月のディレクトリの名前は年の下2桁と月をハイフンでつなげたものである。(例: 2007 年 6 月 → 07-06)
今月のディレクトリの下に複数の写真用ディレクトリがあり、それらのの名前は photoNN (N=0-9) である。
通し番号 NN は 01 からはじまり、1 づつ増加する。
新しく作る写真用ディレクトリの通し番号 NN は、現在ある写真用ディレクトリの通し番号の最大値に 1 を加えたもの からはじめる。
以下のように実装します。
メディアのディレクトリを走査して、ディレクトリ名をキーとし、そのディレクトリにある画像ファイルの リストを値とするハッシュ表を作る。
今月のディレクトリがなければ作る。
今月のディレクトをを調べ、写真用ディレクトリの通し番号を何番から始めればいいか決める。
メディアにある写真をディレクトリごとに HDD にコピーする。
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 より速い気がします。

スクリプト言語としての欠点を挙げると、
関数とメソッドが入り混じっているのですっきりしない。
多くのモジュールを import しなければならない。(80 行で 8 個もある)
があります。

それから、Python には 対話モードがあり、個々の関数をテストできます。(注1) この機能は大きめのプログラムを書くときに便利です。 main に相当する部分を、 if __name__=='__main__': のブロックに入れることによって、 Python によって直接読み込まれたとき以外は動作しないようにすることが出来ます。(注2)
個々の関数のテストは次のようにします。
プロンプトから python とだけ打ち込んで対話モードに入ります。
import hoge としてスクリプトを読み込ませます。 (例えば hoge.py を読み込む場合)
あとは 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 の比較をつれづれと書きましたがご参考になりましたでしょうか? 結論は、
Python, Ruby は Perl に比べて明らかによい。(後発なので当然か)
Python と Ruby はほぼ互角。
最後に主な Web docments を挙げておきます。
Perl
perl.org
cpan.jp
Python
python.org
日本Pythonユーザ会
Ruby
オブジェクト指向スクリプト言語 Ruby
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 で大規模プログラムが作りやすいのは次の理由に拠ります。
ソースファイルごとに名前空間が割り当てられる。
対話モードで関数を一つずつをテストすることが出来る。
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 では以下のようなっています。
Perl は map や grep 等の関数があるが、 foreach ブロックを使うのが一般的
Python にも map や filter 等の関数があるが、内包表現を使うのがよい。
Ruby は map, select, grep などのブロック付きメソッドを使ってあらわす。
このなかで 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. 引数の渡し方
Perl: 引数をフラットなリストに変換して値渡し。参照渡しをするにはプロトタイプを用いる。少し複雑。
Python: 参照渡し。ただし、変更不能なオブジェクトは実質的に値渡し。 つまり、配列は、呼び出した関数内で変更を加えるともともとの配列も変更されてしまうが、数、文字列、タプルなどは変更されない。 レストパラメータ、オプショナルパラメータ、キーワードパラメータをサポート。 詳しくは Python チュートリアル 4.6 関数を定義する や Python 早めぐり 5. 関数定義 を見てください。
Ruby: Python と同じ。
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. 高階関数
Perl: 関数のリファレンスを取ることにより関数を引数として渡せる。 (Perl コードの 46 行目)
Python: 関数名が関数へのアドレスをあらわす。
例えば、baz という関数を定義したとき、 baz(....) はその関数の呼び出し、 baz は関数のアドレスになる。関数のアドレスを引数として渡したり、 for ループの中で 用いることが可能。(Eight Queen 本体 93 行目を参照)
Ruby: 関数名を直接引数に与えると、引数が省略されたものとして解釈されるので、lambda でくるむ必要があります。 python の場合より少しタイプ量が増えます。ruby は ブロックつきメソッドを多用するので、 高階関数をあらわに使う機会は python ほど多くないと思われます。
ディレクトリを再帰的にたどって、ファイルなら関数を適用する関数 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. その他
Python と Ruby はイテレーターを使って遅延評価が出来ます。
Python の オンラインドキュメント は大変良い。Ruby のはイマイチ。
Python と Ruby は実はほとんど同じで、むきになるほどの差はないと思います。 (ただ、Python は Haskell などの最近の関数型言語の影響が強く見られ、 一方、Ruby は Smalltalk や Eiffel などのオブジェクト指向言語と Lisp の影響が見られます。)
Python はオブジェクト指向を強要しないので Ruby より初学者には学びやすいかもしれません。
注1:
Python は言語の機能として対話環境をサポートしています。 perl では Term::Readline::Gnu にある perlsh を使えば Python と同じことができるそうです。 ruby では irb で同じことができます。

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

注3:
perl でファイルを一気に読み込めないといっているわけではありません。 perl で一気読みするには次のようにするそうです。
my $data = do{ local $/; };

HOME Python 書き込む

No comments: