現在地: ホーム Dive Into Python 3

難易度: ♦♦♢♢♢

内包表記

私たちの想像力は、フィクションにおけるように、存在しないものを想像する時に発揮されるのではありません。
むしろ、現にそこにあるものをよく理解しようとする時にこそ想像力は大きく引き伸ばされるのです。
リチャード・ファインマン

 

飛び込む

あらゆるプログラミング言語が、複雑なものを意図的にシンプルにする、そんな機能をもっているものだ。他の言語から来た人は、それをあっさり見逃してしまいかねない。その言語はそれをシンプルにしていなかったからね(何か他のものをシンプルにするのに忙しかったのだ)。この章では、リスト内包表記・辞書内包表記・集合内包表記について教える。これら3つの関連する概念は、とても強力な1つのテクニックを軸にしているのだが、まあその前に、少し回り道をして、ファイルシステムの操作を支援する2つのモジュールを紹介しよう。

ファイルとディレクトリを扱う

Python 3にはosというモジュールが付属している。osは「オペレーティングシステム」の略だ。osモジュールには、ローカルディレクトリ・ファイル・プロセス・環境変数の情報を取得(ときには操作)するための関数が山ほど入っている。Pythonは、サポートするオペレーティングシステムのどれに対しても、統一されたAPIを提供できるように最善を尽くしているので、プログラムは、どのコンピュータ上でも最小限の環境依存コードだけで実行することができる。

現在の作業ディレクトリ

これからPythonを使っていこうとしているので、Pythonシェルの中で長い時間を過ごすことになるだろう。この本を通して、次のような例を見ることになる:

  1. examplesフォルダにあるモジュールの1つをインポートする
  2. そのモジュールの関数を呼び出す
  3. その結果について説明する

もし、現在の作業ディレクトリ (current working directory) について知らないのなら、ステップ1はおそらくImportErrorを出して失敗するだろう。なぜかって? なぜならPythonは、Import検索パスの中からexampleモジュールを探し出そうとするが、examplesフォルダは検索パスのどこにも無いので見つけ出すことができないからだ。この問題を解決するには、次の2つのうちのどちらか1つを行えばいい:

  1. examplesフォルダをimport検索パスに追加する
  2. 現在の作業ディレクトリを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
  1. osモジュールはPythonに付属しているので、いつでもどこでもこれをインポートできる。
  2. 現在の作業ディレクトリを知るには、os.getcwd()関数を使えばいい。グラフィカルなPythonシェルを使っている場合、現在の作業ディレクトリの初期値は、Pythonシェルの実行ファイルがある場所になっている。Windowsでは、Pythonをインストールした場所に依存するが、デフォルトのディレクトリはc:\Python31だ。コマンドラインからPythonシェルを起動した場合の現在の作業ディレクトリは、python3を起動したときに居たディレクトリになっている。
  3. 現在の作業ディレクトリを変更するには、os.chdir()関数を使えばいい。
  4. 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
  1. os.path.join()関数は、1つ以上のパス名の断片をもとにパス名を組み立てる。この例では、文字列を単純に連結している。
  2. このような少し注意が必要な場合でも、os.path.join()がファイル名の前にスラッシュを追加してくれる。私はこの例をWindows上で作ったので、(フォワード)スラッシュではなくバックスラッシュになっている。LinuxやMac OS Xでこの例を再現すれば、これは(フォワード)スラッシュになるだろう。スラッシュで悩むのはよそう。常にos.path.join()を使って、Pythonに正しい処理をやらせればいいのだ。
  3. os.path.expanduser()は、現在のユーザのホームディレクトリを~(チルダ)で表現しているパスの展開を行う。これは、Linux, Mac OS X, Windowsといった、ユーザにホームディレクトリが与えられるすべてのプラットフォームで動作する。返されたパスの末尾にはスラッシュが付いていないが、os.path.join()関数を使えばこれは問題にならない。
  4. 以上のテクニックを組み合わせれば、ユーザのホームディレクトリにあるファイルやディレクトリのパス名を簡単に組み立てられる。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'
  1. split関数は、フルパス名を分解して、パスとファイル名を含むタプルを返す。
  2. 以前、「多値代入を使えば、関数から複数の戻り値を返すことができる」、と言ったことを覚えているだろうか? os.path.split()関数はまさにその処理を行っている。ここでは、split関数の戻り値を、2つの変数からなるタプルへ代入している。各々の変数は、対応する要素の値を戻り値のタプルから受け取るのだ。
  3. 1つ目の変数dirnameは、os.path.split()関数から返されたタプルの1つ目の要素(ファイルパス)を受けとる。
  4. 2つ目の変数filenameは、os.path.split()関数から返されたタプルの2つ目の要素(ファイル名)を受けとる。
  5. 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']
  1. globモジュールは、ワイルドカードを受け取って、そのワイルドカードにマッチするすべてのファイルとディレクトリのパスを返す。この例では、ワイルドカードはディレクトリパスに "*.xml" を加えたものであり、これはexamplesディレクトリにあるすべての.xmlファイルにマッチする。
  2. 今度は、現在の作業ディレクトリをexamplesに変更した。os.chdir()関数は相対パスを受け取れるのだ。
  3. globパターンの中でワイルドカードを複数回使うこともできる。この例では、現在の作業ディレクトリの中から、.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)
  1. 現在の作業ディレクトリはexamplesフォルダだ。
  2. feed.xmlexamplesの中にあるファイルだ。os.stat()関数を呼び出すと、そのファイルに関する何種類かのメタデータを含んだオブジェクトが返される。
  3. st_mtimeは最終更新日だが、この形式はひどく使いにくい(技術的に言えば、これはエポック時(1970年1月1日として定義されている)からの経過秒数だ)。
  4. timeモジュールはPython標準ライブラリに含まれている。このモジュールには、異なる時間表現に変換するための関数や、時刻の値を文字列にする関数、タイムゾーンをいじる関数などが入っている。
  5. 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'
  1. os.stat()関数は、st_sizeプロパティの中でファイルのサイズも返している。feed.xmlファイルのサイズは3070バイトだ。
  2. st_sizeプロパティをapproximate_size()関数に渡すことができる。

