最初に、本セミナーで使用するファイルのダウンロードを行います。次のソースコードを、実行してください。Google Colaboratoryでソースコードを実行するには、ソースコードの任意の場所をクリックした後、次のどちらかの操作を行います。
!git clone https://github.com/tendo-sms/python_intermediate_2022
%cd /content/python_intermediate_2022/01_skillup/rootdir
データ構造化においては、計測データの読み込み、構造化データやグラフのファイル出力など、ファイルを取り扱う処理が必須となります。
ここでは、入出力ファイルのパスを操作するときに役立つ機能・テクニックをご紹介します。
ファイル・ディレクトリを扱う操作を行うとき、入門書などでよく取り上げられるos.pathモジュールやglobモジュールなどを使うことが多いと思います。
ここでは、os.pathモジュールやglobモジュールよりも便利な、「pathlibモジュール」についてご紹介します。
pathlibには多くの機能がありますが、ここではデータ構造化プログラムで役に立つ機能を次の表にまとめます。
機能 | 内容 | 対応する機能 |
---|---|---|
glob | ファイル一覧を取得 | glob.glob() |
name | パス末尾のファイル名またはディレクトリ名を取り出す | os.path.basename() |
stem | 拡張子を取り除く | - |
parent | 上位のパスを取り出す | os.path.dirname() |
parents | 上位のパスを要素ごとのリストで取得 | - |
/演算子, joinpath | パスの連結 | os.path.join() |
mkdir | ディレクトリの作成(再帰的な作成も可能) | os.mkdir(), os.mkdirs() |
resolve, relpath_to | 相対パスと絶対パスを相互変換 | os.path.abspath(), os.path.relpath() |
上記の表を見ると、pathlibでしか実現できないことが多いわけではありません。
しかし、用途ごとに別々のモジュールを使い分けるよりも、すべてpathlibにまとめたほうがプログラムを作成しやすいですし、pathlibを使うことをチーム内でルール化すれば、他の人が作ったソースコードを読みやすくなるという利点もあります。
ARIM事業のデータ構造化プログラムでも、pathlibを使用しています。皆さんも、ぜひpathlibを活用してみてください。
pathlibモジュールは、Pythonに標準で組み込まれたモジュールです。
pipやcondaでパッケージをインストールする必要はありません。今回の例では、pathlibモジュールからPathクラスをインポートします。
from pathlib import Path
次のようなディレクトリ構成を例に、ご説明します。
rootdir/
│ file1.txt
│
└─subdir/
file2.txt
pathlibでは、基準とするディレクトリ(今回はrootdir)を元に、次のようにPathオブジェクトを作成します。このオブジェクトに対して、様々な操作を行います。
inputpath = Path("rootdir")
ファイル一覧を取得するとき、globモジュールを使用することが多いです。pathlibではこれに相当する機能として、globメソッドがあります。
# globの場合
import glob
print(glob.glob("rootdir/**/*", recursive=True))
print("-------------------------------------")
# pathlibの場合
for element in inputpath.glob("**/*"):
print(element)
globモジュールとは異なり、結果はリストではなくイテレータ(for文でひとつずつ取り出すことのできるオブジェクト)として取得します。
実際のプログラムを見てみましょう。
filepath = Path("rootdir/subdir/file2.txt")
print(filepath)
print(filepath.name)
print(filepath.stem)
print(filepath.parent)
parentと混同しやすいですが、parentsという機能もあります。parentsは、ファイルパスから親ディレクトリ名、さらにその親ディレクトリ名、さらにその親のディレクトリ名・・・という形でイテレータオブジェクトを取得できます。
実際のプログラムを見てみましょう。
filepath = Path("rootdir/subdir/file2.txt")
for element in filepath.parents:
print(element)
inputpath = Path("rootdir")
concatpath = inputpath / "subdir" / "file2.txt"
print(concatpath)
次に、joinpathを使った方法を見てみましょう。
concatpath = inputpath.joinpath("subdir", "file2.txt")
print(concatpath)
どちらの方法も、単純に文字列を+演算子で連結するやり方と比べて、OSごとのパス区切り文字(Linuxなら「/」、Windowsなら「\」)が何であるかを意識しなくてよいというメリットがあります。
mkdirを使うと、ディレクトリを作成することができます。引数をparents=Trueとすると、中間のディレクトリが存在していなくても自動的に作成します。
adddir1 = Path("rootdir/adddir1")
adddir2 = Path("rootdir/adddir2/adddir3")
# 最初はディレクトリが存在しないことを確認
!ls -l rootdir/adddir1
!ls -l rootdir/adddir2/adddir3
print("-------------------------------------")
adddir1.mkdir()
adddir2.mkdir(parents=True)
# ディレクトリが作成されたことを確認
!ls -l rootdir/adddir1
!ls -l rootdir/adddir2/adddir3
# 作成したディレクトリを削除
!rmdir rootdir/adddir1
!rmdir rootdir/adddir2/adddir3
!rmdir rootdir/adddir2
resolve, relpath_toを使うと、相対パスと絶対パスを相互変換できます。実際のプログラムを見てみましょう。
rel_path = Path("rootdir/subdir")
print(rel_path.resolve())
abs_path = Path("/content/python_intermediate_2022/01_skillup/rootdir/subdir")
print(abs_path.relative_to("/content/python_intermediate_2022/01_skillup"))
文字列を様々な書式で表すとき、formatメソッドを使っている方も多いかと思います。Python 3.6から、さらに簡単に書式指定を行える、「f-string (フォーマット済み文字列リテラル)」が利用できます。
formatメソッドと、f-stringを比較してみます。
model = "sem"
maker = "hitachi"
id = 1
# formatメソッド
csvfile_format = "/data/{}_{}_{:0>4}".format(model, maker, id)
# f-string
csvfile_fstring = f"/data/{model}_{maker}_{id:0>4}"
print(csvfile_format)
print(csvfile_fstring)
f-stringでは、文字列の前にキーワード「f」を付与します。書式の指定部分はformatメソッドと同じですが、文字列の中に直接変数名を埋め込むことができるので、より直感的で分かりやすい記述とすることができます。
一度に複数のファイルをオープンしたいとき、次のように記述していないでしょうか。
with open(ファイルパス1) as ファイルオブジェクト1:
with open(ファイルパス2) as ファイルオブジェクト2:
ファイル1、ファイル2を使う処理
次のように記述することで、複数ファイルを一度にオープンすることができます。
with open(ファイルパス1) as ファイルオブジェクト1, open(ファイルパス2) as ファイルオブジェクト2:
ファイル1、ファイル2を使う処理
インデントが深くならないのが嬉しいポイントです。インデントが深すぎるプログラムはとても読みづらいので、ぜひ活用してください。
wavelength = [20, 20.02, 20.04, 20.08]
intensity = [3, 0, 3, 7, 3]
for wavelength_v, intensity_v in zip(wavelength, intensity):
print(f"波長={wavelength_v}, 強度={intensity_v}")
【試してみましょう】
上記のソースコードで、2つのリストの長さが異なる場合は、どのような動作をするでしょうか。
「enumerate関数」は、リストやタプルの値をfor文で一つずつ取り出すとき、インデックス(何番目の要素であるかの値)と値を同時に取り出すことができます。次のソースコードを見てみましょう。
wavelength = [20, 20.02, 20.04, 20.08]
for idx, wavelength_v in enumerate(wavelength):
print(f"インデックス={idx}, 強度={wavelength_v}")
次のソースコードは、for文を使ったリスト内包表記です。リスト内包表記は入門書などでも必ず登場し、皆さんも使う機会は多いと思います。次の例では、0~4までの数列をそれぞれ2乗した値のリストを作成しています。
for_list = [ x ** 2 for x in range(5) ]
print(for_list)
内包表記には、リスト内包表記意外にも「辞書内包表記」や「セット内包表記」というものもあります。その名のとおり、for文を使って辞書やセット(集合)を定義します。
次のソースコードは、「辞書内包表記」の例です。前述の「zip関数」も使っています。
wavelength = [20, 20.02, 20.04, 20.08]
intensity = [3, 0, 3, 7, 3]
for_dict = { wavelength_v:intensity_v for wavelength_v, intensity_v in zip(wavelength, intensity) }
print(for_dict)
「セット内包表記」はソースコード例をリスト内包表記の括弧を[]から{}に変えるだけで、簡単に使用できます。
num_list = [1, 2, 2, 3, 3, 3]
for_set = { x ** 2 for x in num_list }
print(for_set)
リストnum_listに含まれるそれぞれの値を、2乗した値の集合を作成しています。2の2乗や3の2乗が複数回現れますが、結果は集合となるため、値の重複はありません。
Pythonプログラムに欠かせない関数ですが、入門書等では詳しく述べられないような、便利な記述方法がたくさんあります。ここでは、データ構造化のプログラムでも役に立ついくつかのテクニックをご紹介します。
関数を定義するときに、引数のデフォルト値を定めることができます。関数の引数が1つの場合、デフォルト値を指定するには、次のとおり記述します。
def 関数名(引数名 = デフォルト値):
関数の処理
関数を呼び出すときに指定を省略すると、デフォルト値が使われます。実際のソースコードを見てみましょう。
def func_default1(argstr = "Default value1"):
print(f"argstr = {argstr}")
func_default1("value1")
func_default1()
関数の引数が複数あるとき、すべての引数にデフォルト値を指定したり、一部の引数にデフォルト値を指定したりできます。
def 関数名(引数名1 = デフォルト値1, 引数名2 = デフォルト値2, ・・・):
関数の処理
全ての引数にデフォルト値を指定する例を見てみましょう。
def func_default2(argstr1 = "Default value 1", argstr2 = "Default value 2"):
print(f"argstr1 = {argstr1}, argstr2 = {argstr2}")
func_default2("value1", "value2")
func_default2("value1")
func_default2()
ここで、「引数1を指定して、引数2はデフォルト値とする呼び出し方法は?」と疑問を持ったかもしれません。結論として、そのような呼び出し方法はありません。注意してください。
次に、一部の引数にデフォルト値を指定する例を見てみましょう。
def func_default(argstr1, argstr2 = "Default value 2"):
print(f"argstr1 = {argstr1}, argstr2 = {argstr2}")
func_default("value1", "value2")
func_default("value1")
今度は、引数1だけにデフォルト値を定義することを考えてみます。次のように記述すればOKでしょうか?
def func_default3(argstr1 = "Default value 1", argstr2):
print(f"argstr1 = {argstr1}, argstr2 = {argstr2}")
func_default3("value1", "value2")
func_default3("value1")
エラーになってしまいました。実は「引数1だけにデフォルト値を定義する」ことはできません。「デフォルト値を定義した引数より後ろに、デフォルト値がない引数を定義することは出来ません。気を付けましょう。
def func_args(*args):
print(args)
for val in args:
print(val)
func_args(1, 2, 3)
func_args(1, 2, 3, 4, 5)
次のように、通常の引数と同時に使うこともできます。
def func_args(arg1, *args):
print(f"arg1 = {arg1}")
print(f"args = {args}")
for val in args:
print(val)
func_args(0, 1, 2, 3)
func_args(0, 1, 2, 3, 4, 5)
通常の引数の前に可変長引数を定義することもできますが、その場合は関数呼び出し時に通常の引数を「引数名 = 値」という形式で指定する必要があります。
def func_args(*args, arg1):
print(f"arg1 = {arg1}")
print(f"args = {args}")
for val in args:
print(val)
func_args(0, 1, 2, arg1 = 3)
func_args(0, 1, 2, 3, 4, arg1 = 5)
ちなみに、関数を呼び出すときに引数を「引数名 = 値」とする形式は、「キーワード引数」と呼びます。通常のように、引数名を指定せずに値だけ指定する形式は「位置引数」と呼びます。覚えておいてください。
実際のソースコードを見てみましょう。引数が**kwargsの関数を呼び出すときは、前述の「キーワード引数」で引数を指定します。
def func_args(**kwargs):
print(kwargs)
for val in kwargs.values():
print(val)
func_args(val1 = 10, val2 = 20, val3 = 30)
func_args(val1 = 10, val2 = 20, val3 = 30, val4 = 40, val5 = 50)
通常の引数と同時に使う方法や注意事項などは、*argsと同じです。
ここまで、可変長引数として*argsと**kwargsをご紹介しました。実は、*や**さえつければ、「args」「kwargs」の部分は任意の名称となります。ただし、慣例的に「args」「kwargs」という名前を使うのが一般的で、誰が見ても一目で可変長引数だと分かりますので、特に理由がなければ「args」「kwargs」という名前を使いましょう。
自分でプログラムを作成するときに使うことがなくても、冒頭で述べたようにMatplotlibなど外部の関数を使うときによく目にしますので、ぜひ覚えておいてください。
関数を自作するとき、defキーワードに続けて関数名を定義します。それ以外にPythonには、名前のない関数である「lambda関数 (無名関数)」は、という機能があります。
とはいえ、名前のない関数って何?と疑問に思われることと思います。ここでは、lambda関数の使用方法をご紹介します。
lambda関数は、次のように記述します。
lambda 引数: 返り値
これは、次のようにdefを使って関数を定義するのと同じ動作です。ただし、lambda関数には関数名がありません(lambdaは機能名であり、関数名ではありません)。
def 関数名(引数):
return 返り値
このように言われても、何に使うのか、よく分からないですよね。実際のソースコードを見ながら、解説します。次のソースコードは、「sorted関数」を使ってリストの並び替えをしています。
list1 = ["Tokyo", "Fukuoka", "Nagoya"]
sorted_list1 = sorted(list1)
print(sorted_list1)
上記のように、sorted関数に文字列のリストを渡すと、アルファベット順に並び替えが行われました。
sorted関数では、どのようなルールで並べ替えるかを、2番目の引数keyに関数を与えることで設定できます。すると、リストの各要素を引数として指定した関数を実行し、その戻り値の順序でソートされます。例えば、組み込み関数のlen関数を2番目の引数に与えてみましょう。
list1 = ["Tokyo", "Fukuoka", "Nagoya"]
sorted_list1 = sorted(list1, key = len)
print(sorted_list1)
sortedの2番目の引数「key=len」がポイントです。これにより、リストのそれぞれの要素にlen関数を適用して、その結果、つまり文字列の長さでソートされます。
今度は、組み込み関数ではなく、自作の関数を使ってみましょう。
def sortfunc(arg_str):
return arg_str[1]
list1 = ["Tokyo", "Fukuoka", "Nagoya"]
sorted_list1 = sorted(list1, key = sortfunc)
print(sorted_list1)
今度は、リストの値の2文字目を使ってソートされました。
このソースコードで、わざわざdefで関数を定義するのは、ちょっと面倒だと思いませんか?こんなとき、lambda関数が役に立ちます。次のソースコードを見てみてください。
list1 = ["Tokyo", "Fukuoka", "Nagoya"]
sorted_list1 = sorted(list1, key = lambda arg_str: arg_str[1])
print(sorted_list1)
最初にご紹介した「lambda 引数: 返り値」の形式で、関数の中身を直接sorted関数の引数に記述しています。非常にすっきりとしたソースコードになりましたね。
関数の中で、引数の値を変更したときの動作には注意が必要です。次のプログラムを見てみましょう。
def func_arg(int_arg, list_arg):
int_arg = 100
list_arg[1] = 200
int_val = 10
list_val = [10, 20, 30]
func_arg(int_val, list_val)
print(int_val)
print(list_val)
関数内で引数のint_argを変更しても、関数の呼び出し元の変数int_valは変更されません。一方、関数内でlist_argの値の一つを変更すると、関数の呼び出し元の変数list_valが変更されます。
実は、値の型が変更可能(ミュータブル)か、変更不可能(イミュータブル)かによって、このように挙動が挙動が変わります。
代表的な型について、ミュータブルか、イミュータブルかをまとめます。
ミュータブルな型の例
イミュータブルな型の例
【ご参考】
C言語など、他のプログラミング言語を学んだことがある方は、「値渡し」「参照渡し」という言葉をご存じかもしれません。
上記の説明で、イミュータブルな型の動作は値渡しに、ミュータブルな型の動作は参照渡しに近いものです。
そのため、「Pythonでも値渡しと参照渡しがあるんだな」と思われたかもしれません。
しかし実は、Pythonはすべて「参照渡し」です。イミュータブルな型は値渡しのような動作となるよう、Python内部で処理されています。
データ構造化において、Pythonのソースコード上ですべてのデータ処理を行うのではなく、機器メーカーが公開しているプログラムなど、既存の外部プログラムを実行してデータ処理を行いたいことも多いです。
Pythonから外部プログラムを実行したい場合は、標準モジュールのsubprocessを利用することで実現可能です。
一昔前までは、Pythonから外部プログラムを実行する場合、osモジュールのsystem関数を使用する(os.system())例が多々見られました。
実はos.system()よりもsubprocessを使用することが推奨されており、最近ではsubprocessの利用例の方が多く紹介されている印象です。
ただし、subprocessの使用方法にはいくつかパターンがあり、さらにOS(WindowsやLinux)によってもオプションの挙動に違いがあるため使用には注意が必要です。
ここでは、基本的な使い方の他、Windows用プログラム(.exe)をLinuxで使用する方法と、ディスプレイが必要なGUIプログラムをディスプレイがない環境で利用する方法について、例を示したいと思います。
subprocessには、外部コマンドを実行するための関数がいくつか準備されています。
# コマンドを実行する。
subprocess.call()
# コマンドを実行し、失敗した場合はCalledProcessError例外をスローする。
subprocess.check_call()
# コマンドを実行し、出力結果を返す。
subprocess.check_output()
現在では、上記の機能を包括したsubprocess.run()を使用することが推奨されています。
それでは、基本的な使い方を確認してみましょう。
# subprocessモジュールのインポート
import subprocess
# 現在の日付を取得するdateコマンドをPythonから実行してみる
cmd = "date"
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print("今日の日付:", result.stdout)
# dateコマンドのオプション引数を指定(表示形式の変換)
cmd = "date '+%Y/%m/%d %H:%M:%S'"
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True)
print("今日の日付(形式指定):", result.stdout)
dateのオプション引数をつけたときに、subprocess.runの中でshell=Trueを指定しました。
これは、Windowsのコマンドプロンプトや、Linuxのbashのような振る舞いをするよう指定するという意味になります。
デフォルトではshell=Falseが指定されており、その場合は指定したコマンド文字列をひとつのコマンドとして実行してしまいます。
# dateコマンドのオプション引数を指定(表示形式の変換)
cmd = "date '+%Y/%m/%d %H:%M:%S'"
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=False)
print("今日の日付(形式変換):", result.stdout)
このように、「date '+%Y/%m/%d %H:%M:%S'」なんていうコマンドなんてないよと言われてしまいました。
'+%Y/%m/%d %H:%M:%S'の部分はdateコマンドのオプションであるので、分離して理解してほしいため、shell=Trueが必要です。
実はコマンドにはリスト形式でも指定することができます。その方がわかりやすいため、リスト形式を使うのがおすすめです。
ただし、このリストでコマンドを指定する場合はshell=Falseにする必要があります。 (デフォルトではshell=Falseのため、指定しなくてよい。)
# リスト形式でのdateコマンドの実行
cmd = ["date", "+%Y/%m/%d %H:%M:%S"]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print("今日の日付(形式変換):", result.stdout)
LinuxではWindows用プログラム(exe)を実行するためのwineというプログラムがあります。
全てのプログラムが動作するという保証はないですが、Linuxをベースとした構造化プログラムの中に組み込むことも可能です。
まずはwineをインストールします。
# wine4.0.4をインストールする。
!apt update
!apt install -y wget libgtkglext1 libpango1.0-0 libpangox-1.0-0 libgtk2.0-0
!dpkg --add-architecture i386
!apt update
!mkdir -pm755 /etc/apt/keyrings
!wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
!wget -nc -P /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/bionic/winehq-bionic.sources
!apt update
!apt install -y --install-recommends wine-stable-i386=4.0.4~bionic wine-stable-amd64=4.0.4~bionic wine-stable=4.0.4~bionic winehq-stable=4.0.4~bionic
ここでは、アルバック・ファイ株式会社から提供されているMPExport.exeを例に説明します。
MPExport.exeは、speデータをtxt形式に変換するWinodws用のプログラムです。
Windowsでは通常次のように実行します。
MPExport.exe -Filename sample.spe -OutputFolder .\ -LogFolder \.
上記のコマンドをLinuxOSから、subprocessを使って呼び出してみます。
# コマンドの内容を指定
MPExport = "/content/python_intermediate_2022/01_skillup/MPExport.exe"
spefile = "\content\python_intermediate_2022\\01_skillup\sample.spe"
output = "\content"
log = "\content"
# コマンドをリスト形式で定義
cmd = ["wine", MPExport, "-Filename", spefile, "-OutputFolder", output, "-LogFolder", log]
# 実行
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print("結果", result.stdout)
成功していればsample.txtが出力されているはずです。 左のフォルダから確認してみましょう。
GUI(グラフィカルユーザインターフェース)のプログラムは、ディスプレイ上にウィンドウを表示させて利用者がマウスでクリックなどしながら操作します。
※ちなみに、コマンドラインで操作する場合をCUI(キャラクタユーザインターフェース)と呼ぶ。
構造化処理では、ディスプレイがないクラウド環境で実行しなければならないためディスプレイがないとエラーが出てしまいます。
そこで、仮想ディスプレイという技術を利用することでこの問題を解消します。
ここでは、VESTA(結晶構造、電子・核密度等の三次元データ、及び結晶外形の可視化プログラム)を使って説明します。
VESTAは、GUIのプログラムでありながらコマンドラインからある程度の操作が可能です。
ただし、ディスプレイは必要なので仮想ディスプレイを使います。
# 仮想ディスプレイを準備します。
!apt update
!apt -y install libglu1-mesa libgtk-3-dev libgomp1 xvfb wget zip
!pip install pyvirtualdisplay
VESTAをダウンロードして解凍、準備します。
!wget https://jp-minerals.org/vesta/archives/3.5.8/VESTA-gtk3.tar.bz2
!tar jxf VESTA-gtk3.tar.bz2
cifファイルを読み込んで画像を生成します。
import time
import subprocess
from pyvirtualdisplay import Display
with Display() as disp:
cmd = ["VESTA-gtk3/VESTA", "-open", "/content/python_intermediate_2022/01_skillup/sample.cif", "-export_img", "sample.png"]
proc = subprocess.Popen(cmd)
time.sleep(10)
proc.kill()
GUIつきのプログラムでは、subprocessを使用した外部起動では同期して実行することが難しいため、subprocess.Popenを使って実行しています。
また、ここではVESTAが描画するまで10秒を待ってから終了させています。
結晶構造の画像が作成されたか、確認してみます。
from IPython.display import Image,display_png
display_png(Image("sample.png"))