現在地: ホーム ‣ Dive Into Python 3 ‣
難易度: ♦♦♢♢♢
❝私たちの想像力は、フィクションにおけるように、存在しないものを想像する時に発揮されるのではありません。
むしろ、現にそこにあるものをよく理解しようとする時にこそ想像力は大きく引き伸ばされるのです。❞
— リチャード・ファインマン
あらゆるプログラミング言語が、複雑なものを意図的にシンプルにする、そんな機能をもっているものだ。他の言語から来た人は、それをたやすく見逃してしまう可能性がある。その言語はそれをシンプルにしていなかったからね(何か他のものをシンプルにするのに忙しかったのだ)。この章では、リスト内包表記・辞書内包表記・集合内包表記について教える。これら3つの関連する概念は、とても強力な1つのテクニックを軸にしている。まあその前に、少し回り道をして、ファイルシステムの操作を支援する2つのモジュールを紹介しよう。
⁂
Python 3にはosというモジュールが付属している。osは「オペレーティングシステム」の略だ。osモジュールには、ローカルディレクトリ・ファイル・プロセス・環境変数の情報を取得(ときには操作)するための関数が山ほど入っている。Pythonは、サポートするオペレーティングシステムのどれに対しても、統一されたAPIを提供できるように最善を尽くしているので、プログラムは、どのコンピュータ上でも最小限の環境依存コードだけで実行できる。
これからPythonを使っていこうとしているので、Pythonシェルの中で長い時間を過ごすことになるだろう。この本を通して、次のような例を見ることになる:
examplesフォルダにあるモジュールの1つをインポートする
もし、現在の作業ディレクトリを知らないのなら、ステップ1はおそらくImportErrorを出して失敗することだろう。なぜかって? なぜならPythonは、Import検索パスの中からexampleモジュールを探し出そうとするが、examplesフォルダは検索パスのどこにも無いので見つけ出すことができないからだ。この問題を解決するには、次の2つのうちのどちらか1つを行えばいい:
examplesフォルダをimport検索パスに追加する
examplesフォルダに変更する
「現在の作業ディレクトリ」は、Pythonがメモリの中に常に保持している見えない属性だ。現在の作業ディレクトリは、Pythonシェルにいるときも、コマンドラインからPythonスクリプトを実行しているときも、PythonのCGIスクリプトをWebサーバ上で実行しているときも、常に存在する。
osモジュールには、現在の作業ディレクトリを扱うための2つの関数が入っている。
>>> import os ① >>> print(os.getcwd()) ② C:\Python31 >>> os.chdir('/Users/pilgrim/diveintopython3/examples') ③ >>> print(os.getcwd()) ④ C:\Users\pilgrim\diveintopython3\examples
osモジュールはPythonに付属しているので、いつでもどこでもこれをインポートすることができる。
os.getcwd()関数を使えばいい。グラフィカルなPythonシェルを使っている場合、現在の作業ディレクトリの初期値は、Pythonシェルの実行ファイルがある場所になっている。Windowsでは、Pythonをインストールした場所に依存するが、デフォルトのディレクトリはc:\Python31だ。コマンドラインからPythonシェルを起動した場合の現在の作業ディレクトリは、python3を起動したときに居たディレクトリになっている。
os.chdir()関数を使えばいい。
os.chdir()関数を呼び出す時に、私はWindows環境なのにLinuxスタイルのパス名(普通のスラッシュ、ドライブレター無し)を使用した。これは、Pythonがオペレーティングシステムの差異を吸収しようとした結果の1つだ。
ディレクトリに関する話をしているので、ここでos.pathモジュールにも触れたい。os.pathには、ファイル名とディレクトリ名を操作するための関数が入っている。
>>> import os
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples/', 'humansize.py')) ①
/Users/pilgrim/diveintopython3/examples/humansize.py
>>> print(os.path.join('/Users/pilgrim/diveintopython3/examples', 'humansize.py')) ②
/Users/pilgrim/diveintopython3/examples\humansize.py
>>> print(os.path.expanduser('~')) ③
c:\Users\pilgrim
>>> print(os.path.join(os.path.expanduser('~'), 'diveintopython3', 'examples', 'humansize.py')) ④
c:\Users\pilgrim\diveintopython3\examples\humansize.py
os.path.join()関数は、パス名の断片を一つ以上使ってパス名を組み立てる。この例では、文字列を単純に連結している。
os.path.join()がファイル名の前にスラッシュを追加してくれる。私はWindows上でこの例を作ったので、(フォワード)スラッシュではなくバックスラッシュになっている。LinuxやMac OS Xでこの例を再現すれば、これは(フォワード)スラッシュになるだろう。スラッシュで悩むのはよそう。常にos.path.join()を使って、Pythonに正しい処理をやらせればいいのだ。
os.path.expanduser()は、現在のユーザのホームディレクトリを~(チルダ)を用いて表しているパスを展開する。これはLinux, Mac OS X, Windowsなどの、ユーザがホームディレクトリを持つすべてのプラットフォームで動作する。返されたパスの末尾にはスラッシュが付いていないが、os.path.join()関数はこれを問題なく処理してくれる。
os.path.join()関数は引数をいくつでも受け取ることができる。これを知ったとき私はとても嬉しかった。なぜなら、新しい言語のツールボックスを作るときには決まってaddSlashIfNecessary()などという馬鹿げた小さな関数を書かなければならなかったからだ。Pythonでこんな馬鹿げた関数を書いてはならない。そんな仕事は、賢い人たちが既に片付けてくれている。
os.pathモジュールには、フルパス名・ディレクトリ名・ファイル名を構成要素に分解するための関数も入っている。
>>> pathname = '/Users/pilgrim/diveintopython3/examples/humansize.py' >>> os.path.split(pathname) ① ('/Users/pilgrim/diveintopython3/examples', 'humansize.py') >>> (dirname, filename) = os.path.split(pathname) ② >>> dirname ③ '/Users/pilgrim/diveintopython3/examples' >>> filename ④ 'humansize.py' >>> (shortname, extension) = os.path.splitext(filename) ⑤ >>> shortname 'humansize' >>> extension '.py'
split関数は、フルパス名を分解し、パスとファイル名を含むタプルを返す。
os.path.split()関数はまさにその処理を行っている。ここでは、split関数の戻り値を、2つの変数からなるタプルへ代入している。各々の変数は、対応する要素の値を戻り値のタプルから受け取るのだ。
os.path.split()関数から返されたタプルの1つ目の要素を受けとる。これはファイルのパスだ。
os.path.split()関数から返されたタプルの2つ目の要素を受けとる。これはファイル名だ。
os.pathには、os.path.splitext()という関数も入っている。この関数はファイル名を分解し、ファイル名と拡張子からなるタプルを返す。ここでも各要素を別々の変数に代入するために、同じテクニックを使っている。
globモジュールは、Python標準ライブラリに含まれているツールの1つだ。このモジュールを使えば、ディレクトリの中身をプログラムで簡単に取得できるようになる。このモジュールはワイルドカードの一種を使用するが、これはコマンドラインで作業する人なら既によく知っているものだろう。
>>> os.chdir('/Users/pilgrim/diveintopython3/')
>>> import glob
>>> glob.glob('examples/*.xml') ①
['examples\\feed-broken.xml',
'examples\\feed-ns0.xml',
'examples\\feed.xml']
>>> os.chdir('examples/') ②
>>> glob.glob('*test*.py') ③
['alphameticstest.py',
'pluraltest1.py',
'pluraltest2.py',
'pluraltest3.py',
'pluraltest4.py',
'pluraltest5.py',
'pluraltest6.py',
'romantest1.py',
'romantest10.py',
'romantest2.py',
'romantest3.py',
'romantest4.py',
'romantest5.py',
'romantest6.py',
'romantest7.py',
'romantest8.py',
'romantest9.py']
globモジュールは、ワイルドカードを受け取って、そのワイルドカードにマッチするすべてのファイルとディレクトリのパスを返す。この例では、ワイルドカードはディレクトリパスに "*.xml" を加えたものであり、これはexamplesディレクトリにあるすべての.xmlファイルにマッチする。
examplesに変更した。os.chdir()関数は相対パスを受け取ることができるのだ。
.pyという拡張子で終わり、testという単語をファイル名のどこかに含むファイルを見つけ出す。
すべての現代的なファイルシステムは、ファイルごとにメタデータ — 作成日・最終更新日・ファイルサイズなど — を格納している。Pythonは、これらのメタデータにアクセスするための単一のAPIを用意している。メタデータにアクセスするためにファイルを開く必要はない。必要なのはファイル名だけだ。
>>> import os >>> print(os.getcwd()) ① c:\Users\pilgrim\diveintopython3\examples >>> metadata = os.stat('feed.xml') ② >>> metadata.st_mtime ③ 1247520344.9537716 >>> import time ④ >>> time.localtime(metadata.st_mtime) ⑤ time.struct_time(tm_year=2009, tm_mon=7, tm_mday=13, tm_hour=17, tm_min=25, tm_sec=44, tm_wday=0, tm_yday=194, tm_isdst=1)
examplesフォルダだ。
feed.xmlはexamplesの中にあるファイルだ。os.stat()関数を呼び出すと、そのファイルに関する何種類かのメタデータを含んだオブジェクトが返される。
st_mtimeは最終更新日だが、この形式はひどく使いにくい(専門的に言えば、これはエポック時(1970年1月1日として定義されている)からの経過秒数だ)。
timeモジュールはPython標準ライブラリに含まれる。このモジュールには、異なる時間表現に変換するための関数や、時刻の値を文字列にする関数、タイムゾーンをいじる関数などが入っている。
time.localtime()関数は、(os.stat()関数の戻り値のst_mtimeプロパティから取得した)エポック時からの経過秒数の時刻値を、年・月・日・時・分・秒などから構成されるもっと便利な構造に変換する。このファイルの最終更新時刻は、2009年7月13日 午後5時25分くらいのようだ。
# 前の例から続く >>> metadata.st_size ① 3070 >>> import humansize >>> humansize.approximate_size(metadata.st_size) ② '3.0 KiB'
os.stat()関数は、st_sizeプロパティの中でファイルのサイズも返している。feed.xmlファイルのサイズは3070バイトだ。
st_sizeプロパティをapproximate_size()関数に渡すことができる。
前節では、glob.glob()関数が相対パスを返すことを見た。1つ目の例は'examples\feed.xml'のようなパス名であり、2つ目の例はさらに短い'romantest1.py'のような相対パス名だった。同じ作業ディレクトリにいる限り、このような相対パス名であっても、問題なくファイルを開いたりメタデータを取得したりできるだろう。しかし、絶対パス名(つまり、ルートディレクトリやドライブレターに至るまでの、すべてのディレクトリを含むパス名)を作りたいのであれば、os.path.readpath()関数が必要になる。
>>> import os
>>> print(os.getcwd())
c:\Users\pilgrim\diveintopython3\examples
>>> print(os.path.realpath('feed.xml'))
c:\Users\pilgrim\diveintopython3\examples\feed.xml
⁂
リスト内包表記は、リストの各要素に関数を適用することによってリストを別のリストにマッピングするコンパクトな方法を提供する。
>>> a_list = [1, 9, 8, 4] >>> [elem * 2 for elem in a_list] ① [2, 18, 16, 8] >>> a_list ② [1, 9, 8, 4] >>> a_list = [elem * 2 for elem in a_list] ③ >>> a_list [2, 18, 16, 8]
elem * 2を適用し、その結果を戻り値のリストに追加する。
リスト内包表記の中ではPythonのすべての式が使える。もちろんその式には、ファイルやディレクトリを操作するためのosモジュールの関数も含まれる。
>>> import os, glob
>>> glob.glob('*.xml') ①
['feed-broken.xml', 'feed-ns0.xml', 'feed.xml']
>>> [os.path.realpath(f) for f in glob.glob('*.xml')] ②
['c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml',
'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml',
'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml']
.xmlファイルのリストを返す。
.xmlファイルのリストを受け取り、それをフルパス名のリストに変換する。
リスト内包表記は、要素のフィルタリングを行い、元のリストよりも小さなリストを作り出すこともできる。
>>> import os, glob
>>> [f for f in glob.glob('*.py') if os.stat(f).st_size > 6000] ①
['pluraltest6.py',
'romantest10.py',
'romantest6.py',
'romantest7.py',
'romantest8.py',
'romantest9.py']
if節を付けることができる。ifキーワードの後ろの式はリストの各要素に対して評価される。各要素は、式がTrueと評価された場合に出力に加えられる。このリスト内包表記は、現在の作業ディレクトリにあるすべての.pyファイルのリストを見て、各々のファイルが6000バイトよりも大きいかどうかをif式でテストしている。そのようなファイルは6つあるので、リスト内包表記は6つのファイル名を含むリストを返す。
今までに説明したリスト内包表記は、単純な式だけ — 定数による乗算・1つの関数の呼び出し・元のリストの要素(をフィルタリングしたもの) — を使用していた。しかし、リスト内包表記はどこまでも複雑にすることができる。
>>> import os, glob
>>> [(os.stat(f).st_size, os.path.realpath(f)) for f in glob.glob('*.xml')] ①
[(3074, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-broken.xml'),
(3386, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed-ns0.xml'),
(3070, 'c:\\Users\\pilgrim\\diveintopython3\\examples\\feed.xml')]
>>> import humansize
>>> [(humansize.approximate_size(os.stat(f).st_size), f) for f in glob.glob('*.xml')] ②
[('3.0 KiB', 'feed-broken.xml'),
('3.3 KiB', 'feed-ns0.xml'),
('3.0 KiB', 'feed.xml')]
.xmlファイルを探しだし、そのファイルサイズを(os.stat()関数を呼びだして)取得し、ファイルサイズと(os.path.realpath()関数で取得した)絶対パスからなるタプルを返す。
approximate_size()関数を各々の.xmlファイルのファイルサイズと共に呼び出している。
⁂
辞書内包表記はリスト内包表記に似ているが、これはリストの代わりに辞書を構築する。
>>> import os, glob
>>> metadata = [(f, os.stat(f)) for f in glob.glob('*test*.py')] ①
>>> metadata[0] ②
('alphameticstest.py', nt.stat_result(st_mode=33206, st_ino=0, st_dev=0,
st_nlink=0, st_uid=0, st_gid=0, st_size=2509, st_atime=1247520344,
st_mtime=1247520344, st_ctime=1247520344))
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*test*.py')} ③
>>> type(metadata_dict) ④
<class 'dict'>
>>> list(metadata_dict.keys()) ⑤
['romantest8.py', 'pluraltest1.py', 'pluraltest2.py', 'pluraltest5.py',
'pluraltest6.py', 'romantest7.py', 'romantest10.py', 'romantest4.py',
'romantest9.py', 'pluraltest3.py', 'romantest1.py', 'romantest2.py',
'romantest3.py', 'romantest5.py', 'romantest6.py', 'alphameticstest.py',
'pluraltest4.py']
>>> metadata_dict['alphameticstest.py'].st_size ⑥
2509
testを含むすべての.pyファイルを探しだし、ファイル名と(os.stat()関数で取得した)メタデータを含むタプルを構築する。
f)が辞書のキーであり、コロンの後ろにあるもの(この例ではos.stat(f))が辞書の値だ。
glob.glob('*test*.py')関数の呼び出しで返されたファイル名だ。
os.stat()関数の戻り値だ。だから、この辞書にディレクトリ内にあるファイル名を渡せば、そのファイルのメタデータを取得できることになる。メタデータの1つはファイルサイズを表す st_size だ。alphameticstest.pyというファイルは2509バイトの長さを持っていることがわかる。
リスト内包表記と同様に、辞書内包表記の中にif節を含めることで、各要素ごとに評価される式によって入力シーケンスをフィルタリングすることができる。
>>> import os, glob, humansize
>>> metadata_dict = {f:os.stat(f) for f in glob.glob('*')} ①
>>> humansize_dict = {os.path.splitext(f)[0]:humansize.approximate_size(meta.st_size) \
... for f, meta in metadata_dict.items() if meta.st_size > 6000} ②
>>> list(humansize_dict.keys()) ③
['romantest9', 'romantest8', 'romantest7', 'romantest6', 'romantest10', 'pluraltest6']
>>> humansize_dict['romantest9'] ④
'6.5 KiB'
glob.glob('*'))、各ファイルのメタデータを取得し (os.stat(f))、キーが各ファイルのファイル名で、値が各ファイルのメタデータになっている辞書を構築する。
6000バイトよりも小さい(if meta.st_size > 6000)ものをフィルタで除外し、そのフィルタリング済みのリストをもとに、ファイル名から拡張子を取り除いたもの(os.path.splitext(f)[0])をキーとして、ファイルのおおよその大きさ(humansizei.approximate_size(meta.st_size))を値とした辞書を構築するのだ。
approximate_size()関数の戻り値だ。
いつか役立つかもしれない、辞書内包表記を使った小技をお見せしよう。キーと値を交換するのだ。
>>> a_dict = {'a': 1, 'b': 2, 'c': 3}
>>> {value:key for key, value in a_dict.items()}
{1: 'a', 2: 'b', 3: 'c'}
もちろんこれは、辞書の値が文字列やタプルのようにイミュータブルであるときにだけ動作する。例えば、リストを含んだ辞書でこれを行おうとすると、見事に失敗する。
>>> a_dict = {'a': [1, 2, 3], 'b': 4, 'c': 5}
>>> {value:key for key, value in a_dict.items()}
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <dictcomp>
TypeError: unhashable type: 'list'
⁂
仲間はずれにされないように、集合も独自の内包表記を持っている。これは辞書内包表記にとてもよく似ていて、唯一の違いは、キーと値のペアの代わりに値だけを持つことだ。
>>> a_set = set(range(10))
>>> a_set
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> {x ** 2 for x in a_set} ①
{0, 1, 4, 81, 64, 9, 16, 49, 25, 36}
>>> {x for x in a_set if x % 2 == 0} ②
{0, 8, 2, 4, 6}
>>> {2**x for x in range(10)} ③
{32, 1, 2, 4, 8, 64, 128, 256, 16, 512}
0から9までの数の集合の2乗を求めている。
if節を持つことができる。
⁂
osモジュール
os — Portable access to operating system specific features
os.pathモジュール
os.path — Platform-independent manipulation of file names
globモジュール
glob — Filename pattern matching
timeモジュール
time — Functions for manipulating clock time
© 2001–11 Mark Pilgrim
© 2011- diveintopython3-ja(日本語版)