絶対パス名を構築する

前の節では、glob.glob()関数が相対パスを返すことを見た。1つ目の例は'examples\feed.xml'のようなパス名であり、2つ目の例はさらに短い'romantest1.py'のような相対パス名だった。同じ作業ディレクトリにいる限り、このような相対パス名であっても、問題なくファイルを開いたりメタデータを取得したりできるだろう。しかし、絶対パス名(つまり、ルートディレクトリもしくはドライブレターに至るまでの、すべてのディレクトリを含むパス名)を作りたいのであれば、os.path.realpath()関数が必要になる。

>>> 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]
  1. これを理解するには右から左へ見ていけばいい。a_listはマッピングの元になるリストだ。Pythonのインタプリタはa_listの要素を一度に1つずつ取り出していき、その各々の値を一時変数のelemに代入する。次に、Pythonは関数elem * 2を適用し、その結果を戻り値のリストに追加する。
  2. リスト内包表記は新しいリストを作るので、元のリストには影響を与えない。
  3. マッピングする変数にリスト内包表記の結果を代入しても問題は起きない。Pythonは新しいリストをメモリ上に作り、リスト内包表記の処理が完了したときに、その結果を元の変数に代入するからだ。

リスト内包表記の中では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']
  1. これは、現在の作業ディレクトリに含まれるすべての.xmlファイルのリストを返す。
  2. このリスト内包表記は.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']
  1. リストをフィルタリングするために、リスト内包表記の最後に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')]
  1. このリスト内包表記は現在の作業ディレクトリにあるすべての.xmlファイルを探しだし、そのファイルサイズを(os.stat()関数を呼びだして)取得し、ファイルサイズと(os.path.realpath()関数で取得した)絶対パスからなるタプルを返す。
  2. このリスト内包表記は、前の例を足がかりにして、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
  1. これは辞書内包表記ではなくリスト内包表記だ。ファイル名にtestを含むすべての.pyファイルを探しだして、ファイル名と(os.stat()関数で取得した)メタデータを含むタプルを構築する。
  2. 返されたリストの各要素はタプルになっている。
  3. 辞書内包表記だ。この構文はリスト内包表記に似ているが、2つの違いがある。1つ目の違いは、角括弧の代わりに波括弧で包まれていることで、2つ目の違いは、1つの式ではなく、コロンで分けられた2つの式が入っていることだ。コロンの前にあるもの(この例ではf)が辞書のキーであり、コロンの後ろにあるもの(この例ではos.stat(f))が辞書の値である。
  4. 辞書内包表記は辞書を返す。
  5. この辞書のキーは、glob.glob('*test*.py')関数の呼び出しで返されたファイル名だ。
  6. 各キーに関連づけられた値は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'
  1. この辞書内包表記は、現在の作業ディレクトリのすべてのファイルのリストを作り(glob.glob('*'))、各ファイルのメタデータを取得し (os.stat(f))、キーが各ファイルのファイル名で、値が各ファイルのメタデータになっている辞書を構築する。
  2. この辞書内包表記は、先の内包表記をもとにしている。ファイルサイズが6000バイトよりも小さい(if meta.st_size > 6000)ものをフィルタで除外し、そのフィルタリング済みのリストをもとに、ファイル名から拡張子を取り除いたもの(os.path.splitext(f)[0])をキーとして、ファイルのおおよその大きさ(humansizei.approximate_size(meta.st_size))を値とした辞書を構築するのだ。
  3. 前の例で見たように、そのようなファイルは6つあるので、辞書には6つの要素がある。
  4. 各キーの値は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}
  1. 集合内包表記は入力として集合を受け取ることができる。この集合内包表記では、0から9までの数の集合の2乗を求めている。
  2. リスト内包表記や辞書内包表記と同様に、集合内包表記にも、要素をフィルタリングするためのif節を付けることができる。
  3. 集合内包表記の入力が集合である必要はない。どんなシーケンスでも入力として受けとることができる。

もっと知りたい人のために

© 2001– Mark Pilgrim
© Fukada, Fujimoto(日本語版)