このブログを検索

第3章 内包表記

3.1. 没頭しよう

プログラミング言語には共通した特徴があります。それは、複雑なものを意識してシンプルにするということです。あなたが他のプログラミング言語から移ってきたのであれば、やすやすと見逃してしまうかもしれません。なぜなら、前の言語はそこまでシンプルではなかったでしょうから(他のことをシンプルにするのが忙しかったに違いありません)。この章では、リスト、辞書、セットの内包表記について説明します。この3つの考え方はよく似ていますが、ある強力な手法を軸にしています。先に少し寄り道して、2つのモジュールを使ってローカルファイルシステムを操作してみましょう。

3.2. ファイルやディレクトリを扱う

Python3には、os(operating system)というモジュールがあります。osモジュールには多くの関数が含まれていて、ローカルディレクトリ、ファイル、プロセス、環境変数の情報を取得したり、操作することができます。Pythonでは、サポートしている全OSに対して、できる限り統一したAPIを提供しています。そのため、作成したプログラムを走らせるときには、どのコンピュータ上でもプラットフォーム固有のコードは最小限となっています。

3.2.1. カレントワーキングディレクトリ

Pythonを始めると、シェルで多くの時間を使うことになるはずです。この本では、このような例がよく出てきます。
  1. examplesフォルダにあるモジュールのひとつをインポートする
  2. モジュールの中の関数を呼び出す
  3. 結果を説明する
カレントワーキングディレクトリについて知らなければ、おそらくステップ1はImportErrorが出て失敗します。なぜでしょうか?Pythonはimportサーチパスからexampleモジュールを探しますが、examplesはサーチパスの中のディレクトリにはないので、見つけられません。この2つのどちらかを実行すれば、ここを通過できます。
  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

①osモジュールはPythonに組み込まれているので、いつでも、どこででもインポートすることができます。

②os.getcwd()関数を使って、カレントワーキングディレクトリを取得しています。グラフィカルPythonシェルを使っているのであれば、Pythonシェルのexeファイルがある場所がカレントワーキングディレクトリになります。Windowsの場合は、インストールした場所に依存します。デフォルトではc:\Python31 です。もしPythonシェルをコマンドラインから実行するのであれば、カレントワーキングディレクトリは最後にPython3を使用したディレクトリです。

③os.chdir()関数を使うと、カレントワーキングディレクトリを変更できます。

④os.chdir()関数を呼び出したとき、Windows上でも、Linux風のパス名を使用しました(フォワードスラッシュ、ドライブ名なし)。これはPythonがOSをの違いを乗り越えようとしている例の1つです。

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

ディレクトリについて話していますので、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()関数は1つ以上のパス名をつなげて新しくパス名を作ります。この場合は、単純に文字列をつなげています。

②少しだけ①と異なっています。os.path.join()関数はパス名を結合する前にスラッシュ(/)を追加してくれます。今はWindowsでの例なので、フォワードスラッシュではなく、バックスラッシュです。スラッシュのことで頭がいっぱいになっていてはいけません。os.path.join()を使って、Pythonに正確にやってもらいましょう。

③os.path expanduser()は"~"をカレントユーザのホームディレクトリとして、パス名を拡張します。どのプラットフォームであっても、ユーザがホームディレクトリを持っているならこのコードは有効です。返ってくるパスは末尾のトレイリングスラッシュがありませんが、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つの変数として代入しています。各変数は、返ってきたタプルの値をそれぞれ受け取るのです。

③第1の変数dirnameは、os.path.split(pathname)から返ってきたタプルの1つ目の値を受け取っています。

④第2の変数filenameは、タプルの2つ目の値を受け取っています。

⑤os.pathにはos.path.splitext()という関数があり、ファイル名を分割してファイル名と拡張子をタプルで返します。ここでも複数の変数に代入する方法を使っています。

3.2.3. ディレクトリをリスト化する

globモジュールはPython標準ライブラリにあるツールです。プログラムでディレクトリの中身を簡単に知ることができますし、コマンドラインではおなじみのワイルドカードを使うことができます。
>>> 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モジュールは、ワイルドカードに合致するファイルとディレクトリを返します。この例では、ワイルドカードは「ディレクトリパス(examples)+"*.xml"」なので、examplesフォルダ中の".xmlファイル"が該当します。

②カレントワーキングディレクトリをexamplesフォルダに変更します。os.chdir()関数は相対パス名を受け取ります。

③globでは複数のワイルドカードを使うことができます。この例では、カレントワーキングディレクトリの中にあって、".py"の拡張子で終わり、"test"を名前のどこかに含むファイルが取り出されます。

3.2.4. ファイルのメタデータを取得する

最近のファイルシステムでは、各ファイルにメタデータが格納されます。作成日、最終更新日、ファイルサイズ、などです。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は更新時間ですが、このフォーマットは非常に使いにくいです(正確には、Epochと呼ばれる1970年1月1日からの秒です)。

④timeモジュールはPython標準ライブラリの1つです。この中の関数を使って、時間の表記を変えたり、時刻を文字列に変えたり、タイムゾーンを変更したりできます。

⑤time.localtime()関数は、os.stat()、st_mtimeで得たEpoch秒を、もっと使いやすい年、月、日、時、分、秒などに変換します。このファイルの最終更新は、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属性をaproximate_size()関数に渡します。

3.2.5. 絶対パス名

前節では、glob.glob()関数が相対パス名のリストを返しました。最初の例ではパス名は"examples\feed.xml"で、2番目の例ではもっと短い相対パス名で"remantest1.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

3.3. リスト内包表記

内包表記は、リストを別のリストに写像する短縮表記法で、リストの各要素に対して関数を適用します。
>>> 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]

①読み解くために、右から左へと見てみましょう。a_listは写像されるリストです。Pythonインタプリタはa_listの要素を1つずつループし、一時的に各要素の値を変数elemに代入します。Pythonはelem*2という関数を計算し、新しいリストに加えます。

②リスト内包表記は新しいリストを作るので、元のリストは変更されません。

③内包表記の結果を写像する元のリストに代入することもできます。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']
①カレントワーキングディレクトリにある.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ファイルのリストを見て、if節で各ファイルのサイズが6000バイトより大きいかどうかを判定します。今回は該当するファイルが6つあり、リスト内包表記は6つのファイル名のリストを返します。

これまで見てきたリスト内包表記は、簡単な表現でしか判定していませんでした。定数を掛ける、ひとつの関数を呼び出す、(フィルタリングして)単純に元の要素を返す、などです。しかし、リスト内包表記はどれだけ複雑になっても大丈夫です。
>>> 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ファイルのサイズを出力しています。

3.4. 辞書の内包表記

辞書の内包表記は、リスト内包表記と同様ですが、リストの代わりに辞書が作られます。
>>> 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)                                             # ④

>>> 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()関数を呼び出して作ります)のタプルを生成します。

②出力されたリストの各要素はタプルです。

③こちらは辞書の内包表記です。まず、波括弧で囲み(四角括弧ではありません)、条件は1つではなくコロンで分けた2つの条件を各要素に与えます。コロンの前の条件(f)は辞書のキーで、コロンのあとの条件(os.stat(f))が辞書の値です。

④辞書の内包表記は辞書を返します。

⑤この辞書のキーは単なるファイル名で、glob.glob.("*test*.py")を呼び出して返ったものです。

⑥各キーに対する値は、os.stat()関数で返ったものです。つまり、ファイル名で辞書内から“見つけ出して”、メタデータを取り出したのです。メタデータのひとつはst_sizeで、これはファイルサイズを返します。alphameticstest.pyというファイルは2059バイトだとわかりました。

リスト内包表記と同じように、辞書内包表記に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])、サイズは各ファイルのおよその値です(humansize.approximate_size(meta.st_size) を使います)。

③前の例でみたように6つのファイルが該当します。つまり、この辞書にも6つの要素があります。

④各キーの値はapproximate_size()関数から返ってきた文字列です。

3.4.1. 辞書の内包表記で使える小ワザ

辞書の内包表記で、いつか役に立つトリックを1つ紹介しましょう。キーと値を入れ替える方法です。
>>> 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 "", line 1, in 
  File "", line 1, in 
TypeError: unhashable type: 'list'

3.5. セット内包表記

ご多分に漏れず、セットにも内包表記があります。辞書の内包表記と非常によく似ています。違いはキー:値のペアではなく、セットが入ることだけです。
>>> 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乗を計算します。

②リストや辞書の内包表記のように、セット内包表記にはif節を入れてフィルターをかけることができます。

③セット内包表記が受け取るのは必ずしもセットでなくても構いません。どんな配列でもよいのです。

3.6 さらに読みたい人に

  •     os module
  •     os — Portable access to operating system specific features
  •     os.path module
  •     os.path — Platform-independent manipulation of file names
  •     glob module
  •     glob — Filename pattern matching
  •     time module
  •     time — Functions for manipulating clock time
  •     List comprehensions
  •     Nested list comprehensions
  •     Looping techniques

0 件のコメント:

コメントを投稿