Pythonまとめ(1.基本構文)

本記事は筆者が業務自動化などでたまに使うPythonを、すぐ思い出せるようにポイントをまとめたものです。初学者がわかりやすいように、あるいは正確で十分な記載になるようにという観点ではまとめていないため、ちゃんと学習したい方は他のサイトをご利用ください。

表記ルール

プログラムコードは以下のように原則としてブロックで示す。表の中などインラインで示すときはpython codeのように記す。

python code

実行結果がある場合は水色のブロックで示す。

出力結果

プログラムコードの中には対話モードで実行したものが含まれる。その場合は「>>>」または「...」が行頭にあるものがプログラムコードであり、行頭にないものは実行結果、出力である。

>>> def method():
...     print(1)
...
>>> method()
1                    # 実行結果

コードや入力などにおいて半角空白があることを明示するときは「␣」を使用する。同様に改行があることを明示するときには「␤」を使用する。

基本構文

変数の宣言

変数とは何らかの値と名前を関連付けて保存するものである。Pythonでは宣言や定義することなく変数を使用できる。もし初期化されていない変数を使おうとするとUnboundLocalErrorやNameErrorが発生する。

varname = 値 # これにより変数varnameは値を参照するように初期化される

print(  varname ) # これは初期化されている変数をしようしているため問題なし
print( nonename ) # nonenameは初期化されていない変数のためエラーが発生する

データ型

データ型とは変数が参照しているデータの種類のことである。Pythonは動的型付けに分類されるプログラミング言語であり、変数の型は実データが代入されたときに決定する。そのためVisual Studioのような開発環境では変数の型を推定して開発支援を行う。たとえばvariable = "string"というコードからは変数variableが文字列であることが推定できる。そうするとvariableが文字列であるとして、文字列で使用可能なメソッド候補を表示したりできる。

しかし、場合によっては推定が十分に行えないことがある。その場合は型ヒントを記載することで開発環境やコンパイラに対して変数の型のヒントを与えることが出来る。当然ながらこの型ヒントは開発者にとっても意味のあるものとなる。

型ヒントの記載方法として型アノテーションが使用できる。詳細は型ヒントの項目を参照すること。この型アノテーションはいわば型に関するコメントであり、プログラムの動作には影響しない。

varname:str = "value"

関数の定義

関数は入力を受け取り、何らかの処理を行い、結果を出力する処理のかたまりである。Pythonにおいて関数を定義するにはdefを使う。プログラムにおいて関数の入力を引数、出力は戻り値と呼ぶ。

def 関数名(引数名1,引数名2,...):
    関数の処理
    return 戻り値

なお、型アノテーションをつけると以下のようになる。

def 関数名(引数名1:引数名1の型, 引数名2:引数名2の型, ...) -> 戻り値の型:
    関数の処理
    return 戻り値

引数

仮引数と実引数

引数について仮引数と実引数という2つの用語が使い分けられることがある。仮引数とは関数を定義するときに引数部分に書く変数のことであり、実引数とは仮引数に代入される具体的な値のことである。英語ではそれぞれparameter(仮引数)とargument(実引数)と呼ばれる。英語の方が分かりやすいだろう。

和を求める関数sumで2つの違いを見る。

def sum(a:int, b:int) -> int:
    return a+b

この関数定義の引数a、bは変数であり、どちらも関数が呼びだれるときに具体的な値が決まる。これがパラメータ、すなわち仮引数である。次にこのsum関数を呼び出す。

sum(4, 5)

このsum関数に渡された引数の値4、5は仮引数に代入される具体的な値であり実引数である。

デフォルト値

引数のデフォルト値を指定することができる。デフォルト値がある仮引数は呼び出し時に省略できる。以下の例であればfunction_name(10)でも呼び出すことができる。

def function_name( arg1, arg2 = default2, arg3 = default3, ... ):
    関数の処理

デフォルト値を持つ仮引数の後に、デフォルト値を持たない仮引数を定義することはできない。以下はデフォルト値を持つの引数の後ろに、デフォルト値を持たない引数がある不適切なコードである。

def methodtest( arg1, arg2="", arg3): # 誤り
    pass
位置引数とキーワード引数

Pythonでは実引数を指定するときに、仮引数の定義順に書く方法(位置引数)仮引数の名前を指定して書く方法(キーワード引数)の2つがある。デフォルトでは引数は位置引数とキーワード引数の両方として振る舞うことができる。

def checksum(value1, value2, sum):
    return value1+value2 == sum

上のような、3つの引数を持つ関数で考えてみる。この関数はvalue1とvalue2の和がsumになるかチェックする関数である。

この場合、一番シンプルな実引数の指定方法は以下のように仮引数の定義通りに書く方法である。これが位置引数である。

>>> checksum(2,5,6) # Check if 2+5 equal 6
False
>>> checksum(2,5,7) # Check if 2+5 equal 7
True

キーワード引数を使うと以下のように、定義された順番を無視して書くことができる。

>>> checksum(sum=10,value1=3,value2=7) # Check if 3+7 equal 10
True

以下のように位置引数とキーワード引数を混在させてもよい。ただし、この例では混在させるメリットは特にない。

>>> checksum(3, sum=7, value2=4) # Check if 3+4 equal 7
True

キーワード引数は位置を無視して値を渡す方法であるため、位置引数とキーワード引数を混在させるときには、位置引数よりキーワード引数が先にくる。

位置専用引数とキーワード専用引数

デフォルトでは位置引数・キーワード引数の両方で値を渡すことができるが、どちらか一方でのみ値を渡せる引数も作成できる。関数の定義において/の前に定義された引数は位置引数としてしか値を渡せない位置専用引数となり、キーワードを指定して引数を渡すことはできない。*の後に定義された引数はキーワード専用引数となり、位置で引数を渡すことはできない。

def method(位置専用引数, /, 標準の引数(位置引数でもキーワード引数でも可), *, キーワード専用引数):
    関数の処理

以下のような3つの値を加算する関数で専用引数の例をみてみる。

>>> def sum3(arg1, /, arg2, *, arg3):
...     return arg1+arg2+arg3

この関数ではarg1は位置引数として、arg3はキーワード引数として値を渡さなければならない。arg2はどちらでも良い。

>>> argtest(1, arg2=2, arg3=3)
6
>>> argtest(1, 2, arg3=3)
6

それ以外の方法で値を渡そうとするとエラーになる。

>>> argtest(1, 2, 3)
TypeError: argtest() takes 2 positional arguments but 3 were given
エラーメッセージ訳:argtest()には位置引数が2つあるが3つ指定された

>>> argtest(arg1=1, arg2=2, arg3=3)
TypeError: argtest() got some positional-only arguments passed as keyword arguments: 'arg1'
エラーメッセージ訳:argtest()の位置専用引数の一部がキーワード引数として渡された:'arg1'
可変長引数

仮引数に*または**をつけることで可変長の引数を受け取ることができる。*は位置引数で可変長の引数を受け取るときに使い、値はタプルになる。**はキーワード引数で可変長の引数を受け取るときに使い、値は辞書になる。キーワード引数の制約により両方を併用するときは*より**の方が後にくる。

def printtest(arg1, *arg2, **arg3):
    print(f"  ARG1:{arg1}")
    print(f" *ARG2:{arg2}")
    print(f"**ARG3:{arg3}")

printtest( "引数1", "引数2", "引数3", "引数4", "引数5", Arg6="引数6", Arg7="引数7")
  ARG1:引数1
 *ARG2:('引数2', '引数3', '引数4', '引数5')
**ARG3:{'Arg6': '引数6', 'Arg7': '引数7'}

可変長引数とデフォルト値

可変長引数はデフォルト値があるものとして扱われる。空のタプルか空の辞書となる。以下のコードはエラーなく動作する。

def functest(arg1="", *arg2):
    pass

可変長引数とキーワード専用引数

位置引数の可変長引数は位置引数の最後となり、それ以降はキーワード専用引数となる。そのためキーワード専用引数を明示的に指定しようとするとエラーになる。

def functest(*arg, keyarg1): # これだけでkeyargは位置専用引数
    pass
def functest(*arg, *, keyarg1): # これは*が不要
    pass
値渡しと参照渡し

プログラムにおいて関数に値を渡す一般的な方法に値渡しと参照渡しがある。値渡しは値のコピーを関数に渡すものであり、関数側で値を変更しても呼び出し側に影響を与えることはない。参照渡しは値の場所を関数に渡すものであり、関数側で値を変更すると呼び出し側にも影響を与える。

Pythonにおいては値渡しが使われるが、値として渡されるのは参照である。これを「オブジェクトへの参照渡し」と呼ぶ。Pythonでは参照渡しそのものはできない。

  • 関数側で渡されたオブジェクトに変更を加えた場合、呼び出し側にも影響を与える。ただし、そのオブジェクトがイミュータブルである場合は除く。
  • 関数側で渡されたオブジェクトに別のオブジェクトを代入をした場合、呼び出し側には影響を与えない。
シャローコピーとディープコピー

オブジェクトが参照渡しによる影響を受けないようにするには、オブジェクトをコピーし、コピーしたオブジェクトを関数に渡す

2×2の行列を書き換える関数でオブジェクトのコピーについて考える。

def updatelist(list2x2):
    list2x2[0][1] = 6
    list2x2[1] = [7,8]

2x2の行列を参照する変数list1を他の変数list2に代入し、list2をupdatelist関数に渡してみる。

def main():
    list1 = [[1,2], [3,4]]

    list2 = list1
    updatelist(list2) # list2を更新する関数

    print( f"{list1=}" )

main()
list1=[[1, 6], [7, 8]]

この方法ではコピーされていないためlist1も影響を受ける。このプログラムがlist1の元の値を残しておきたいという意図でlist2にlist1を代入したのであれば、意図したとおりに動作しないため不具合である。代入文list2 = list1はlist2とlist1が同じオブジェクトを参照するだけである。

改善案として別のオブジェクトlist2を作成し、list2に値をコピーするように修正してみる。

def main():
    list1 = [[1,2], [3,4]]

    list2 = []
    for item in list1:
        list2.append(item)
        
    updatelist(list2) # list2を更新する関数

    print( f"{list1=}" )

main()
list1=[[1, 6], [3, 4]]

list2は新しいオブジェクトとして作成しているが、updatelistによる更新の影響をlist1も受けている。これはlist2の中身にlist1の元オブジェクトへの参照を含んでいるためである。このような元のオブジェクトへの参照を含むようなコピーをシャローコピー(浅いコピー)とよぶ。シャローコピーではコピー元のオブジェクトに影響を与えることがある。サンプルにおけるシャローコピーのイメージは下図のとおりである。

list1に一切の影響を与えないように修正を加えると以下の通りとなる。

def main():
    list1 = [[1,2], [3,4]]

    list2 = []
    for row in list1:
        temp = []
        for item in row:
            temp.append(item)
        list2.append(temp)

    updatelist(list2) # list2を更新する関数

    print( f"{list1=}" )

main()
list1=[[1, 2], [3, 4]]

list2を新しいオブジェクトとして作成し、その中身も1つずつコピーしている。この場合はupdatelistによる更新はlist1へ影響を与えない。このように完全に別物としてコピーすることをディープコピー(深いコピー)という。サンプルにおけるディープコピーのイメージ図は下図のとおりとなる。

この例では2x2のリストという前提であるが、様々なオブジェクトをコピーしたいことがある。そのような場合にはcopyライブラリのcopy.deepcopy()が便利である。

戻り値

処理結果を呼び出し元に渡すときはreturnを用いる。returnで値が指定されない場合や、return文無しで関数が終了した場合はNoneが返される。

def methodtest1(arg1, arg2):
    return arg1+arg2

def methodtest2():
    pass

print( methodtest1(3,5) )
print( methodtest2() )
8
None

ラムダ式

ラムダ式は名前を持たない小さな関数である。無名関数とも呼ばれる。関数を渡す必要があるが、単純な処理であったり、1度だけ使わないような処理であったり、関数を作るまでもない場合などに使用される。

lambda arguments: expression

以下に2つの引数を持ち、その和を求めるラムダ式の例を示す。

func = lambda x,y: x + y

print( func(1,3) )
4

標準入出力

標準出力

標準出力に表示するにはprint関数を使う。デフォルトでは出力後に改行される。

print('print data')
print data␤

printは出力する対象を任意の個数受け取ることができる。デフォルトでは各データの間には空白が入れられる。

print('print data')
print("data1", "data2", "data3")
data1␣data2␣data3␤

与えられたデータがstrでない場合、str()関数により文字列に変換されて出力される。どのように表示されるかは各オブジェクトの実装に依存する。

print関数の定義

print関数は以下のように定義される。

print( *objects, sep=' ', end='\n', file=sys.stdout, flush=False)
キーワード名デフォルト値意味
sep半角空白出力内容が複数指定されたときの区切り文字
end\n出力終了後に出力する文字
fileNone出力先。write(string)メソッドを持つオブジェクト。Noneの場合はsys.stdout
flushFalseTrueの場合、出力後に強制的にフラッシュする(バッファから出力先へ書き出す)。

標準入力

標準入力から読み込むにはinput関数を使う。引数promptがあると、promptを表示した後に標準入力からの待機状態になる。

input()
input(prompt)

以下に例を示す。

s = input("Enter your ID:")
print(s)
Enter your ID:123
123

条件分岐

if、elif、elseを用いて条件に応じた処理を行う。

if condition1:
    condition1がTrueの場合
elif condition2:
    condition1がFalseかつcondition2がTrueの場合
else:
    すべてのconditionがFalseの場合

反復処理

while

条件を満たす間だけ繰り返す反復処理はwhileを使う。

while condition:
    条件を満たす間、繰り返す処理

条件を満たす間だけ繰り返す反復処理はwhileを使う。

do...while

このwhile文は最初に条件を満たすかチェックするため、一度も処理が実行されないこともある。言語によっては必ず1回は実行するdo...whileがある。Pythonにはdo...whileはないため、以下のように後述のbreakを使って実装する。

while True:
    最低1回は実行する処理
    if condition:
        break

for

forはシーケンスなどIterableなオブジェクトに対する反復処理を表す。

for varname in iterable:
    各要素に対して順番に実行する処理

回数を指定した処理を行う場合はrange関数と併用する。range関数は指定した範囲の整数を表すオブジェクトを返す関数である。たとえばrange(5) = (0,1,2,3,4)、range(3,6) = (3,4,5)となる。

for varname in range(times): 
    times回実行する処理
for varname in range(start, end): 
    end-start回実行する処理

反復処理の途中終了

break

forまたはwhileの中でbreakに到達すると、現在の反復処理を完全に終了し、反復処理の後に続く処理に移る。

a = 0
while True:
    a += 1
    if a > 3:
        break
    print(a)
print("end")
1
2
3
end

この例ではwhile True:となっている無限ループである。しかしa>3のときにbreakに到達するようになっており、そこで反復処理が終了する。

continue

forまたはwhileの中でcontinueに到達すると、現在の反復処理を終了し、次の値の反復処理から再開する。

a = 0
while a < 5:
    a += 1
    if a < 3:
        continue
    print(a)
print("end")
3
4
5
end

この例ではaが0から5に達するまで繰り返され、値を表示するプログラムである。しかしa<3のときはcontinueに到達するようになっており、6行目のprint(a)は実行されることなく反復処理の最初から再開される。

反復終了後の処理

forまたはwhileにelseがついている場合、そのブロックはループがbreakすることなく終了した場合に実行される。これにはループが1回も実行されなかった場合も含まれる。

以下に数値のリストの中に負の値が含まれるかを表示する例を示す。

def find_negative(*numbers):
    for n in numbers:
        if n < 0:
            print(f"found:{n}")
            break
    else:
        print('not found')


find_negative(4,5,-3,5,-9) # 負数を含む場合
find_negative(2,6,5,6)     # 負数を含まない場合
found:-3
not found

負の値があるとbreakに到達し、ループが途中終了する。この場合はelseのブロックは実行されない。負の値がない場合はループが最後まで処理するため、elseのブロックが実行される。

pass

何もしない文。構文規則上で文が必要だが、処理することがないときに使う。

パターンマッチ(match...case)

上から順にパターンを評価し、最初にマッチした処理を実行する。_(アンダーバー)はすべてに一致する。この構文はPython 3.10から導入されたため古いバージョンでは動作しない。

match x:
    case 1:
        xが1のときの処理
    case 2:
        xが2のときの処理
    case _:
        xが1でも2でもないときの処理

switchとの違い

C言語などでswitch…caseを使ったことがある人は、break文がないことに注意。Pythonのmatch…caseは最初に一致したcaseの処理だけが実行され、後続のcaseが実行されることはない。

or条件

|でパターンをOR条件で複数指定できる。

match x:
    case 1|2|3:
        xが1か2か3のときの処理
    case _:
        それ以外のときの処理

追加条件(ガード)

パターンが一致し、かつif条件を満たす場合にマッチしたとみなすことができる。

match x:
    case 1 if y == True:
        xが1かつyがTrueのときの処理
    case 1|2:
        xが1または2のときの処理

コレクションのパターンマッチ

リストやタプル

マッチする対象がリストやタプルの場合、要素ごとにパターンマッチできる。要素ごとに|(OR指定)や_(無条件一致)を指定できる要素数も一致する必要がある

match ['a', 200, 30]:
    case 'a'|'b', 100, 10|20:
        print( 'pattern 1' )
    case 'a', 100|200, 30:
        print( 'pattern 2' )
    case 'b', 100, _:
        print( 'pattern 3' )
    case 'b', 200, _:
        print( 'pattern 4' )
可変長のシーケンスマッチ

case _,_:と指定すると長さが2のシーケンスにのみマッチする。可変長引数と同じように*をつけることで可変長のリストにマッチさせることができる。一部の要素でパターンマッチをしたいが、要素数は問わない場合に利用できる。

match [1,2,3,4]:
    case 1,2,_:         # 長さが違うのでマッチしない
        print("1,2,_")
    case 1,2,*_:        # これにはマッチする
        print("1,2,*_")

後述のキャプチャとも併用できる。複数の引数を受け取る命令文を解析する例を示す。

values = "drop table customer price".split(" ")
print(values)

match values:
    case "drop","table",*tablenames:
        print("==drop table==")
        for table in tablenames:
            print(table)
['drop', 'table', 'customer', 'price']
==drop table==
customer
price

なおcase _:は長さが1以外のリストやタプルにもマッチする。タプルの各要素にマッチするのではなく、タプル全体にマッチすると考えれば良い。

辞書(マッピング)のパターンマッチ

辞書ではキーに対応する値が一致するかでパターンマッチできる。値をアンダーバーにするとキーが存在すれば一致する。指定されなかったキーはマッチ対象とならない。

def matchtest(dict1):
    match dict1:
        case {"TYPE":0}:          # キーTYPEの値が0であること
            print("pattern 1")
        case {"ID":_, "NAME":_}:  # キーIDとNAMEが存在すること
            print("pattern 2")
        case {"ID":_}:            # キーIDが存在すること
            print("pattern 3")
        case _:
            print('error')

matchtest( {"TYPE":0, "ID":100} ) # Pattern 1
matchtest( {"TYPE":1, "ID":10, "NAME":"test"} )  # Pattern 2
matchtest( {"TYPE":2, "ID":20, "NOTE":"test"} )  # Pattern 3
matchtest( {"TYPE":2, "NAME":"test", "NOTE":"test"} ) # Pattern error

キャプチャパターン

マッチした値を名前に関連付け、caseの中で参照できる。これをキャプチャと呼び、caseで名前を指定することで実現できる。

match [1, 1, 5]:
    case 1, 1, x: # これは3つ目の値は何でもよく、その値をxにキャプチャするという意味になる
        print( f'pattern 1 and {x=}' )
    case x:       # これはすべてのオブジェクトにマッチし、その値をxにキャプチャという意味になる
        print(x)

値に条件がある場合はasを使う。

def matchtest(list1):
    match list1:
        case 1, 0, 1|2|3 as z:
            print( f'pattern 1. {z=}' )
        case 1, 0, z:
            print( f'pattern 2. {z=}' )
        case [0, *y] as z:
            print( f'pattern 3. {y=}, {z=}' )
        case z:
            print( f'pattern 4. {z=}' )

matchtest( [1, 0, 1] )  # Pattern 1
matchtest( [1, 0, 5] )  # Pattern 2
matchtest( [0, 9, 4] )  # Pattern 3
matchtest( [1,-3, 5] )  # Pattern 4
pattern 1. z=1
pattern 2. z=5
pattern 3. y=[9, 4], z=[0, 9, 4]
pattern 4. z=[1, -3, 5]

定数や変数とのパターンマッチ

変数の値とパターンマッチさせようとしてcaseに変数名を指定するとキャプチャパターンとみなされてしまう。しかし.を含む名前はキャプチャパターンとみなされないため、Enumなどの定数とパターンマッチできる。

values = [1,2,3,4]

class Number():
    A:int = 1
    B:int = 2

for value in values:
    print(f"{value}:", end="")
    match value:
        case Number.A:
            print("A")
        case Number.B:
            print("B")
        case _:
            print("_")
1:A
2:B
3:_
4:_

クラスパターンマッチ

任意のクラスともパターンマッチできる。caseでクラス名と属性の値を指定する。たとえばcase Point(x=0, y=_)と指定した場合は以下のようにパターンにマッチするか検査される。

  1. マッチ対象のオブジェクトがPointまたはその派生型であるか(isinstance(obj, Point)がTrueとなるか)
  2. xアトリビュートを持ち、その値が0であるか
  3. yアトリビュートを持っているか
from abc import ABC

class Point(ABC):
    pass

class Point1D(Point):
    def __init__(self, x=0):
        self.x = x

class Point2D(Point):
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

class Point3D(Point):
    def __init__(self, x=0, y=0, z=0):
        self.x = x
        self.y = y
        self.z = z

pointtable = [
    [Point1D(x= 1),           Point1D(x= 2),           Point1D(x= 3),          ],
    [Point2D(x= 1,y=-1),      Point2D(x= 2,y=-2),      Point2D(x= 3,y=-3),     ],
    [Point3D(x= 1,y=-1,z=10), Point3D(x= 2,y=-2,z=20), Point3D(x= 3,y=-3,z=30),],
]

for pointrow in pointtable:
    for point in pointrow:
        match point:
            case Point(x= 3, y=-3):
                print('pattern 1')
            case Point(x= _, z= _):
                print('pattern 2')
            case Point(y=_):
                print('pattern 3')
            case Point(x = 1, y =-1|-2):
                print('pattern 4')
            case Point(x = 1):
                print('pattern 5')
            case Point():
                print('pattern 6')
            case _:
                print('No Match' )
pattern 5
pattern 6
pattern 6
pattern 3
pattern 3
pattern 1
pattern 2
pattern 2
pattern 1

上のサンプルでは基底クラスPointだけでパターンマッチしているが、case Point1D(x=3)|Point2D(x=3):のように混在させても構わない。

組み込み型のクラスパターンマッチ

組み込み型(bool、bytearray、bytes、dict、float、frozenset、int、list、set、str、tuple)は例外として属性名を指定せず位置引数のような形で値を指定する。キャプチャパターンと組み合わせると、若干わかりにくいかもしれない。

numbers = [ 1, 1.3, 3, 5.5, "A" ]

for number in numbers:
    print(f"{number:>3}: ", end="")
    match number:
        case int(1|2):
            print("1 or 2")
        case int(a): # これは int() as a と等しい
            print(f"integer {a}")
        case int() | float():
            print("number")
        case _:
            print("not number")
  1: 1 or 2
1.3: number
  3: integer 3
5.5: number
  A: not number

基本的な型

はじめに

Pythonで使われる基本的な型を示す。

種類型名
数値(整数)int
数値(浮動小数)float
論理bool
日時datetime
文字列str
主な型

複数の要素を保持する型をコレクションという。主なコレクションを示す。

種類型名
リスト(配列)list
タプルtuple
辞書dictionary
集合set
主なコレクション型

数値型

Pythonでは数値を表す型として整数値を表すintと浮動小数点を表すfloatがある。Python3ではintではビット長が定められておらず、メモリの許す限り大きな値を扱える。一方でfloatは64bitの浮動小数点表現である。

>>> a = 124
>>> b = 12.5
>>> type(a)
<class 'int'>
>>> type(b)
<class 'float'>

floatは浮動小数点表現であるため誤差が生じる。試しに0.3-0.2-0.1という計算をしてみると、0にとても近い値(\textrm{-2.7…e-17} \risingdotseq -2.7\times 10^{-17} = -0.000000000000000027)になる。

>>> 0.3-0.2-0.1
-2.7755575615628914e-17

高精度な計算を必要とするときはdecimalパッケージが利用できる。同じ計算をdecimalパッケージを使用すると0の結果が得られる。

>>> import decimal as d
>>> d.Decimal('0.3')-d.Decimal('0.2')-d.Decimal('0.1')
Decimal('0.0')

浮動小数点の等価比較

decimalパッケージを使用しないで浮動小数点を比較するには、2つの値が十分に近いときに等価であるとみなす。mathパッケージのmath.iscloseメソッドが利用できる。

よく使う数値演算・関数

算術演算子
計算演算子
足し算+
引き算-
掛け算*
べき乗**
割り算/
割り算の商//
割り算の余り(剰余)%
数学演算
計算関数/演算子サンプル実行結果
絶対値abs( x )abs( -14 )14
四捨五入round( x, 桁数 )round( 12.345, 1 )12.3
round( 12.345, -1 )10.0
単純切り捨てmath.trunc( x )math.trunc( 15.8 )15
math.trunc( -15.8 )-15
-方向の切り捨て(床関数)math.floor( x )math.floor( 15.8 )15
math.floor( -15.8 )-16
切り上げ(天井関数)math.ceil( x )math.ceil( 15.8 )16
math.ceil( -15.8 )-15
最大公約数math.gcd( x, y )math.gcd( 1134, 3132 )54
最小公倍数math.lcm( x, y )math.lcm( 1134, 3132 )65572
平方根math.sqrt( x )math.sqrt( 2 )1.41421356..
n乗根x ** (1/n)27 ** (1/3)3
三角関数
計算演算子サンプル実行結果
sin(正弦)math.sin( rad )math.sin( math.radians(30) )0.499...
cos(余弦)math.cos( rad )math.cos( math.radians(30) )0.866...
tan(正接)math.tan( rad )math.tan( math.radians(30) )0.577...

角度の単位

角度の単位はラジアンである。度とラジアンを変換する関数として math.degrees()、math.radians()がある。

逆三角関数

逆三角関数はaをつける。たとえばarcsin(逆正弦)ならmath.asin( x )となる。なお、逆正接にはmath.atan( x )math.atan2( y, x )の2つがある。atanは与えられた辺の比率となる角度\theta (-\dfrac{\pi}{2} \leqq \theta \leqq \dfrac{\pi}{2})を返す。atan2はXY平面上の点における偏角\thetaを返す。例えばmath.atan2(-1,-1)は-135度に相当するラジアンを返す。

数学定数
定数定義
円周率math.pi
自然対数の底math.e

論理型 bool

boolは真(True)、偽(False)を表すオブジェクトで、intのサブクラスである。Trueは1であり、Falseは0である。

>>> True + True
2

bool以外のオブジェクトを論理型に変換する場合、以下のオブジェクトがFalseとして扱われる。

  • None
  • 数値型において0であるもの(0.0やDecimal(0)などを含む)
  • コレクションにおいて空であるもの
    • 空文字列("")を含む

この定義から、文字列が空かあるいはNoneであることを確認したいならif not var:のように書くだけで良いことになる。

比較演算子

比較演算子は2つのオブジェクトを比較し、論理値を返す演算子である。オブジェクトによって小さい、大きいの定義は異なることがある。また異なるオブジェクト同士では比較できない場合がある。

比較演算子サンプル
等しい==10 == 20
等しくない!=10 != 20
より小さい(未満)<10 < 20
より大きい(超)>10 > 20
以下<=10 <= 20
以上>=10 >= 20
範囲内< ... <
=付きも可
5 < 10 <= 10
5 < 10 < 15 == 20 ※3個以上も可
含むin10 in (10,20,30)
含まないnot in10 not in (10,20,30)
同じオブジェクトisa is b

型の比較

オブジェクトの型はtype関数で取得でき、isで型が一致するか判定できる。これは型が完全一致している必要があり、派生型では一致しない。そのため型を比較するときはisinstance関数を使うことのほうが個人的には多い。

# 完全一致
type( obj ) is (class1, class2, ...)     # 比較対象のクラスが1つだけであればタプルにしなくて良い

# 継承を含む
isinstance( obj, (class1, class2, ...) ) # 比較対象のクラスが1つだけであればタプルにしなくて良い

type関数とisinstance関数の動作の違いを示すサンプルを示す。

class A():
    pass

class B(A):
    pass

a = A()
b = B()

print( "type比較" )
print( type(a) is A )       # True
print( type(b) is A )       # False

print( "isinstance比較" )
print( isinstance(a, A) )   # True
print( isinstance(b, A) )   # True

isinstanceは派生型でも一致するため、boolやIntEnumがintに一致する点などに注意が必要である。

isinstance( True, int ) # True

class Number(IntEnum):
    A = 1
    B = 2

isinstance( Number.A, int ) # True

文字列 str

文字列リテラル(文字列定数)

文字列リテラルは1つまたは3つのクオーテーション('または")で囲まれた文字列の定数のことである。

'文字列'
'''文字列'''

"文字列"
"""文字列"""

三重引用符には改行を含めることができる。改行は\nとして扱われる。'で囲んだ文字列リテラルでは"をそのまま文字として使うことができる。逆に"で囲んだ文字列リテラルでは'をそのまま使うことができる。そうでない場合はエスケープが必要となる。

sampletxt = """
"Sample"
'Text'
"""

print(repr(sampletxt))
'\n"Sample"\n\'Text\'\n'

ソースコードの見やすさのためにインデントすると、インデントもリテラルに含まれてしまう。textwrapモジュールのdedent(de-indentの略)を使うと行頭の空白を削除してくれる。

import textwrap

sampletxt = """
            "Sample"
            'Text'
            """

print("--")
print(sampletxt)
print("--")
print(textwrap.dedent(sampletxt))
print("--")
--

␣␣␣␣␣␣␣␣␣␣␣␣"Sample"
␣␣␣␣␣␣␣␣␣␣␣␣'Text'
␣␣␣␣␣␣␣␣␣␣␣␣
--

"Sample"
'Text'

--

特殊文字(エスケープシーケンス)

一般的な文字では表現できないが特別な意味があるもの(たとえばバックスペース)を、文字として表現するために特殊文字を使う。多くの言語では「バックスラッシュ+文字」で表される。Pythonでよく使われる特殊文字は以下の通り。

  • \n 改行
  • \t タブ
  • \\ バックスラッシュ
    • 文字として\を出力するには\\のように2つ重ねる。
  • \' \" 引用符

ロー文字列

ほとんどのエスケープシーケンス処理が無効となる文字列。文字列の前にrをつける。

>>> print('文字\n列')
文字
列
>>> print(r'文字\n列')
文字\n列

ロー文字列において、最後にバックスラッシュを置くことはできない。その場合はr"test" + "\\"のようにする。

フォーマット文字列(フォーマット済み文字列リテラル、f文字列、f-string)

{}内の式が文字列に展開される。{}内で計算も可能。文字列の前にfかFをつける。Python 3.6から導入された。

>>> a=1; b=5
>>> f"{a}+{b}={a+b}"
'1+5=6'

フォーマット文字列は、最初に評価したときに文字列にフォーマットされる。ループなどで値が更新される場合は、ループの中で評価されるようにしなければならない。

>>> a = 1; b = 0
>>> frm = f"{a}+{b}={a+b}" # この時点で文字列frmは 1+0=1 となる
>>> for i in range(3):
...     b = i # ここでbの値が変わってもfrmには影響を与えない
...     print( frm )
1+0=1
1+0=1
1+0=1

なお導入当初はフォーマット文字列に実装上の制約がいくつかあったが、Python 3.12から解放された。たとえば以下の表記は古いバージョンでは動作しないことがある。

val=100

# {}の中で同じ引用符や\t、\nを使う、f文字列を入れる
print(f"{f"\t{val}"}")

# {}の中で改行
print(f"{
    val
         }")

# コメントをつける
print(f"{
    val # コメント
    }")
        100
100
100
=指定子

変数の後にイコールをつけると変数名も出力できる。デバッグ向けの機能であり、イコールをつけたときはデフォルトでreprによって文字列に変換されるようになる。

import datetime

now = datetime.datetime.now()

print(f"{now}")
print(f"{now = }")
2023-11-11 19:22:17.319629
now = datetime.datetime(2023, 11, 11, 19, 22, 17, 319629)
書式指定子

文字列に変換するときの書式を指定できる。書式の指定方法については「フォーマット書式」を参照すること。

import datetime

now = datetime.datetime.now()
print(f"{now = }")
print(f"{now = !s}")
print(f"{now = :%Y-%m-%d}")
now = datetime.datetime(2023, 11, 11, 19, 32, 17, 619162)
now = 2023-11-11 19:32:17.619162
now = 2023-11-11

文字リストとしての文字列

文字列は文字のシーケンスとみなすことができる。つまり、forループで1文字ずつ処理でき、インデックスを使用して指定番目の部分文字列を取り出すこともできる。

>>> a="test"
>>> for char in a:
...     print(char)
t
e
s
t
>>> a="12345"
>>> print(a[1])
2
>>> print(a[1:-1])
234

ただし、この記法で文字列を書き換えることはできない。strはイミュータブルである。

>>> a = "abcde"
>>> a[1] = 'x'
Exception has occurred: TypeError
'str' object does not support item assignment

文字列の結合

+演算子で文字列を結合できる。文字列以外と結合するには明示的に変換が必要である。文字列リテラル同士であれば+無しでも結合される

print( a + b ) # +が必要
print( "str" "ing" ) # 文字列リテラル同士なら+がなくてもOK
print( "str" f"{b}" ) # 文字列リテラル同士なら+がなくてもOK
print( a + b[0] + b[1] + b[2] )

CSVのように特定の区切り文字を使って結合するには'区切り文字'.join( 文字列のリスト )が便利である。ただしCSVなら行われるべき処理(たとえばセル内改行のような処理)が必要なら、文字列の結合ではなくCSVパッケージを使用するとよい。

>>> ','.join(["2012-03-08", "Hana Senou", "Female"])
'2012-03-08,Hana Senou,Female'

文字列のメソッド

主なメソッドを示す。メソッドの一部にはstart,end引数により、文字列の指定範囲に対して実行できる。このstart,endの指定はスライス記法のstr[start:end]と等しい。

種別判定
メソッド概要
str.isalpha()文字列が1文字以上であり、すべての文字がユニコードにおいて「*Letter」として定義されているならTrue。
>>> "abcde".isalpha()
True
>>> "あいうえお".isalpha()
True
>>> "←".isalpha()
False

正確にはユニコードにおいて「Lm(Modifier Letter)」、「Lt(Titlecase Letter)」、「Lu(Uppercase Letter)」、「Ll(Lowercase Letter)」、「Lo(Other Letter)」に分類されるもの。
str.isascii()文字列が空または全てASCII文字の場合True。
>>> "abcde".isascii()
True
>>> "abcdeあ".isascii()
False
str.isdecimal()文字列が1文字以上であり、すべての文字がユニコードにおいて「Decimal Number」(10進数でつかわれる文字)であるならTrue。
>>> "123".isdecimal()
True
>>> "-123".isdecimal()
False
>>> "一二三".isdecimal()
False
>>> "١٢٣".isdecimal()
True
str.isdigit()文字列が1文字以上であり、すべての文字がユニコードにおいてNumeric Typeが「Decimal」または「Digit」であるならTrue。
>>> "①⒈❶".isdigit()
True
str.isnumeric()文字列が1文字以上であり、すべての文字がユニコードにおいてNumeric Typeが「Decimal」、「Digit」、「Numeric」であるならTrue。
str.isprintable()文字列が印字可能な文字であるか、空であればTrue。非印字可能文字はユニコードで「Other」または「Separator」と定義されているもののうち、半角空白を除いたもののこと。
str.isspace()文字列が1文字以上であり、すべての文字がユニコードにおいてNumeric Typeが「Decimal」、「Digit」、「Numeric」であるならTrue。
str.isidentifier()文字列がPythonで定義された有効な識別子であればTrue。
>>> "Hello".isidentifier()
True
>>> "!Hello".isidentifier()
False

Falseやclassのような予約語、キーワードであるかを確認するにはkeywordモジュールのiskeyword関数を使う。
str.isupper()
str.islower()
文字列に大文字/小文字の区別がある文字を1文字以上含み、かつそれらがすべて大文字(または小文字)であればTrue。
>>> "あいう".isupper()
False
>>> "あいうA".isupper()
True
検索
メソッド概要
"sub" in strstrが文字列subを含むならTrue。※メソッドではないがまとめて記載している
>>> "cde" in "abcde"
True
>>> "cb" in "abcde"
False
str.startswith(prefix[, start[, end]])
str.endswith(suffix[, start[, end]])
指定の文字列で始まる/終わる場合はTrue。
>>> "abcde".endswith("cde")
True
>>> "abcde".startswith("cde")
False
>>> "abcde".startswith("cde",2)
True
str.count(sub[, start[, end]])文字列subの出現回数を返す。一度一致した文字は再使用されない。
>>> "aaaaa".count("a")
5
>>> "aaaaa".count("aa")
2
>>> "aaaaa".count("aaa")
1
str.find(sub[, start[, end]])
str.rfind(sub[, start[, end]])
文字列subを検索し、位置を返す。見つからない場合は-1。str.findは最初にsubが出現した位置を返し、str.rfindは最後に出現した位置を返す。
>>> "abcabc".find("bc")
1
>>> "abcabc".rfind("bc")
4
>>> "abcabc".find("d")
-1
str.index(sub[, start[, end]])
str.rindex(sub[, start[, end]])
findと同じだが、見つからなかった場合にValueErrorが発生する。
置換・除去
メソッド概要
str.replace(old, new[, count])部分文字列oldをnewで置き換えた文字列を返す。
countが指定された場合、最初のcount個のみ置換する。
>>> "abcabc".replace("bc", "nd")
'andand'
>>> "abcabc".replace("bc", "nd", 1)
'andabc'
str.strip([chars])
str.rstrip([chars])
str.lstrip([chars])
文字列の最初と最後にある指定文字charsを取り除く。rstripは末尾のみ、lstripは最初のみ、stripは前後両方。charsは文字列ではなく、除外する文字のリストとして扱われる。charsの指定がない場合は空白文字(全半スペースや改行、タブなど)が取り除かれる。
>>> "\n abcde \t".strip()
'abcde'
>>> "\n abcde \t".lstrip()
'abcde \t'
>>> "\n abcde \t".rstrip()
'\n abcde'
>>> "abcdeabcdea".strip("abc") # これはaかbかcという意味
'deabcde'
str.removeprefix(prefix)
str.removesuffix(suffix)
文字列のプレフィックス、サフィックスを取り除いた文字列を返す。該当の文字を含まない場合はそのままの値が返される。
>>> "abcde".removeprefix("abc")
'de'
>>>"abcde".removesuffix("abc")
'abcde'
分割・結合
メソッド概要
str.split(sep=None, maxsplit=1)
str.rsplit(sep=None, maxsplit=1)
文字列を区切り文字sepで分割したリストにする。区切り文字が連続した場合、長さが0の文字列があるとみなされる。分割する最大回数はmaxplit回であり、リストの長さは最大でmaxsplit+1になる。
>>> ",1,2,3,,5,".split(",")
['', '1', '2', '3', '', '5', '']

sepが指定されないまたはNoneの場合は、挙動が異なる。区切り文字は空白文字が使われ、連続した空白文字は1つの区切りとみなされる。
>>> " 1 2 3 5 ".split()
['1', '2', '3', '5']
str.partition(sep)
str.rpartition(sep)
区切り文字sepで文字列を前後に分割する。結果は(区切り文字の前, 区切り文字, 区切り文字の後)のタプルとなる。
patitionは最初の区切り文字、rpartitionは最後の区切り文字で分割する。
>>> "1,2,3,,5,".partition(",")
('1', ',', '2,3,,5,')
>>> "1,2,3,,5,".partition(" ")
('1,2,3,,5,', '', '')
str.splitlines(keepends=False)文字列を改行で分割したリストを返す。改行は\nだけでなく\r(復帰)や\v(垂直タブ)なども含まれる。keependsがFalseの場合、分割したリストには改行文字は含まれない。
>>> "a\nb\r\vc\f".splitlines()
['a', 'b', '', 'c']

空行は空文字としてリストに含まれるが、「空文字」や「改行で終わる文字列の最後の行」はリストに含まれない。
>>> "".splitlines()
[]
>>> "a\n".splitlines()
['a']
str.join(iterable)iterableの文字列を結合する。文字列を区切るセパレータは自身(str側)となる。
>>> ",".join(["a","b","cde"])
'a,b,cde'
ケース
メソッド概要
str.upper()
str.lower()
文字列を大文字/小文字に統一した文字列を返す。
str.title()単語の1文字目だけを大文字、残りを小文字にする。
引用符を含む場合など、正しく動作しないケースが有る。
str.swapcase()大文字を小文字に、小文字を大文字にする。
パディング
メソッド概要
str.center(width[, fillchar])
str.ljust(width[, fillchar])
str.rjust(width[, fillchar])
文字列を幅widthで中央寄せ/左寄せ/右寄せにした文字列を返す。パディング文字にfillcharを使用する。省略時は半角空白。
>>> "1000".center(12,"-")
'----1000----'
>>> "1000".rjust(12,"-")
'--------1000'
>>> "1000".ljust(12,"-")
'1000--------'
str.zfill(width)文字列を幅widthで右寄せにした文字列を返す。パディング文字が0で、+-記号で始まる場合は1文字目が記号となるようにパディングされる。
>>> "123".zfill(5)
'00123'
>>> "-123".zfill(5)
'-0123'
"+あ".zfill(5)
'+000あ'

日時型 dateとdatetime

Pythonの日時に関係するものとして、date、datetime、time、timezoneがある。主な特徴を示す。

クラス特徴ライブラリ用途の例
date日付を表すdatetime現在の日付を取得
datetime日付と時刻を表すdatetime現在の日付と時刻を取得
timezoneタイムゾーンを表すdatetimeタイムゾーンの操作
time時間を表すtimeUNIX時間の取得、sleep

タイムゾーン

Python2ではタイムゾーンを扱うためにpytzライブラリを使用していたが、Python3からdatetimeが標準でタイムゾーンを取り扱うことができる。datetime.nowとdatetime.utcnowという2つの現在日時を取得するメソッドが用意されている。

>>> from datetime import datetime,date,timezone
>>> # ローカル時刻で現在日時を取得
>>> datetime.now()
datetime.datetime(2023, 7, 17, 22, 23, 49, 145537)
>>> date.today()
datetime.date(2023, 7, 17)
>>> # UTCで現在日時を取得
>>> datetime.utcnow()
datetime.datetime(2023, 7, 17, 13, 26, 31, 339851)
>>> datetime.utcnow().date()
datetime.date(2023, 7, 17)

datetime.nowは引数でタイムゾーンを明示的に指定して現時刻を取得できる。この場合、作成したdatetimeにもタイムゾーン情報が入っている。

>>> from datetime import datetime,timezone,timedelta
>>> # タイムゾーンを明示的に指定して現在日時を取得
>>> jst = timezone(timedelta(hours=+9), 'JST')
>>> datetime.now(jst)
datetime.datetime(2023, 7, 17, 22, 43, 33, 180461, tzinfo=datetime.timezone(datetime.timedelta(seconds=32400), 'JST'))
>>> datetime.now(jst).date()
datetime.date(2023, 7, 17)

指定時刻のオブジェクトを作成する

>>> from datetime import datetime,date
>>> datetime2 = datetime(2022,5,12,15,30,24,34567)
>>> datetime2
datetime.datetime(2022, 5, 12, 15, 30, 24, 34567)
>>> datetime2
datetime.datetime(2022, 5, 12, 15, 30, 24, 34567)
>>> date2 = date(2022,5,13)
>>> date2
datetime.date(2022, 5, 13)

年月日時分の一部だけを取り出す

>>> datetime1.year
2023
>>> datetime1.month
7
>>> datetime1.day
17
>>> datetime1.hour
22
>>> datetime1.minute
52
>>> datetime1.second
19
>>> datetime1.microsecond
535502
>>> datetime1.microsecond
535502
>>> date1.weekday() # 曜日は関数なので注意
0 # 月曜始まるで月曜が0、日曜は6
>>> date1.weekday() # 曜日は関数なので注意
1 # 月曜始まりで月曜が1、日曜は7

時間を操作する

date、datetimeはイミュータブルであり更新できないが、replaceメソッドで一部を置き換えた別のオブジェクトを取得できる。

>>> dt1 = datetime(2023,7,17,20,00)
>>> dt1.replace(year=2500,month=8,day=5,hour=0,minute=0)
datetime.datetime(2500, 8, 5, 0, 0)

時間の差

dateやdatetimeの差を求めるとtimedeltaオブジェクトが得られる

>>> dt1 = datetime(2023,7,17,20,00)
>>> dt2 = datetime(2023,7,17,21,30)
>>> dt2-dt1
datetime.timedelta(seconds=5400)
>>> dt1-dt2
datetime.timedelta(days=-1, seconds=81000)

逆にtimedeltaを使って時間を進めたり戻したりできる

>>> dt1 = datetime(2023,7,17,20,00)
>>> dt1 + timedelta(days=1) # dayではなくdaysであることに注意
datetime.datetime(2023, 7, 18, 20, 0)
relativedelta

timedeltaでは週数や日数を指定してオブジェクトを作成できますが、月や年単位では作成できない。timedeltaのコンストラクタは以下のように定義されている。

datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)

つまり、timedeltaは1年後のような指定ができない。代わりに日数で365日後と指定することはできるが、この方法は閏年が考慮されない。

>>> import datetime
>>> dt = datetime.datetime.now()
>>> dt
datetime.datetime(2023, 7, 22, 17, 57, 57, 462458) # 今日は7月22日
>>> dt + datetime.timedelta(days=365)
datetime.datetime(2024, 7, 21, 17, 57, 57, 462458) # 365日後は7月21日。閏年のため1日ずれている。

そのような場合はdateutilのrelativedeltaを使うと翌月や翌年の指定ができる。dateutilは追加インストール(pip install python-dateutil)が必要である。

>>> import datetime
>>> from dateutil.relativedelta import relativedelta # relativedeltaをインポート
>>> dt = datetime.datetime.now()
>>> dt
datetime.datetime(2023, 7, 22, 17, 57, 57, 462458) # 今日は7月22日
>>> dt + relativedelta(years=+1)
datetime.datetime(2024, 7, 22, 17, 57, 57, 462458) # 1年後は7月22日。閏年が考慮されている。

日時と文字列の変換

日時と文字列を変換するには、日時→文字列はstrftimeメソッド、文字列→日時はstrptimeメソッドを使う。

日時を文字列に変換する例
>>> # ISOフォーマット
>>> date1 = datetime.now(jst).date()
>>> datetime1 = datetime.now(jst)
>>> date1.isoformat()
'2023-07-17'
>>> datetime1.isoformat()
'2023-07-17T22:52:19.535502+09:00'
>>> date1.strftime('%Y-%m-%d %H-%M-%S.%f')
'2023-07-17 00-00-00.000000'
>>> datetime1.strftime('%Y-%m-%d %H-%M-%S.%f')
'2023-07-17 22-52-19.535502'
>>> datetime1.strftime('%Y-%m-%d(%a) %H-%M-%S.%f %Z')
'2023-07-17(Mon) 22-52-19.535502 JST'

strftimeで使用できるものの一覧はstrftime() and strptime() Format Codesにある。

文字列をdatetimeオブジェクトに変換する例
>>> datetime.strptime("2023-07-17", "%Y-%m-%d") # 任意のフォーマット
datetime.datetime(2023, 7, 17, 0, 0)
>>> datetime.fromisoformat("2022-05-12T15:30:24") # ISOフォーマット
datetime.datetime(2022, 5, 12, 15, 30, 24)

複数要素を保持できる型(コレクション)

Pythonでは複数要素を保持できる型をコレクションとよぶ。主なコレクションに配列(list)、タプル(tuple)、辞書(dictionary)、集合(set)がある。主な特徴は以下の通りである。

表記方法特定の要素へのアクセス方法特徴
配列角括弧
[a, b, c]
インデックスを使用
list[0]
最も標準的な順序付きコレクション。
タプル丸括弧
( a, b, c )
インデックスを使用
tuple[0]
イミュータブルな順序付きコレクション。
辞書波括弧とコロン
{"key1":value1, "key2":value2}
キーを使用
dict["key1"] (キー)
キーバリュー型の順序なしコレクション。
集合波括弧
{a, b, c}
順番やキーを持たないため不可重複しない集合演算可能なコレクション。

コレクションの共通な特徴は以下の通りである。

  • len()で要素数が取得できる
  • in演算子でコレクションの中にある要素が含まれるかどうかを判定できる

シーケンス

要素に順序があり、整数の順序番号(インデックス)で要素にアクセスできる特徴を持つコレクションをシーケンスという。配列、タプルはどちらもシーケンスである。

シーケンスのインデックスは0始まりである。また末尾から数えた順序番号を負数で指定できる。要素数10のシーケンスでインデックスの例を示す。

>>> sequence = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
>>> sequence[3]
'D'
>>> sequence[-1] # インデックスを-1にすると常に最後の要素を返す(要素がない時を除く)
'J'

存在しないインデックスを指定するとインデックスエラーが発生する。

>>> sequence[20]
IndexError: list index out of range
スライス

シーケンスは整数インデックスにより指定番目の要素へアクセスできるが、スライスを使うと指定された範囲にある要素を取り出せる。スライスは以下のようにコロンで範囲を指定する。

sequence[ 開始位置(start) : 終了条件(stop) ]

スライスで取り出されるのは取り出し開始位置から終了条件に達するまでとなり、終了条件に指定した要素は取り出されない。たとえば[3:5]と指定した場合、得られる要素はインデックスが3と4のものである。

>>> sequence[3:5]
['D', 'E']

インデックスはマイナスでもよいため、開始位置を3の代わりに-7、終了位置を5の代わりに-5としても問題ない。そのため以下4つのスライス指定はいずれも同じ結果になる。

sequence = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
sequence[3:5]
sequence[3:-5]
sequence[-7:-5]
sequence[-7:5]

スライスではインデックスを省略できる。開始位置を省略した場合は0、終了条件を省略した場合は要素数が指定されたと見なされる。

これを活用すると先頭n個(あるいは末尾n個)を取り出すことが簡単にできる

>>> sequence[:3]
['A', 'B', 'C']
>>> sequence[-3:]
['H', 'I', 'J']

なお、スライスでは範囲外のインデックスを指定してもエラーにならず、取り出せるものだけ取り出される。取り出せるものがない場合は空のシーケンスが得られる。

>>> sequence[5:20]
['F', 'G', 'H', 'I', 'J']
>>> sequence[10:20]
[]
拡張スライス

シーケンスの一部は拡張されたスライス(拡張スライス)も使用できる。拡張スライスでは取り出し間隔が指定できる。本記事では拡張スライスでは無いスライスのことを便宜上「標準のスライス」と呼ぶことがある。

sequence[ 取り出し開始位置(start) : 取り出し終了条件(stop) : 取り出し間隔(step) ]

拡張スライスは取り出し開始位置から間隔ごとに、終了条件に達するまで取り出していく。例えば、[0:5:2]とすると、下図のようにインデックスが0、2、4の要素が取り出される。

>>> sequence[0:5:2]
['A', 'C', 'E']

取り出し間隔をマイナスにすると逆順に取り出す。このとき取り出し位置も開始位置>終了位置と大小関係が逆転する。たとえばインデックスが2から4までを逆順で取り出す場合では、取り出し開始位置は4、取り出し終了条件を1とすると実現できる。

>>> sequence[4:1:-1]
['E', 'D', 'C']

開始位置や終了位置を省略した場合も正のときと位置関係が逆になる。(正確には開始位置が-1、終了位置は-(要素数+1)の位置になる)

逆順で先頭3個、末尾3個を取り出すときは以下のようになる。※インデックスの考え方を残すために、あえて計算を省略していない。

>>> sequence[(3-1)::-1]
['C', 'B', 'A']
>>> sequence[:-(3+1):-1]
['J', 'I', 'H']

しかし、直感的ではないので普通に取り出してから逆順に並び替えた方が良い。

>>> list(reversed(sequence[:3]))
['C', 'B', 'A']
スライスへの代入

スライス記法は値を取り出すだけでなく、代入先としても使用できる。代入によりシーケンスの長さが拡張・縮小されることがある。

標準のスライスに代入すると、その部分が置換される結果になる。

>>> a = [1, 2, 3, 4, 5]
>>> a[2:] = ["a", "b"]
>>> a
[1, 2, 'a', 'b']
>>> a = [1, 2, 3, 4, 5]
>>> a[:2] = ["a","b","c","d","e"]
>>> a
['a', 'b', 'c', 'd', 'e', 3, 4, 5]
>>> a = [1, 2, 3, 4, 5]
>>> a[2:4] = ["a","b","c","d","e"]
>>> a
[1, 2, 'a', 'b', 'c', 'd', 'e', 5]

長さが0になる標準のスライスでは、そこに挿入される。

>>> a = [ 1, 2, 3, 4, 5 ]
>>> a[1:1]
[]
>>> a[1:1] = [ "a","b","c","d","e" ]
>>> a
[1, 'a', 'b', 'c', 'd', 'e', 2, 3, 4, 5]
# レンジ外のスライス指定での挿入例
>>> a = [ 1, 2, 3, 4, 5 ]
>>> a[100:200] # レンジ外のスライス指定も長さが0になるので
[]
>>> a[100:200] = ['a', 'b', 'c'] # こうすると末尾への挿入になる
>>> a
[1, 2, 3, 4, 5, 'a', 'b', 'c']
拡張スライスへの代入

拡張スライスを代入先として指定すると、各要素を置き換える動作となる。

>>> a = [ 1, 2, 3, 4, 5 ]
>>> a[::2]
[1, 3, 5] # 要素数が3の拡張スライス
>>> a[::2] = ['a', 'b', 'c'] # 要素数が同じ3のリストを代入
>>> a
['a', 2, 'b', 4, 'c']

拡張スライスの要素数と代入するコレクションの要素数が一致しないとエラーになる。

>>> a = [ 1, 2, 3, 4, 5 ]
>>> a[::2]
[1, 3, 5] # 要素数が3の拡張スライス
>>> a[::2] = ['a', 'b', 'c', 'd', 'e'] # 要素数5を代入
ValueError: attempt to assign sequence of size 5 to extended slice of size 3
# ValueError:サイズが3の拡張スライスにサイズが5のシーケンスを割り当てようとした

仮に要素数が0であっても拡張スライスであれば挿入では無くエラーになる。

>>> a = [ 1, 2, 3, 4, 5 ]
>>> a[2:2:-1]
[]
>>> a[2:2:-1] = ['a', 'b', 'c', 'd', 'e']
ValueError: attempt to assign sequence of size 5 to extended slice of size 0

ただし、拡張スライス記法であっても取り出し間隔が1であれば標準的なスライスと同じように挿入や置換が行われる。

>>> a = [ 1, 2, 3, 4, 5 ]
>>> a[2:2:-1]
[]
>>> a[2:2:-1] = ['a', 'b', 'c', 'd', 'e']
ValueError: attempt to assign sequence of size 5 to extended slice of size 0
>>> a[2:2:1]
[]
>>> a[2:2:1] = ['a', 'b', 'c', 'd', 'e'] 
>>> a
[1, 2, 'a', 'b', 'c', 'd', 'e', 3, 4, 5]

マッピング

要素に順序がなく、一意のキーで対応する要素にアクセスできるコレクションをマッピングという。キーバリュー型、キー付きコレクションなどとも呼ばれることがある。

辞書はマッピングであり、キーが商品名、バリューが在庫数である辞書stockは以下のように作成できる。要素にアクセスするときは [キー名] を付ける。

>>> stock = {"Apple":100, "Bread":200}
>>> stock["Apple"]
100

キーは文字列で無くてもよい。ただ辞書はハッシュ値を使用してキーを管理しているため、ハッシュ可能でなければならない。例としてハッシュ不可能であるリストをキーにした辞書を作成してみた。他にハッシュ不可能なものとしては辞書やセットなどがある。

>>> unhashable_list = [1, 2, 3, 4]
>>> dict1 = { unhashable_list:100 }
TypeError: unhashable type: 'list'

独自に作成したクラスは基本的にはハッシュ可能となっているが、使い方によっては予期せぬ動作をするかもしれない。

キーの存在確認

存在しないキーにアクセスするとKeyErrorとなる。

>>> dict = {} # dictは空の辞書
>>> dict["key"] # 存在しないkeyにアクセス
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'key'

ディクショナリにとあるキーが存在するかはin演算子でテストできる。

>>> "key" in dict
False

複数のキーがあるかを一括で確認したいならdict.keysメソッドを使用できる。dict.keys()は辞書が持つキー一覧を取得するメソッドであるが、このキー一覧はSetのようなオブジェクト(set-like object)になっており、不等号で包含関係を比較できる。つまり>、>=、<、<=は数学の集合論でいう⊃、⊇、⊂、⊆にそれぞれ相当する。

dicta = { "a":1, "b":2, "c":3 }

isinstance(dict.keys(), collections.abc.Set) # True

dicta.keys() >= {"a","b"}     # True
dicta.keys() >= {"a","b","d"} # False

なお、辞書をsetに変換しても同じ結果が得られる。

dicta = { "a":1, "b":2, "c":3 }
set(dicta) # これは{'b', 'c', 'a'}のような順不問のキーの集合になる
存在しないキーへのアクセス

もし存在しないキーでもエラーにしたくないならget()を使う。getメソッドでは第2引数でキーが存在しなかった場合の値を指定でき、デフォルトではNoneになる。

>>> dict = {} # dictは空の辞書
>>> dict.get("key") # キーが存在しない場合はNoneが返されるため、何も表示されない
>>> dict.get("key", 0) # キーが存在しない場合は0を返すように指定
0
defaultdict

ケースによってはdefaultdictのほうが使いやすいことがある。defaultdictは存在しないキーを参照するときにデフォルト値を返すため、つねにgetでアクセスしているかのように使うことができる。例として空のリストを初期値にもつdefaultdictは以下のように実装できる。

>>> from collections import defaultdict # defaultdictは標準型ではないためインポート必要
>>> defdict = defaultdict(lambda:0) # 引数で初期化する関数を指定、常に0を返すlambda関数を渡している。
>>> defdict["key"] # defaultdictで存在しないkeyにアクセス
0
使用例

重複ありリストで要素の出現回数をカウントするプログラムを考えてみる。普通の辞書を使うと以下のように書けるだろう。

target = [0,3,5,6,3,2,4,7,1,5]

counter = dict()
for no in target:
    counter[no] = counter.get(no,0) + 1

これはcounter辞書が要素の出現回数を記録する変数であり、forループの中で辞書の値を1ずつ増やしている。初めて出現した要素だと辞書にキーが無いためgetを使ってこれまでの出現回数を取得しているというわけである。続いてdefaultdictを使うとgetを使わなくてよいので、累算代入(加算代入)を使用して以下のように書くことができる。

from collections import defaultdict

target = [0,3,5,6,3,2,4,7,1,5]

counter = defaultdict(lambda:0)
for no in target:
    counter[no] += 1
値の取り出し

dictはkeys、values、itemsメソッドで、それぞれキーの一覧、バリューの一覧、キーとバリューの一覧を取り出せる。

>>> stock = {"Apple":100, "Bread":200}
>>> stock.keys()
dict_keys(['Apple', 'Bread'])
>>> stock.values()
dict_values([100, 200])
>>> stock.items()
dict_items([('Apple', 100), ('Bread', 200)])
dict.keys()キーのリストを取得
dict.values()値のリストを取得
dict.items()キーと値の組み合わせ(タプル)のリストを取得

セット

セット(集合)は、順序がなく、要素が重複しないコレクションである。集合演算を行いたいときや重複除外したいときに使うことができる。

>>> set = { "Apple", "Bread", "Apple", "Apple" }
>>> set
{'Apple', 'Bread'}

この例を見ると重複している要素"Apple"が1つにまとめられていることが分かる。

集合演算

セットでは和集合や積集合を求められる。A = {0,1,2,3,4,5,6,7,8,9,10}B = {2,3,5,7,11,13,17,19}のとき、以下のような結果が得られる。

和集合(AかBに含まれる)A | B{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 17, 19}
差集合(Aにだけある)A - B{0, 1, 4, 6, 8, 9, 10}
積集合(AとBの両方にある)A & B{2, 3, 5, 7}
対象差集合(AかBのどちらか一方だけにある)A ^ B{0, 1, 4, 6, 8, 9, 10, 11, 13, 17, 19}

コレクションの比較

大小判定(>、>=、<、<=)
シーケンス

シーケンス同士の大小比較は、最初の要素から要素ごとに順番に比較される。

>>> list1 = [ 'h', 'e', 'l', 'l', 'o' ]
>>> list2 = [ 'h', 'e', 'm', 'l', 'o' ]
>>> list1 < list2
True

この例ではlist1[0] == list2[0], list1[1] == list2[1], list1[2] < list2[2]と順に比較していき、結果はlist1 < list2となる。

要素数の異なるシーケンスの比較では、まず要素ごとの比較が行われ、どちらかの終端に達した時点で違いがない場合は、要素数が少ない方が小さいとされる。

>>> list1 = [ 10, 20, 30]
>>> list2 = [ 10, 20, 20, 40]
>>>
>>> list1 < list2
False # 3番目の要素がlist2の方が小さい

>>> list1 = [ 10, 20, 30]
>>> list2 = [ 10, 20, 30, 40]
>>>
>>> list1 < list2
True # 3番目の要素までは同じだが、list1の方が要素数が少ない
辞書

辞書は大小比較をサポートしない。

集合

集合は大小比較は包含判定として動作する。

>>> set1 = {1, 2, 3, 4, 5}
>>> set2 = {5, 4, 3, 2, 1}
>>> set3 = {1, 5, 3}
>>> set4 = {1, 2, 8}
>>>
>>> set1 >= set2
True
>>> set1 > set3
True
>>> set1 > set4
False
一致判定

==演算子でコレクションの比較ができる。シーケンス(順序付きコレクション)である場合、順序も一致している必要がある。

>>> # リストでの比較
>>> list1 = [10,20,30]
>>> list2 = [10,20,30]
>>> list3 = [30,20,10] # 中身は同じだが順序が異なる
>>> list1 == list2
True
>>> list1 == list3
False
>>> #辞書型は順序を持たないためキーバリューの組み合わせがすべて一致していれば良い。
>>> dict1 = { "key1":10, "key2":20 }
>>> dict2 = { "key2":20, "key1":10 } # 見た目上の順序は異なる
>>> dict1 == dict2
True

もしシーケンスで順序不問で一致判定をしたいなら、setを使うことを考えてみても良い。setは集合オブジェクトであり、要素の順序と重複した要素が無視される。

>>> list1 = [10,20,30]
>>> list2 = [30,20,10]
>>> set1 = set(list1)
>>> set2 = set(list2)
>>> set1 == set2
True

ただしsetは重複除外もしているため同じ値が異なる回数出現するシーケンスでも一致する。これが好ましくない動作ならDeepDiffライブラリなどを使用するか、シーケンスをソートしてから比較するのもよいだろう。

>>> list3 = [10,20,30,30,20,10]
>>> set3 = set(list3)
>>> set3
{10, 20, 30}
>>> set1 == set3
True
>>> list1 = [ 10, 20, 30]
>>> list2 = [ 30, 20, 10]
>>> list1 == list2
False
>>> sorted(list1) == sorted(list2)
True

イテレータ

コレクションの中にはイテレータと呼ばれるものを使って要素に順次アクセスできるものがある。そのようなコレクションはiterableであるという。イテレータを使う例を示す。

>>> collection = [1,2,3]
>>> iterator = iter(collection)
>>> next(iterator)
1
>>> next(iterator)
2
>>> next(iterator)
3
>>> next(iterator)
StopIteration
>>>

組み込み関数iterとnextを使用している。iterableなコレクションに対してiterを実行するとイテレータが得られる。そのイテレータに対してnextを実行すると1つずつ要素を取り出すことが出来るという仕組みである。

iterableなコレクションはfor … inをつかって要素を順に処理できる。

for item in iterable_collection:
    print(item)

配列、タプル、辞書、セットはいずれもiterableである。例としてタプルで要素を処理する例を示す。

>>> item_tuple = ("Apple", "Bread")
>>> for item in item_tuple:
...     print(item)
...
Apple
Bread

配列とタプルでは、コレクションの要素が順にitemに入り処理されるが、辞書のようなマッピングではキーの値が順にitemに入ることになる。もし辞書でバリューだけを取り出して順次処理をしたい場合は、辞書のvaluesメソッドでバリューのコレクションを取得すれば良い。

>>> stock_dict
{'Apple': 100, 'Bread': 200}

>>> for item in stock_dict:
...     print(f"{item=}")
...     print(f"{stock_dict[item]=}")
...
item='Apple'
stock_dict[item]=100
item='Bread'
stock_dict[item]=200

>>> for stock in stock_dict.values():
...     print(stock)
...
100
200

ここでイテレータおよびiterableの動作についてもう少し補足するために、組み込み関数iterとnextを使わずにイテレータを使うコードを紹介する。

>>> collection = [1,2,3]
>>> iterator = collection.__iter__()
>>> iterator.__next__()
1
>>> iterator.__next__()
2
>>> iterator.__next__()
3
>>> iterator.__next__()
StopIteration

イテラブルなコレクションは__iter__メソッドを実装している。このメソッドはコレクションの要素に順次アクセスできるイテレータを返す。このイテレータは__next__メソッドを実装している。このメソッドを呼び出すと次の要素を返すか、終端に達したことを示すStopIteration例外を送出する。for ... inの構文はイテレータというものを深く意識すること無く要素に順次アクセスする処理を実現するものと考えれば良いだろう。

コレクションの長さ

コレクションの要素数は組み込み関数lenで得られる。

>>> len(dict1)
3

enumerate

繰り返しのときに要素だけでなく順序番号も取得できるenumerate関数が用意されている。enumerateはインデックスと値の組み合わせをタプルで返す。

for (index,value) in enumerate(collection):
    print(f"collection[{index}]={value}") 

タプルの()は省略できるため、実際には以下のような表記がされることが多い。

for index,value in enumerate(collection):

辞書型に対してenumerateを使用した場合、キーとキーの順序番号が得られる。なお、辞書型はPython 3.7からキーの順序が保証されるようになった。

>>> dict = {"key1":100, "key2":200}
>>> for i,v in enumerate(dict):
...     print(f"{i=},{v=}")
i=0,v='key1' # Python3.7からは常にkey1が0番目、key22が1番目である
i=1,v='key2'

enumerateは第二引数で開始番号を指定できる。Pythonではシーケンスは0からインデックスが始まる(ゼロベースのインデックス)が、人が見る場合は先頭が1番のほうが自然である。そのような場合に1から開始するように指定できる。

>>> for i,v in enumerate(list, 1):
...     print( f"{i=}, {v=}" )
i=1, v='h'
i=2, v='e'
i=3, v='l'
i=4, v='l'
i=5, v='o'

ただし開始番号を指定した場合、オブジェクトの本当のインデックスとenumerateが返す順序番号がずれるため、その値をインデックスとして使わないように注意すること。

>>> for i,v in enumerate(list, 1):
...     print( f"{i=}, {v=}, {list[i]=}" )
i=1, v='h', list[i]='e'
i=2, v='e', list[i]='l'
i=3, v='l', list[i]='l'
i=4, v='l', list[i]='o'
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>;
IndexError: string index out of range
# list[5]へのアクセスが発生しインデックスエラーとなる

なお、正確にはenumerate関数はenumerateオブジェクトを返す。このenumerateオブジェクトは後述するgenerator iteratorに等しい。番号と値のタプルのリストを取得したいならlist関数と組み合わせる。

>>> enumerate(["a","b"],0)
<enumerate object at 0x0000020A85D79380>
>>> list(enumerate(["a","b"],0))
[(0, 'a'), (1, 'b')]

要素の追加と削除

要素の追加要素を指定して削除指定位置の要素の削除(取り出し)すべて削除
listlist1.append( newvalue )list1.remove( newvalue )*1*2list1.pop( index )*3*4*5list1.clear()
tuple不可(イミュータブル)不可(イミュータブル)不可(イミュータブル)不可(イミュータブル)
dictdict1.[ newkey ] = newvalue*6リスト内包表記を使用dict1.pop( key )*3*4*5dict1.clear()

*1 存在しない値を与えるとValueErrorになる
*2 複数に一致する場合は、最初に一致したものだけが削除される
*3 popは、取り出すメソッドであり、取り出した要素を返す
*4 存在しないインデックスを与えるとIndexErrorになる  
*5 負数の値を指定して、末尾からの位置も指定できる。-1が最後尾である。  
*6 キーが既に存在される場合は上書きされる。setdefaultメソッドを使うと、キーが存在する場合は上書きしない処理ができる。dict1.setdefault("key", value)

dictにはpopitem()もあり、LIFOで要素を取り出すこともできる。(Python 3.7以降)

なお追加したい要素がコレクションである場合、コレクションが1つの要素として追加されることに注意すること。コレクションを連結させたいなら下表の方法を使う。

>>> list1 = [1,2,3]; list2 = [4,5,6]
>>> list1.append(list2)
>>> list1
[1, 2, 3, [4, 5, 6]]
>>> list1[3]
[4, 5, 6]
既存のコレクションに追加別のコレクションとして作成
listlist1.extend(list2)list3 = list1 + list2
tuple不可(イミュータブル)tuple3 = tuple1 + tuple2
dictdict1.update(dict2)dict3 = dict1 | dict2

リストを連結する場合、以下のようになる。

>>> list1 = [1,2,3]; list2 = [4,5,6]
>>> list1.extend(list2)
>>> list1
[1, 2, 3, 4, 5, 6]

なお辞書の連結においては重複したキーの値がある場合は上書きされる。そのため正確には連結ではなく更新(メソッド名もupdate)であり、dict1.update(dict2)とdict2.update(dict1)では異なる値になる可能性がある。

>>> dict1 = {1:10,2:20}
>>> dict2 = {1:100,2:200}
>>> print( dict1 | dict2 )
{1: 100, 2: 200}
>>> print( dict2 | dict1 )
{1: 10, 2: 20}

内包表記(List Comprehensions)

内包表記はリストやディクショナリをfor構文を使用して作成する表記法である。元となるiterableなコレクションがあり、その各要素に対して何らかの処理を行った結果のリストやディクショナリを作成できる。ただしタプルは内包表記では生成できない。タプルのこの表記は別の用途(ジェネレータ式)で定義されている。

# リスト
[ expression for variable_name in itarable ]
# ディクショナリ
{ key_expression:value_expression for variable_name in itarable }
# セット
{ expression for variable_name in itarable }

1~10までの数値のコレクションをもとに、キーが数値、バリューがキーの2乗値となる辞書を作成する例を示す。

>>> datarange = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> { x:x**2 for x in datarange }
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}
フィルター

内包表記ではifを使って条件を満たしたイテラブルの要素だけを使用できる

[ rule_expression for variable_name in itarable if condition_for_iterable ]

1~20の整数の中で3の倍数のリストは、以下の表記で作成できる。

>>> datarange = list(range(1,21))
>>> datarange
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
>>> [ x for x in datarange if x%3==0 ]
[3, 6, 9, 12, 15, 18]
複数コレクションの内包表記

1つの内包表記にfor...inやifを2個以上書くことができる。以下の2つはどちらもエラーなく実行可能なPythonコードである。

[ x**2 for x in (1,2,3,4,5,6,7,8,9,10) if x > 5 if x%2==0 ]

[ (x,y,z) for x in (1,2) for y in (1,2) for z in (1,2) if not (x == y and y == z) ]
[36, 64, 100]
[(1, 1, 2), (1, 2, 1), (1, 2, 2), (2, 1, 1), (2, 1, 2), (2, 2, 1)]

1つめのリスト内包表記なら、以下のように展開されると考えればよい。

array = []
for x in (1,2,3,4,5,6,7,8,9,10):
    if x > 5:
        if x%2 == 0:
            array.append(x)

forを複数個書く場合は順序に意味があることに注意する。以下にforの順序を入れ替えた場合に現れる際を例示する。

>>> [ (x,y) for x in (1,2) for y in (3,4) ]
[(1, 3), (1, 4), (2, 3), (2, 4)]
>>> [ (x,y) for y in (3,4) for x in (1,2) ]
[(1, 3), (2, 3), (1, 4), (2, 4)]

リストに含まれる値の順序が異なっていることがわかる。これはforループにどう展開されるか考えればわかる。

>>> for x in (1,2):
...     for y in (3,4):
...         print(x, y)
...
1 3
1 4
2 3
2 4
>>> for y in (3,4):
...     for x in (1,2):
...         print(x, y)
...
1 3
2 3
1 4
2 4

コレクション向けの便利な関数

コレクションで使える便利な関数を紹介する。なお、関数によってiterableやシーケンスなどコレクションが限定されていることに注意すること。

sumイテラブルの要素の合計値を求める。
minイテラブルの要素の中の最小のものを得る。
maxイテラブルの要素の中で最大のものを得る。
filterイテラブルの要素の中で条件を満たす要素だけを取り出す。
mapイテラブルの全要素に処理を適用したものを得る。
reversedシーケンスの要素を逆順に取り出すイテレータを得る。
sortedイテラブルの要素を整列させたものを得る。
zip複数のイテラブルから一つずつ要素を取り出す。
allイテラブルの要素が全てTrueか調べる。
anyイテラブルの要素にTrueが1つでもあるか調べる。
sum

イテラブルなコレクションの合計値を返す。

sum(iterable)
sum(iterable, /, start=0) # startは開始時の値。startとコレクションの値の合計値となる。

startの利用例を示す。コレクションの合計値は60であり、startの値がそれに加算される形となる。

>>> datas = [10, 20, 30]
>>> sum(datas)
60
>>> sum(datas, 100)
160
min

イテラブルなコレクションあるいは引数のリストから最小のものを返す。keyで大小判定に使用する値を指定できる。keyの使用例は次のmaxでまとめて記載する。

min(iterable, key=None)          # iterableは空であってはいけない
min(iterable, default, key=None) # こちらはiterableが空でもOK。defaultはiterableが空だったときの値
min(arg1, arg2, *args, key=None) # これはiterableではなく引数で指定する
max

イテラブルなコレクションあるいは引数のリストから最大のものを返す。keyで大小判定に使用する値を指定できる。

max(iterable, key=None)          # iterableは空であってはいけない
max(iterable, default, key=None) # こちらはiterableが空でもOK。defaultはiterableが空だったときの値
max(arg1, arg2, *args, key=None) # これはiterableではなく引数で指定する

keyの使用例を示す。keyで指定した関数が返した値に基づいて大小判定が行われている。

datas = [ (10,20), (10,25), (40,10), (30,30) ]

print( min(datas) )
print( min(datas, key=lambda x:x[1]) )
print( max(datas, key=lambda x:x[0]+x[1]))
(10, 20)
(40, 10)
(30, 30)
filter

イテラブルなコレクションに対して条件を満たす要素だけを取り出すfilter関数が用意されている。

filter( function, iterable )

filter関数はfunctionが真となる要素だけを取り出す。奇数だけを取り出すフィルターは、ラムダ式を用いて以下のように書ける。filter関数はiterableであるfilterオブジェクトを返す。filterオブジェクトはiterableであり、リストとして使うためにはlist関数を使う。

>>> list1 = [ 1, 6, 3, 10, 5, 9]
>>> filter(lambda x:x%2, list1)
<filter object at 0x000002AE3E2B6080>
>>> list(filter(lambda x: x%2, list1))
[1, 3, 5, 9]

このfilter関数は内包表記やジェネレータ式を使用して以下のようにかける。ラムダ式を使うfilterよりもこの方が軽量であるとされる。

>>> list2 = [x for x in list1 if x%2]
>>> list2
[1, 3, 5, 9]
>>> list_iter = (x for x in list1 if x%2)
>>> next(list_iter)
1
>>> next(list_iter)
3
>>> next(list_iter)
5
>>> next(list_iter)
9
>>> next(list_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
map

イテラブルなコレクションの全要素に一括で処理を適用できるmap関数が用意されている。

map( callable, iterable)

絶対値に変換する例を示す。mapもfilterと同じくiterableであるmapオブジェクトを返すため、リストとして使うためにはlist関数を使う。

>>> lista = [ -5, 10, 30]
>>> list( map( abs, lista))
[5, 10, 30]

このmap関数は内包表記やジェネレータ式を使用して以下のように書ける。

>>> lista = [ -5, 10, 30]
>>> [abs(x) for x in lista]
[5, 10, 30]
reversed

シーケンスを逆順に取り出すイテレータ(reverse iterator)を返す。

reversed( sequence )

使用例を示す。

>>> datas = [10, 20, 30]
>>> reversed(datas)
<list_reverseiterator object at 0x000002B5A9925630>
>>> for x in reversed(datas):
...     print(x)
...
30
20
10

この関数は順序がない(シーケンスでは無い)コレクションでは使用できない。

>>> valueset = {1, 5, 3}
>>> reversed(valueset)
TypeError: 'set' object is not reversible
sorted

イテラブルなコレクションをソートしたリストを返す。元のコレクションは変更されない。

>>> lista = [ -30, 10, 5]
>>> sorted( lista )
[-30, 5, 10]

key引数でソートに使用するキーを、reverse引数で逆順(降順)にするかを指定できる。これは絶対値が降順となるようにソートする例である。

>>> lista = [ -30, 10, 5]
>>> sorted(tuplea, key=lambda x:abs(x), reverse=True )
[-30, 10, 5]
zip

複数のイテラブルなコレクションから値を一つずつ取り出したタプルを作成する。mapやfilterと同じくiterableなzipオブジェクトを返すため、リストとして使うためにはlist関数を使う。

>>> iterable1 = (1,2,3)
>>> iterable2 = ["M","F","M"]
>>> iterable3 = ["Augustine","Onslow","Basil"]

>>> list(zip(iterable1,iterable2,iterable3))
[(1, 'M', 'Augustine'), (2, 'F', 'Onslow'), (3, 'M', 'Basil')]

コレクションの要素数が異なる場合、デフォルトでは最も要素数が少ないコレクションにあわせてタプルが作成される。strict引数をTrueにするとコレクションの長さが異なる場合にValueErrorが送出される。

>>> iterable1 = (1,2,3)
>>> iterable2 = ("A")

>>> list(zip(iterable1, iterable2))
[(1, 'A')]

>>> list(zip(iterable1, iterable2, strict=True))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: zip() argument 2 is shorter than argument 1

要素数が長いコレクションに合わせてタプルを作成するためにはitertoolsライブラリのzip_longest(*iterables, fillvalue=None)が使用できる。

all

コレクションの要素が全てTrueに相当するときにTrueとなる。

>>> dataset1 = [1, -5, 10, 2]
>>> dataset2 = [6,  3,  0, 9]
>>> all(dataset1)
True
>>> all(dataset2)
False

dataset2にはFalseに相当する0が入っているため、Falseが返される。

リスト内包表記と併用すれば、全ての要素が何らかの条件を満たすか検査するときに使用できる。たとえば、全ての要素が0以上かどうか検査する例を示す。

>>> all([x >= 0 for x in dataset1])
False
>>> all([x >= 0 for x in dataset2])
True
any

コレクションの要素が1つでもTrueに相当するものがあるときにTrueとなる。

>>> dataset1 = [1, -5, 10, 2]
>>> dataset2 = [6,  3,  0, 9]
>>> any(dataset1)
True
>>> any(dataset2)
True

allと同じようにして、何らかの条件を満たす要素が1つでもあるか検査できる。0未満の要素があるかどうか検査する例を示す。

>>> any([x < 0 for x in dataset1])
True
>>> any([x < 0 for x in dataset2])
False

ジェネレータ

ジェネレータはイテレータを作成する方法の一つである。以下のようなyield文を含む関数をgenerator関数と呼ぶ。

def generator_function1():
    yield 1
    yield 2
    yield 3

この関数を実行すると、戻り値としてgeneratorオブジェクトが得られる。

gen_iter = generator_function1()
print(gen_iter)
<generator object generator at 0x000001D52C5549E0>

このgeneratorオブジェクトはgenerator iteratorと呼ばれるイテレータの一種であり、for文などで使用できる。以下に例を示す。

for x in gen_iter:
    print(x)
1
2
3

これを見るとgenerator iteratorはジェネレータ関数のyieldの後の値を順番に返すイテレータであることが分かる。

generator iteratorに対してnext()関数を実行すると、yield文に到達するまでの処理が実行される。yield文に到達すると処理は一時中断され、yieldに続く式の値を返す。再びnext()関数を実行すると中断時点から処理が再開され、また次のyield文に到達するまでの処理が実行される。もしyield文に到達しないで関数が終了した場合は、StopIteration例外が発生し終端に到達した扱いとなる。

>>> gen_iter = generator_function1()
>>> next(gen_iter)
1
>>> next(gen_iter)
2
>>> next(gen_iter)
3
>>> next(gen_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Pythonのドキュメント等でコレクションの値を返すことが期待される関数で、戻り値がジェネレータであると書かれている場合、それはgenerator iteratorのことである。多くの場合ではイテラブルなオブジェクトであるとだけ理解しておけば実装上の不都合はないだろう。

例としてA_n=1,2,3,4,5,…と無限に続く数列を考えてみる。無限に続くため当然ながらリストで表すことはできないが、ジェネレータでは以下のように簡単に実現できる。

def generator_function2():
    a = 1
    while True:
        yield a
        a += 1


def generator_sample2():
    for i in generator_function2():
        print(i)
        if i >= 100: break # 無限に続くためループ側で終了するようにしておく


generator_sample2()
1
2
3
4
(省略)
100

この例はジェネレータの特徴をよく表している。リストなどのイテレータはすでにあるコレクションから要素を順番にとりだすのに対し、ジェネレータは次の要素が必要になったときに要素を生成できる。そのため無限長の配列を作ることができる、メモリが少なくてすむなどの特徴がある。

generator iteratorの独自メソッド

generator iteratorは単純なイテレータとしてだけでなく、固有のsend、close、throwメソッドが使用できる。

send

sendメソッドではgenerator iteratorへ値を渡して、その値に基づいた次の値を生成させることができる。

def generator_function3():
    sum = 0
    while True:
        sent = yield sum
        if isinstance(sent, int):
            sum += int(sent)
        else:
            try:
                intvalue = int(sent)
                sum += intvalue
            except ValueError:
                pass


def generator_sample3():
    gen_iterator = generator_function3()
    sendvalue = None
    try:
        while True:
            x = gen_iterator.send(sendvalue)
            print(x)
            sendvalue = input("number:")
    except StopIteration:
        pass
    except EOFError:
        pass
    finally:
        gen_iterator.close(


generator_sample3()

このプログラムは数値の総計を返すジェネレータである。ジェネレータに値を渡すにはfor...inやnextを使う代わりにsendメソッドを使う。generator iteratorの初回呼び出し、つまりyieldで一時中断になっていない状態では値を受け取ることができないため、send(None)としなければならない。

ポイントは4行目と20行目である。4行目のジェネレータ側ではyieldで一度生成した値を返した後、次に生成するときに値を受け取られるようにしている。

        sent = yield sum

そして20行目のジェネレータを使っている側ではsendメソッドで値を渡している。

            x = gen_iterator.send(sendvalue)

なお、nextを呼び出したときはNoneが渡された物として処理される。nextとsendを混ぜて使用しても問題ない。

throw

throwでジェネレータ側で例外を発生させることが出来る。ジェネレータが一時中断しているところ(通常はyieldの場所)で例外が発生した扱いとなる。ジェネレータ側で例外がキャッチされなかったり、例外処理中に別の例外が発生した場合は、throwの呼び出し側に戻って例外処理が行われる。

また、throwメソッドもnextと同様に次の値を生成してyieldで返すことが期待される。値を返すことなくジェネレータ側の処理が終了した場合はStopIteration例外が発生し終端に到達した扱いとなる。

close

closeメソッドを呼び出すと、ジェネレータが一時停止しているところ(通常はyieldの場所)でGeneratorExit例外が発生した扱いとなる。ジェネレータ側では、この例外をキャッチしクリーンアップ処理を行うことができる。

以下の場合、closeメソッドは正常に呼び出し元の処理に戻る。

  • ジェネレータがこの例外を正しくキャッチし正常にクリーンアップが終了した場合
  • ジェネレータがすでに終了していた場合
  • GeneratorExit例外が発生した場合(発生した例外が処理されなかった場合も含む)

このcloseメソッドを呼び出したにも関わらずyieldで値が返された場合はRuntimeError例外が発生する。また、GeneratorExit以外の例外が発生した場合は普通のメソッドと同じく呼び出し元に通知される。

サブジェネレータ

ジェネレータにおいて、リストなどのイテラブルな値をyieldで一つずつ返すときはyield fromが利用できる。これをサブジェネレータとよぶ。

def generator_sample4():
    yield from (1,2,3)

この例のgenerator_sample4は以下のgenerator_sample5と等価である。

def generator_sample5():
    for num in (1,2,3):
        yield num

ジェネレータ式

ジェネレータ式は簡易的にgenerator iteratorを作成する記法である。リスト内包表記と同じ表記方法を用いるが、()で囲む。

>>> gen = (x for x in range(1, 4))
>>> type(gen)
<class 'generator'>
>>> for i in gen:
...     print(i)
...
1
2
3

リスト内包表記と同様に複数のfor...inを記載できる。

>>> gen_iter = ( (x,y) for x in (1,2) for y in (3,4) )
>>> next(gen_iter)
(1, 3)
>>> next(gen_iter)
(1, 4)
>>> next(gen_iter)
(2, 3)
>>> next(gen_iter)
(2, 4)
>>> next(gen_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

列挙型

列挙型の基本

列挙型は「関連する定数を集めたり」「ユニークな値と関連付けられる名前を管理する」ときに使うものである。Pythonではenumモジュールで提供される。

色名を管理する列挙型の例を示す。列挙型はクラス方式とファンクション方式の2つの方法で作成できる。

from enum import Enum

# クラス方式
class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

# ファンクション方式
Color = Enum('Color', ['RED', 'GREEN', 'BLUE'])

「Color.RED」のようにアクセスしたり、対応する数値から名前を取り出すことができる。

>>> Color = Enum('Color', ['RED', 'GREEN', 'BLUE'])
>>>
>>> Color.RED
<Color.RED: 1>
>>> Color(1)
<Color.RED: 1>
>>> member = Color.RED
>>> member.name
'RED'
>>> member.value
1
>>> Color(2).name
'GREEN'

値の重複

Enumでは名前は重複できないが、値は重複しても良い。値が重複している場合、あとから定義されたものは先に定義された値の別名として扱われる。以下の例では1という値を持つ2つの名前:Color.REDとColor.KURENAIを定義している。Color.KURENAIを参照すると実体はColor.REDであることが分かる。

>>> class Color(Enum):
...     RED = 1
...     KURENAI = 1
...     BLUE = 2
...
>>> Color(1)
<Color.RED: 1>
>>> Color.KURENAI
<Color.RED: 1>

なお、Enumをイテレータで処理するときに別名は取り出されない。もし、別名も含めたすべての名前を取得したいならEnum.__members__を参照すると良い。

>>> for c in Color:
...     print(c)
...
Color.RED
Color.BLUE

>>> for c in Color.__members__.items():
...     print(c)
...
('RED', <Color.RED: 1>)
('KURENAI', <Color.RED: 1>)
('BLUE', <Color.BLUE: 2>)

もし値が重複するべきでないのであれば、uniqueデコレータが利用できる。これは間違った値の設定防止に役に立つ。

from enum import Enum, unique

@unique
class Color(Enum):
    RED = 1
    KURENAI = 1
    BLUE = 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\xxxxx\AppData\Local\Programs\Python\Python312\Lib\enum.py", line 1594, in unique
    raise ValueError('duplicate values found in %r: %s' %
ValueError: duplicate values found in <enum 'Color'>: KURENAI -> RE

値の自動設定

名前が統一されていることが重要であり、その値が何であるかが大きな意味を持たない場合、autoが指定できる。autoの挙動は_generate_next_value_()メソッドで制御できる。

from enum import Enum, auto

class Color(Enum):
    RED = auto()
    BLUE = auto()
    GREEN = auto()

比較

列挙型はisにより同一性を、==/!=により等価性を評価できる。ただし順序は持たないため<=などの比較はできない。

>>> Color.RED is Color.KURENAI
True
>>> Color.RED is not Color.BLUE
True
>>> Color.RED == Color.KURENAI
True
>>> Color.RED == 1
False
>>> Color.RED > Color.BLUE
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '>' not supported between instances of 'Color' and 'Color'

なお、Enumが定義されたモジュールの再読込が行われた場合、比較できなくなることがある。

from enum import Enum

class Color(Enum):
    RED = 0
    BLUE = 1
    GREEN = 2
import ColorEnum
from importlib import reload

redcolor1 = ColorEnum.Color.RED

reload(Enum1)

redcolor2 = ColorEnum.Color.RED

print(redcolor1 is redcolor2)
print(redcolor1 == redcolor2)
False
False

派生型

Enumにはいくつかの派生型がある。ここでは簡単な紹介に留める。公式ドキュメントではEnumとFlagを使用することが推奨されている。

クラス名特徴
IntEnum整数のように振る舞うEnum。intのサブクラスでもある。
IntEnum同士は比較ができる。
StrEnum文字列のように振る舞うEnum。strのサブクラスでもある。
IntFlagビットフラグのように動作するEnum。intのサブクラスでもある。
ビット演算以外を行うとIntFlagではなくなる。
Flagフラグのように動作するEnum。
>>> class UNIXPermission(IntFlag):
...     R = 4
...     W = 2
...     X = 1
...     RWX = 7 # これはR|W|Xの値と等しいのでエイリアス扱いとなる
...
>>> UNIXPermission.R | UNIXPermission.W
<UNIXPermission.R|W: 6>
>>> UNIXPermission.RWX
<UNIXPermission.RWX: 7>
>>> UNIXPermission.R + UNIXPermission.W
6

構造体

dataclass

構造体とは様々なデータをまとめたものである。Pythonには言語仕様に構造体そのものはないが、同等のことができるdataclassがPython 3.7から導入された。dataclassesモジュールからdataclassをインポートし、クラス定義の前に@dataclassデコレータを付ける

from dataclasses import dataclass

@dataclass
class Human:
    name: str
    age: int

コンストラクタ

コンストラクは自動的に生成される。コンストラクタの引数の順序は定義順になる。

from dataclasses import dataclass

@dataclass
class Human:
    name: str
    age: int

h1 = Human("Eve", 0)

フィールドの初期値を設定することもでき、その場合はコンストラクタでも省略できる。

@dataclass
class Human:
    name: str
    age: int = 20

h1 = Human("test")

引数の制約を受けるため、初期値を設定したフィールドは後ろにまとめられていなければならない。以下は誤った指定の例である。

@dataclass
class Human:
    name: str
    age: int = 20
    sex: str
Exception has occurred: TypeError
non-default argument 'sex' follows default argument

コンストラクタ実行時になにか処理を行いたいなら__post_init__が使用できる。例えば他の要素により決定する要素を初期化できる。

@dataclass
class Human:
    name: str
    birth: date
    age: int = field(init=False) # 初期化しないフィールド

    def __post_init__(self):
        pass # ここでbirthからageを計算する処理を実行

スペシャルメソッド

dataclassはいくつかのスペシャルメソッドを自動で実装する。実装するかどうかはデコレータで指定できる。デフォルトは以下の通り。

@dataclass(init=True,          # __init__
           repr=True,          # __repr__
           eq=True,            # __eq__
           order=False,        # __lt__, __le__, __gt__, __ge__
           unsafe_hash=False,  # __hash__
           frozen=False,       # イミュータブル化
           match_args=True,    # __match_args__
           kw_only=False,      # すべての引数がキーワード専用引数になる
           slots=False)        # __slots__

field

dataclassでは以下のように初期値に空のlistを指定することができない。

@dataclass
class Human:
    name: str
    age: int = 0
#   address: list[str] = []  これはエラーになる

一部のフィールドではこのようなときに特別な設定が必要になる。たとえば特定のフィールドは一致しなくても同じとみなしたい場合もそうである。そのような特別な設定をするにはfiledを使用する。

dataclasses.field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING)

fieldを使用して初期値に空のリストを指定する例を示す。

from dataclasses import dataclass, field

@dataclass
class Human:
    name: str
    age: int
    title: list[str] = field(default_factory=list)

以下は年齢を比較対象にせず、reprで表示もしないフィールド設定の例である。

from dataclasses import dataclass, field

@dataclass
class Human1:
    name: str
    age: int = field(default=0, compare=False, repr=False)

@dataclass
class Human2:
    name: str
    age: int = 0

h11 = Human1("Eve", 5); h12 = Human1("Eve", 10)
h21 = Human2("Eve", 5); h22 = Human2("Eve", 10)

print( F"{h11=}, {h12=}, {h21=}, {h22=}")
print( F"{h11==h12 = }" )
print( F"{h21==h22 = }")
h11=Human1(name='Eve'), h12=Human1(name='Eve'), h21=Human2(name='Eve', age=5), h22=Human2(name='Eve', age=10)
h11==h12 = True
h21==h22 = False

dataclassのエクスポート

作成したdataclassをJSONやYAMLに変換する方法として、Dataclass Wizardライブラリが利用できる。Dataclass Wizardライブラリは標準ライブラリではないためインストールが必要だ。なおYAMLならYAMLのライブラリであるruamel.yamlでも実現できるだろう。

py -m pip install dataclass-wizard

使い方は簡単で、dataclassを作るときに指定のクラスを継承するだけだ。すると変換メソッドが呼び出せる形になる。YAMLならYAMLWizardになる。

from dataclasses import dataclass, field
import dataclass_wizard as dw

@dataclass
class Human1(dw.YAMLWizard):
    name: str
    age: int = field(default=0, compare=False, repr=False)

yaml = h.to_yaml()
print( "==YAML==" )
print( yaml )

h_load = Human1.from_yaml( yaml )
print( "==Loaded==" )
print( h_load )
==YAML==
age: 30
name: Eve

==Loaded==
Human1(name='Eve')

上記では文字列とdataclassを相互変換しているが、ファイルから入出力するメソッドも用意されている。

Dataclass Wizardのオプション

上の例ではYAMLに出力したHuman1のフィールド順序がクラス定義とは異なっていることが分かる。YAMLの変換ではPyYAMLを使っており、PyYAMLのオプションを指定できる。たとえばsort_keysでキーをアルファベット順にソートしないようにする。

from dataclasses import dataclass, field
import dataclass_wizard as dw

@dataclass
class Human1(dw.YAMLWizard):
    name: str
    age: int = field(default=0, compare=False, repr=False)

h = Human1("Eve", 30)

yaml = h.to_yaml(sort_keys=False)
print( "==YAML==" )
print( yaml )
==YAML==
name: Eve
age: 30

==Loaded==
Human1(name='Eve')

独自クラス

基本的な構文

class CounterClass(superclass): # スーパクラスがない場合(objの場合)は()ごと省略可能
    total = 0                   # クラス変数(すべてのインスタンスで共通の変数)

    def __init__(self, name):   # コンストラクタ
        self.name = name        # インスタンス変数

    def getName(self):          # インスタンスメソッド
        return self.name

    @classmethod
    def getCount(cls):          # クラスメソッド
        return cls.total

    @staticmethod
    def convertFrom():          # スタティックメソッド
        pass # 何らかの処理

親クラスの取得

super()関数で親クラスのメソッドを参照できる。コンストラクタで親クラスのコンストラクタを呼び出す例を示す。

    def __init__(self, name):   # コンストラクタ
        super().__init__(name)

可視性

Pythonでは各メソッドや変数の可視性を指定できない。代わりにアンダースコア_から始まる名前にすることでその変数やメソッドは外部から利用することを想定していないことを示す。通常はアンダースコアで始まる関数や変数を外部から参照するべきでない

>>> class Human:
...     _name = "example"
...
>>> Human._name # アクセスできるが、お作法上よろしくない
'example'
マングリングと可視性

アンダースコア2つ__で始まる変数名はマングリング(名前修飾)が行われる。classname.__attributeはclassname._classname__atributeのように_クラス名が付与される。その結果として外部からのアクセスが意図して実装しない限りできなくなる。

>>> class Human:
...     __name = "example"
...
>>> Human.__name # 普通にはアクセスできない
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Human' has no attribute '__name'
>>> Human._Human__name
'example'

ただし、マングリングは継承クラスとの名前の重複を防ぐための仕組みであり、外部からの隠蔽を目的とする機構ではない。想定されるマングリングの使用例を示す。まず、これはマングリングを使用していないプログラムである。

class Reader:
    def _readdata(self):
        print("Reader->readdata")

    def read(self):
        self._readdata()


class CSVReader(Reader):
    def _readdata(self):
        print("CSVReader->readdata")
        super().read()
    
    def read(self):
        self._readdata()

csvreader = CSVReader()
csvreader.read()
RecursionError: maximum recursion depth exceeded

このプログラムは実行できない。csvreader.read()は無限ループに陥ってしまう。流れとしては以下の通りだ。

  1. CSVReader.read()がself._readdata()を呼び出す。このself._readdata()はCSVReaderクラスの_readdataメソッドである。
  2. CSVReader._readdata()がsuper().read()を呼び出す。このsuper().read()はReaderクラスのreadメソッドのことである。
  3. Reader.read()がself._readdata()を呼び出す。このself._readdata()はReaderクラスの_readdataメソッドではなく、オーバーライドされたCSVReaderクラスの_readdataメソッドである。
  4. 2番目に戻る

これは_readdataメソッドの名前が衝突し、オーバーライドされたために起こっている。次にマングリングを使用したプログラムを示す。

class Reader:
    def __readdata(self): # _Reader__readdataに置換される
        print("Reader->readdata")

    def read(self):
        self.__readdata() # _Reader__readdataに置換される


class CSVReader(Reader):
    def __readdata(self): # _CSVReader__readdataに置換される
        print("CSVReader->readdata")
        super().read()
    
    def read(self):
        self.__readdata() # _CSVReader__readdataに置換される
CSVReader->readdata
Reader->readdata

2つのクラスの__readdataはそれぞれ_Reader__readdata、_CSVReader__readdataに置換される。そのため、前のコードで発生したReader.read()が呼び出すself._readdata()がオーバーライドされたCSVReaderクラスの_readdataメソッドになるという事象が発生せず、正常に実行できる。

なお、厳密にはマングリングはアンダースコア2つ以上で始まり、アンダースコア1つ以下で終わる名前に適用される。なので__name__はマングリングの対象とはならない。

プロパティ

プロパティはメンバ変数のようにアクセスできる関数である。getterやsetter付きのメンバ変数と考えてもいいだろう。@propertyデコレータをつけたメソッドがそのプロパティのgetterとなり、@propertyname.setterデコレータをつけたメソッドがそのプロパティのsetterとなる。

プロパティへのアクセスは()をつけずに、インスタンス変数と同じようにアクセスする。

class PropertyTest:
    def __init__(self) -> None:
        self._balance = 0

    @property
    def balance(self):
        return self._balance
    
    @balance.setter
    def balance(self,value):
        if value<0: raise ValueError()
        self._balance = value

proptest = PropertyTest()
print( proptest.balance )
proptest.balance = 10
print( proptest.balance )
proptest.balance = -1
print( proptest.balance )

@propertyデコレータは内部的にはpropertyクラスのオブジェクトを作成している。以下のコードは上と同等の実装である。

class PropertyTest2:
    def __init__(self) -> None:
        self._balance = 0

    def getbalance(self):
        return self._balance

    def setbalance(self, value):
        if value<0: raise ValueError()
        self._balance = value

    # 引数は順にgetter, setter, deleter, docstring
    balance = property(getbalance, setbalance, None, None)

プロパティのユースケースとしては以下のようなものが考えられる。

  • 値を設定するときにチェックを行う
  • 他の属性値の影響で値が変わる

他の属性値の影響を受けるプロパティの例を示す。

class PropertyNameTest:
    def __init__(self, firstname, familyname) -> None:
        self.firstname = firstname
        self.familyname = familyname

    @property
    def fullname(self):
        return f"{self.firstname} {self.familyname}"
    

proptest = PropertyNameTest("Ned", "Peyton")
print( proptest.fullname )

特殊メソッド

Pythonでは特殊メソッド(スペシャルメソッド)を実装することでオブジェクトの動作を定義できる。例えば数値的な要素を持つクラスであれば__add__メソッドを実装することで演算子+による加算が行えるようになる。

インスタンスの生成・削除に関する特殊メソッド

用途メソッド呼び出し例説明
インスタンスの生成object.__new__( cls, ... )obj = Class()新しいインスタンスを作成するときに使う。通常は作成するクラスのインスタンスを返すが、異なるクラスのインスタンスを返しても良い。__new__が異なるクラスのインスタンスを返した場合__init__は呼び出されない。
実装しない場合、親クラスの__new__が呼び出される。
通常は__init__でオブジェクトを初期化するため、__new__はあまりカスタマイズしない。
インスタンスの初期化object.__init__( self, ... )obj = Class()__new__によって作成されたオブジェクトの初期化を行う。
実装しない場合、親クラスの__init__が呼び出される。
インスタンスの削除object.__del__( self )del objインスタンスが破棄されるときに呼び出される。ファイナライザやデストラクタとも呼ばれる。
補足:呼び出し例のdel xは正確にはxの参照カウントを減らすものであり、参照カウントが0になったときにx.__del__()が呼び出される。

__new__メソッドの実装の違いにより、__init__メソッドが呼び出される場合とそうでない場合の例を示す。Sample1クラスの__init__メソッドは数値リストを返しているため、__init__メソッドが呼び出されない。

class Sample1():
    def __new__(cls):
        print("new")
        return [1, 2, 3]

    def __init__(self): # この__init__は呼び出されない
        print("init")

class Sample2():
    def __new__(cls):
        print("new")
        return super().__new__(cls)

    def __init__(self):
        print("init")

print("Sample1")
Sample1()

print("\nSample2")
Sample2()
Sample1
new

Sample2
new
init

文字列・バイト列に変換する特殊メソッド

用途メソッド呼び出し例説明
公式の文字列に変換object.__repr__( cls, ... )objオブジェクトの「公式の」文字列を計算する。この文字列は同じオブジェクトを再生成できるような表現が望ましい。できないなら"<何らかの情報>"の形式の文字列を返すべきである。
これはデバッグでも用いられるため、曖昧でない十分な情報を含むことが重要である。
非公式の文字列に変換object.__str__( self, ... )str(obj)オブジェクトの「非公式の」文字列を計算する。__repr__とは異なり、ユーザフレンドリーな(簡潔な)文字列を使用して良い。デフォルトでは__repr__を呼び出す。
バイト列に変換object.__bytes__( self )bytes(obj)オブジェクトのバイト列表現を計算する。
フォーマットされた文字列に変換object.__format__( self, format )f'{obj:+,}'フォーマット文字列などで指定されたフォーマットどおりの文字列を計算する。

公式(repr)と非公式(str)の違いはdatetimeの実装を見るとわかりやすい。datetimeにおいてreprが返す文字列は、実行可能なPythonコードとなっている。このコードを実行すると同値のオブジェクトが得られる。このような条件さえ整えば同じオブジェクトを再生成できるような値が望ましいのがreprである。

import datetime
now = datetime.datetime.now()

print( f"{repr(now) = }" )
print( f"{str(now)  = }" )

# 文字列をPythonコードとして実行するeval関数にreprの結果を渡す
then = eval( repr(now) )
print( f"{str(then) = }" )
repr(now) = 'datetime.datetime(2023, 10, 3, 8, 59, 3, 527894)'
str(now)  = '2023-10-03 08:59:03.527894'
str(then) = '2023-10-03 08:59:03.527894'

比較に関するメソッド

用途メソッド呼び出し例説明
より小さいobject.__lt__( self, other )obj < other
小さいか等しいobject.__le__( self, other )obj <= other
等しいobject.__eq__( self, other )obj == other
等しくないobject.__ne__( self, other )obj != other
大きいobject.__gt__( self, other )obj > other
大きいか等しいobject.__ge__( self, other )obj >= other
ハッシュobject.__hash__( self )hash( obj )dictなどでも使われる
論理object.__bool__( self )bool( obj )オブジェクトそのものの真偽判定

obj > otherを実行したときに、もしotherがobjのサブクラスの場合はotherのメソッドが優先される。つまりobj.__gt__(other)ではなく、その対となるother.__le__(obj)が実行される。

数値演算に関するメソッド

self + otherのような二項演算子でも特殊メソッドobject.__add__(self, other)が呼び出される。これを実装することで数値のように動作するクラスを作成できる。すべての数値演算に関するメソッドはPython3 Documentation 数値型をエミュレートするを参照すること

属性に関するメソッド

オブジェクトに関連付けられ、通常ドットを使用してobject.nameの形でアクセスできるものを属性またはアトリビュートとよぶ。属性へアクセスすると内部的には以下のメソッドが呼び出されている。

用途メソッド呼び出し例説明
属性への代入object.__setattr__( self, name, value )obj.x = 5
属性へのアクセスobject.__getattribute__( self, name )obj.x属性にアクセスするときに呼び出される。
属性の削除object.__delattr__( self, name )del obj.x

これらのメソッドをカスタマイズすることで挙動を変更できる。メソッドの呼び出し状況を確認するために標準出力するように変更してみる。

class Item(object):
    def __init__(self) -> None:
        self.stock = 0
    
    def __setattr__(self, __name: str, __value: Any) -> None:
        print(f"__setattr__ called:{__name},{__value}")
        super().__setattr__(__name,__value)
    
    def __getattribute__(self, __name: str) -> None:
        print(f"__getattribute__ called:{__name}")
        return super().__getattribute__(__name)
    
    def __delattr__(self, __name: str) -> None:
        print(f"__delattr__ called:{__name}")
        super().__delattr__(__name)


i = Item()
print("--------------------------------")
i.stock = 100
print("--------------------------------")
print(i.stock)
print("--------------------------------")
del i.stock
print("--------------------------------")
i.name = "ItemName"
__setattr__ called:stock,0
--------------------------------
__setattr__ called:stock,100
--------------------------------
__getattribute__ called:stock
100
--------------------------------
__delattr__ called:stock
--------------------------------
__setattr__ called:name,ItemName

__setattr__メソッドは定義されていない属性であれば自動で追加する動作がデフォルトである。なので最後のi.name = "ItemName"はエラーにならない。

一方で__getattribute__メソッドは定義されていない属性だとAttributeErrorが発生する。

i = Item()
print("--------------------------------")
print(i.name)
__setattr__ called:stock,0
--------------------------------
__getattribute__ called:name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 11, in __getattribute__
__getattribute__ called:__dict__
__getattribute__ called:__class__
AttributeError: 'Item' object has no attribute 'name'

もしクラスに__getattr__メソッドが定義されている場合、存在しない属性にアクセスした場合(主に__getattribute__でAttributeErrorが発生した場合)に__getattr__メソッドが呼び出される。そこでデフォルト値を定義するなどの挙動を制御できる。なお、__getattribute__は、アクセスしようとした属性の値を返すか、AttributeErrorを送出しなければならない。

class Item(object):
    def __init__(self) -> None:
        self.stock = 0

    def __setattr__(self, __name: str, __value: Any) -> None:
        print(f"__setattr__ called:{__name},{__value}")
        super().__setattr__(__name,__value)

    def __getattribute__(self, __name: str) -> None:
        print(f"__getattribute__ called:{__name}")
        return super().__getattribute__(__name)
    
    def __delattr__(self, __name: str) -> None:
        print(f"__delattr__ called:{__name}")
        super().__delattr__(__name)

    def __getattr__(self, __name: str):
        print(f"__getattr__ called:{__name}")
        match __name:
            case "stock":
                self.stock = 0
                return self.stock
            case "name":
                self.name = "Void"
                return self.name
            case _:
                raise AttributeError(f"Unsupported attribute:{__name}")


i = Item()
print("--------------------------------")
print(i.stock)
print("--------------------------------")
print(i.name)
__setattr__ called:stock,0
--------------------------------
__getattribute__ called:stock
0
--------------------------------
__getattribute__ called:name
__getattr__ called:name       # 存在しない属性へのアクセス
VOID

属性へのアクセスは組み込み関数getattr、setattrでも行える。getattrでは属性が存在しないときのデフォルト値を指定できるメリットがある。

i = Item()
print("--------------------------------")
setattr(i, "stock", "100")
print("--------------------------------")
print( getattr(i, "name", "DEFAULTNAME") )  # 呼び出し時点では存在しないが__getattr__がサポートしている属性
print("--------------------------------")
print( getattr(i, "price", 10000) )         # 存在しない、__getattr__もサポートしていない属性で、デフォルト値の指定あり
print("--------------------------------")
print( getattr(i, "id") )                   # 存在しない、__getattr__もサポートしていない属性で、デフォルト値の指定なし
__setattr__ called:stock,0
--------------------------------
__setattr__ called:stock,100
--------------------------------
__getattribute__ called:name
__getattr__ called:name
__setattr__ called:name,Void
Void
--------------------------------
__getattribute__ called:price
__getattr__ called:price
10000
--------------------------------
__getattribute__ called:id
__getattr__ called:id
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 27, in __getattr__
__getattribute__ called:__dict__
__getattribute__ called:__class__
AttributeError: Unsupported attribute:id
遅延評価

__getattribute__などを利用して属性の遅延評価が行える。遅延評価を簡単に言うなら、属性の値が必要になってから属性値を計算することで、インスタンス生成時にかかるコストや、値を保持するために必要なメモリなどを抑えるというものである。

たとえば初期化に時間がかかるアトリビュート:nameがあるとしよう。普通の実装では、nameを使用するかによらずインスタンスを生成するときにコンストラクタでnameを初期化するために時間がかかる。以下のプログラムでは擬似的にtime.sleepで時間がかかるようにしている。

class Item(object):
    def __init__(self) -> None:
        time.sleep(1) # nameの計算に時間がかかる
        self.name = "default name"


item = Item() # この処理に時間がかかる

遅延評価を行うサンプルを示す。次に示す実装では初めてnameを取得しようとしたときにnameの初期化が行われ、時間がかかる。もしnameが使われなければ時間がかかることはない。また、一度初期化が行われれば、次から時間がかかることもない。

class Item_Delayed(object):
    def __init__(self) -> None:
        pass
    
    def __getattr__(self, __name: str):
        match __name:
            case "name":
                time.sleep(1) # nameの計算に時間がかかる
                setattr(self, "name", "Void")
                return "Void"
            case _:
                raise AttributeError(f"Unsupported attribute:{__name}")


itemdelay = Item_Delayed()
print(itemdelay.name) # name属性の初回アクセスで時間がかかる
print(itemdelay.name) # name属性の二回目アクセスで__getattr__は呼び出されないため時間がかからない

次にメモリ使用量へ影響する例を見てみる。10万個の数値データを持つクラスを生成してみた。

from memory_profiler import profile

class Item:
    def __init__(self) -> None:
        self.codes = [x for x in range(0,100000)]


class ItemDelayed:
    def __getattr__(self, __name: str):
        if  __name == (codes := "codes"):
            value = [x for x in range(0,100000)]
            setattr(self, codes, value)
            return value
        else:
            raise AttributeError(f"Unsupported attribute:{__name}")


@profile
def main():
    item = Item()
    itemDelayed = ItemDelayed()
    itemDelayed.codes


main()
Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    19     41.8 MiB     41.8 MiB           1   @profile
    20                                         def main():
    21     46.7 MiB      4.9 MiB           1       item = Item()
    22     46.7 MiB      0.0 MiB           1       itemDelayed = ItemDelayed()
    23     50.6 MiB      3.9 MiB           1       itemDelayed.codes

これはmemory_profilerライブラリを使用して、行ごとにメモリ使用量の増減を出力したものである。Itemクラスではインスタンス生成時にメモリが増加しているのに対し、ItemDelayedクラスではcodesへアクセスが発生したときにメモリが増加している。

デコレータ

デコレータは複数の関数またはクラスに対して共通の加工を行う機能である。

デコレータ利用の背景と動作イメージ

2つの数字を加算する関数add_numberがある。add_numberは変更することなく、この処理の開始前後で標準出力にメッセージを出力する処理を行いたい。

def add_number(int1,int2):
    return int1+int2

これは内部でadd_numberを呼び出す別の関数を作成すれば簡単に実現できる。

def add_number_debug(int1,int2):
    print("add_number start")
    v = add_number(int1,int2)
    print("add_number end")
    return v

このように、定型の加工を既存の関数に行いたいというケースがあり、それを実現する方法の1つがデコレータである。

まずは、この加工をプログラムで行ってみる。つまり「add_number関数を引数として受け取りadd_number_debug関数を返す」関数を作成する。次のmodifier関数は加工する関数を引数targetにとり、その前後に出力を追加した関数wrapper_functionを返している。

def add_number(int1,int2):
    return int1+int2

def modifier(target):
    def wrapper_function(int1,int2):
        print( "add_number start")
        v = target(int1,int2)
        print( "add_number end")
        return v
    
    return wrapper_function


add_number_debug = modifier(add_number)

print( add_number_debug(1,3) )

ここで作成したmodifierは引数が2つに固定されており、出力内容もadd_numberという関数に固定されている。それを任意の引数の個数、関数名に対応するように改善した関数を加工する関数printmethodは以下のようになる。

def printmethod(target:Callable):
    def wrapper_function(*positionalargs, **keywordargs):
        print(f"{getattr(target, '__name__', 'Function')} start")
        v = target(*positionalargs, **keywordargs)
        print(f"{getattr(target, '__name__', 'Function')} end")
        return v
    
    return wrapper_function

ここで加工したい関数に対して@printmethodを付与する。

@printmethod
def add_number(int1,int2):
    return int1+int2

こうすることでadd_numberに対してprintmethodによる加工ができようされる。例えば、これでadd_number(3,4)を呼び出すと、以下の結果が得られる。

add_number start
add_number end
7

試しにmult_number関数を追加で作成し、それに対してprintmethodによる加工をした場合の例を示す。

@printmethod
def mult_number(int1,int2):
    return int1*int2

mult_number(3,4)
mult_number start
mult_number end
12

引数をとるデコレータ

動作を調整するパラメータを引数にとるデコレータも作成できる。この場合は二重のwrapper_functionを作成する形になる。

def recordtime(tzinfo:datetime.tzinfo):
    def wrapper_function(target:Callable):
        def inner_function(*positionalargs, **keywordargs):
            print( f"{datetime.datetime.now(tzinfo)} start")
            v = target(*positionalargs, **keywordargs)
            time.sleep(2.5)
            print( f"{datetime.datetime.now(tzinfo)} end")
            return v
        return inner_function
    return wrapper_function


@recordtime(datetime.timezone.utc)
def add_number(int1,int2):
    return int1+int2

まずrecordtime(datetime.timezone.utc)の部分が実行される。この関数が返すwrapper_functionは以下と同じと考えることができる。

def wrapper_function(target:Callable):
    def inner_function(*positionalargs, **keywordargs):
        tzinfo = datetime.timezone.utc # パラメータは設定済み
        print( f"{datetime.datetime.now(tzinfo)} start")
        v = target(*positionalargs, **keywordargs)
        time.sleep(2.5)
        print( f"{datetime.datetime.now(tzinfo)} end")
        return v
    return inner_function

このwrapper_functionはデコレータ用の関数の構文を満たしていることがわかる。つまりrecordtime関数はパラメータが設定済みのデコレータ用関数を返す関数といえる。こうなってしまえばあとは普通のデコレータと同じである。パラメータを変えて実行するサンプルを示す。

@recordtime(datetime.timezone(datetime.timedelta(hours=9)))
def add_number(int1,int2):
    return int1+int2


@recordtime(datetime.timezone.utc)
def mult_number(int1,int2):
    return int1*int2


print( add_number(3, 5) )
print( mult_number(3, 5) )
2023-11-13 21:23:24.248930+09:00 start
2023-11-13 21:23:25.250401+09:00 end
8
2023-11-13 12:23:25.250908+00:00 start
2023-11-13 12:23:26.252100+00:00 end
15

例外処理

基本構文

0除算など、例外が発生した場合の処理は以下のように指定する。

try:
    # 例外が発生しうる処理
except SomeError as e:
    # ある例外が発生したときの処理
except (CertinError, AnotherError) as e:
    # ある例外が発生したときの処理
else:
    # 例外が発生しなかったときの処理
finally:
    # 例外の発生如何に関わらず最後にかならず実行する処理

キャッチした例外を参照しないならas eは不要。

例外を上げる

raise ValueError("エラーメッセージ")

例外を処理中に別の例外が発生することがある。その場合はraise...from...で元となった例外を示すことができる。

raise newexception from originalexception

トレースバックを取得

import traceback

try:
    # 例外が発生しうる処理
except:
    traceback.print_exc()

例外の種類

よく使われる例外の種類を示す。

例外名主な発生する状況
Exceptionすべての例外の基底クラス。
AttributeError存在しない属性を参照している場合など。タイプミスの可能性あり。
EOFErrorデータを読み取るときに予期せずEOF(End Of File)となったとき
ImportErrorモジュールのインポートで問題が発生したとき
IndexErrorシーケンスのインデックスが範囲外のとき
KeyErrorマップのキーが存在しないとき
KeyboardInterruptユーザーが割り込みを発生させたとき
MemoryErrorメモリが不足したとき
NameError存在しない変数を参照したとき
OSErrorシステム関連のエラーが発生した場合に発生
OverflowError数値演算でオーバーフローが発生したとき
RuntimeError実行時エラーが発生したとき
StopIterationイテレータが終了したとき
SyntaxError構文エラーがあったとき
SystemErrorインタプリタ内部でエラーがあった場合に発生
TypeError型エラーがあったとき
ValueError値が不正なとき

独自例外

適当な例外がない場合は独自に例外を作ることもできる。Exceptionクラスを基底クラスにすれば良い。

class OriginalException(Exception):
    def __init__(self, message):
        super().__init__(message)

型ヒント

Pythonは動的型付け言語と呼ばれ、変数の型を記載しない。実行したときに変数や属性に代入されている型が決定される。

とはいえ実装するときには型を意識しなければならない。コードに変数の型が書かれていないことで、コードを時間を空けて見たときになんの型かわからなくなってしまったり、開発環境の支援(例えばメソッドの候補を表示する)機能が十分に働かないことがある。型ヒントを使用することで、コードの可読性が上がり、開発環境の支援機能がより適切に働くなどのメリットがある。

型ヒントは以下のように記載する。

# 変数の宣言
i:str = "文字列"

# 関数の引数と戻り値
def methodname(arg1:str, arg2:int) -> str:
    pass

型の記法

主な型の記法に示す。

記法備考
整数int
小数float
文字列str
リストlist[ X ] (3.9以上)古いバージョンではtyping.Listでtypingモジュールのインポートが必要。
タプルtuple[ X, Y, ... ] (3.9以上) 古いバージョンではTupleでtypingモジュールのインポートが必要。
辞書dict[ キーの型, バリューの型 ] (3.9以上) 古いバージョンではtyping.Dictでtypingモジュールのインポートが必要。
シーケンスSequence[ X ]
MutableSequence[ X ]
インポート必要
from collections.abc import Sequence, MutableSequence
コレクションCollection[ X ]インポート必要
from collections.abc import Collection
イテラブルIterableインポート必要
from collections.abc import Iterable
関数Callable[ [引数の型], 戻り値の型 ]インポート必要
from collections.abc import Callable
すべてAnyインポート必要
from typing import Any
自身Selfクラス定義などで使用。インポート必要
from typing import Self

特殊な記法

形式記法意味
ユニオン、ORUnion[ X, Y ]
X | Y
指定された型のいずれか
オプショナルOptional[ X ]指定された型 または None
Optional[X] は Union[ X, None ] と同義
リテラルLiteral[ ... ] (3.8以上)[]の中で指定された値
Literal[ "RED", "BLUE" ] であれば、"RED"または"BLUE"という文字列

その他の記法

その他の文、関数

open

ファイルを開く。詳細はIOモジュールの説明を参照。

assert

assertはデバッグ支援用の構文であり、アサーションとよばれるものである。アサーションは満たされるべき条件を示すものであり、満たされない場合にAssertionErrorを発生させる。ただし最適化を有効にするとアサーションが無効になり、条件の評価は行われない。

assert condition[, message]

assertは関数ではなく式である。そのためassert(...)のように()をつけない。つけてしまうとSyntaxWarningが通知される。次の例はassertを関数のように使用している。これはconditionに(False,"Error")と指定したのに等しい。空でないタプルは常にTrueであるため、このassertは常に成功する。

# 間違った例
assert(False, "Error")
SyntaxWarning: assertion is always true, perhaps remove parentheses?

assert文が長くなると折り返したくなることがある。その場合は継続文字\で文が次の行も継続していることを示す。

number = 5
assert isinstance(number, int|float) and number > 0, \
       f"NUMBER should be a positive number, but {number} was given."

assertの利用例

assertの主な役割は、バグが発生したときの通知である。ユーザに年齢を尋ねるプログラムの例を示す。

age_str = input("enter your age:")
if not re.match("\d+$",age_str,re.ASCII) or (age := int(age_str)) <= 0:
    print("不正な値が入力されました。")

ユーザの入力値がおかしくなることは十分予想できることであり、入力値が不正なときはメッセージを表示し処理を終了させたり、再入力を促すのが一般的な仕様である。このような仕様外の値が想定されるデータの検証でassertを使うべきではない。なぜなら最適化を有効にするとassertは削除されてしまい、検証が行われなくなるためである。また、AssertionErrorをexceptで補足するようなコードも書くべきではない。assertはあくまで開発時のデバッグに用いるのがよい。

それではassertの利用パターンの例を素因数分解する処理で考えてみる。これは試し割りにより素因数分解するものである。

このプログラムには不具合がある。

def factorlist():
    # 試し割りする値のジェネレータ
    yield from (2,3,5)
    value = 7
    while True:
        for d in (2,2,2,4):
            yield value
            value += d

def pf(number:int):
    # 試し割りで素因数分解する
    primelist = []
    for v in factorlist():
        if number <= v: break
        while number%v == 0:
            number /= v
            primelist.append(v)
    return primelist

試しに実行してみると、以下のようになる。これは正しく素因数分解できている。

>>> pf(100)
[2, 2, 5, 5]
>>> pf(987945)
[3, 5, 7, 97, 97]
引数の値のチェック

この関数は実引数でおかしな値(たとえば数値でない値)が渡された場合を考慮していない。この対応として以下のような選択肢がある。

  1. 入力値をチェックしない
  2. 入力値をチェックする
  3. 入力値のアサーションは行う

入力値をチェックしないということは、呼び出し元のプログラムが正しい値を渡すことや例外処理を行うことを期待するということだ。試しに文字列を渡してみるとTypeErrorが発生する。

TypeError: '<=' not supported between instances of 'str' and 'int'

これから文字列と整数の比較が行われたことは分かるものの、この関数に不具合があったのか、この関数を呼び出す側に不具合があったのかはわからない。

入力値をチェックするなら以下のようなコードになるだろう。この実装であれば、不正な値が指定された時点でValueErrorが送られる。

def pf_with_inputcheck(number:int):
    if not (isinstance(number, int) and number >= 1):
        raise ValueError(f"Number must be a integer larger than 1, but was {number!r}.")

    primelist = []
    for v in factorlist():
        if number <= v: break
        while number%v == 0:
            number /= v
            primelist.append(v)
    return primelist

この関数に文字列を渡してみると以下のような例外が発生し、正整数が期待されているのに文字列が渡されたのが分かるようになる。

ValueError: Number must be a integer larger than 1, but was '1'.

次にアサーションでチェックする例を示す。

def pf_with_inputassert(number:int):
    assert isinstance(number, int) and number >= 1, \
           f"Number must be a integer larger than 1, but was {number!r}."

    primelist = []
    currentvalue = number
    while currentvalue > 1:
        for i in range(2,currentvalue):
            if currentvalue%i == 0:
                currentvalue //= i
                primelist.append(i)
                break
 
    return primelist

この関数に文字列を渡してみると以下のような例外が発生し、正整数が期待されているのに文字列が渡されたことで、期待した条件が満たされなかったことが分かるようになる。

AssertionError: Number must be a integer larger than 1, but was '100'.

if文での入力チェックでは異常値と判定する条件を指定するのに対し、assertでは正常とみなす条件を示している点に注意が必要である。また、最適化を有効にしてプログラムを実行するとassertでのチェックは行われなくなるので、開発中はデバッグのためにチェックしてリリース時にはチェックしないということができる。不具合が無い限りはおかしな値が入ることがないときにはassertの方が適しているといえるだろう。

不具合はテストでつぶすという考えで、if文でのチェックもassertでのアサーションもしないという選択も間違いではない。開発規模や設計思想にもとづいて選ぶことが重要だ。

処理結果のチェック

このプログラムで20を素因数分解すると正しい結果が得られない。

>>> pf(20)
[2, 2]

このような不具合の発見のために素因数分解が正しくできたか検算をassertでさせることができる。

def pf_with_outputassert(number:int):
    # 試し割りで素因数分解する  
    originalnumber = number 
    primelist = []
    for v in factorlist():
        if number <= v: break
        while number%v == 0:
            number /= v
            primelist.append(v)
    
    assert (result := reduce(lambda x,y: x*y, primelist))==originalnumber, \
        f"Prime factorization error. {originalnumber} != {"*".join(map(str,primelist))} = {result}."
    return primelist

これで20を素因数分解してみると以下のような20が2*2に素因数分解されたというエラーが発生する。

AssertionError: Prime factorization error. 20 != 2*2 = 4.

このような検算には追加のコストがかかるが、不具合の早期発見には役に立つ。必要ないときは最適化を有効にしておけば良い。このassertの利用法は単体テストで置き換えることができるだろう。そのためassertは行わない選択も正しい。

ちなみに不具合を除いたコードは以下のようになる。

def pf_fixed(number:int):
    # 試し割りで素因数分解する  
    originalnumber = number 
    primelist = []
    for v in factorlist():
        if number == 1: break
        while number%v == 0:
            number /= v
            primelist.append(v)
    
    return primelist
処理途中のチェック

長い処理では途中で条件を確認する方が都合が良いこともあるだろう。ここでは素因数分解には時間がかかることから、値を100,000未満に限定するように改変してみた。

def factorlist_bug():
    yield 2
    yield 3
    value = 5
    while value < 10000:
        yield value
        value += 2

def pf_bug(number:int):
    if not( isinstance(number,int) and 1 < number < 100000 ):
        raise ValueError(f"Number must be integer and from 2 to 99999. Got {number}({type(number)}).")    
    # 試し割りで素因数分解する  
    originalnumber = number 
    primelist = []
    for v in factorlist_bug():
        if number == 1: break
        while number%v == 0:
            number /= v
            primelist.append(v)
    else:
        assert False, "You should not encounter this message. " \
            f"{originalnumber=}, {number=}, {v=}, {primelist=}"

    return primelist

5行目にタイプミスがあるため、一部の値で素因数分解に失敗しAssertionErrorが発生する。

>>> pf_bug(67432)
[2, 2, 2, 8429]
>>> pf_bug(67434)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 13, in pf_bug
AssertionError: You should not encounter this message. originalnumber=67434, number=11239.0, v=9999, primelist=[2, 3]

このassertは本来は到達しないはずの場所に書かれている。そこでassert Falseとすることでassertに到達すると必ずAssertionErrorが発生する仕組みとなっている。これはきれいなアサーションの書き方ではないが、不具合がどこに含まれているか通知するのに役立つ可能性がある。

del

オブジェクトを使用しなくなったことを明示的に示すものであり、参照を削除する。

x = 5

del x

print(x) # NameErrorとなる

ミュータブルなシーケンスに対してdelを実行すると、その要素が削除される。

>>> data = [0,1,2,3]
>>> del data[2]
>>> data
[0, 1, 3]

global

globalを使用して大域変数を現在のコードブロックで書き換えることを宣言する。xを大域変数のxと関連付けると考えても良いかもしれない。

x = 5 # グローバル変数


def method1(): # ローカルで定義していない変数xを参照
    print(f"  1:{x}") 


def method2(): # グローバルと重複している変数xに代入
    x = 7
    print(f"  2:{x}")


def method3(): # global宣言の上で、変数xに代入
    global x

    x = 7
    print(f"  3:{x}")


print("method1")
method1()
print(f"  1:{x}")

print("\nmethod2")
method2()
print(f"  2:{x}")

print("\nmethod3")
method3()
print(f"  3:{x}")
method1
  1:5    # 何も書かなくてもグローバル変数は参照できる
  1:5

method2
  2:7    # メソッド内ではx=7となっている
  2:5    # 上のx=7はローカル変数であるため、グローバル変数xには影響ない

method3
  3:7    # メソッド内ではx=7となっている
  3:7    # global宣言しているため、グローバル変数xも変更されている

exec、eval

文字列をPythonスクリプトまたは式として実行する。globalsとlocalsでグローバル変数、ローカル変数を辞書形式で指定できる。

exec(object, globals=None, locals=None, /, *, closure=None) 
# execはコードを指定できる closure引数は3.11から追加

eval(expression, globals=None, locals=None)
# evalは式のみ

evalは式の評価結果が戻り値となる。execは常にNoneを返す。

>>> eval('4+5')
9
>>> exec('4+5')
>>>

with

ファイルをオープンするときにwithを使うと、withが終了するときに自動的にファイルがクローズされる。これはコンテキストマネージャによるものである。コンテキストマネージャを利用するとコンテキスト(≒一時的な状態・環境)の開始、終了の処理を委ねることができる。withはtry/finallyの標準的な使用方法を示すものと考えることができる。

ファイルをオープンするときの使用例を示す。with外部でfのreadlineメソッドを呼び出すとクローズされたファイルへのアクセスということでエラーが発生する。これはwithが終了したときに自動でfが閉じられるためである。

with open('log.ini', mode='r') as f:
    print( "with内部" )
    print( f.readline() )

print( "with外部" )
print( f.readline() )     # ValueError: I/O operation on closed file.

withで使用できるものはスペシャルメソッド__enter__と__exit__が定義されている。その名の通り__enter__がwithブロックに入るときの処理であり、__exit__がwithブロックを抜けるときの処理である。

class Test():
    def __enter__(self):
        print("enter")
        return self

    def __exit__(self, exc_type, exc_value, traceback): # with内部で発生した例外を受け取る引数がある
        print("exit")

with Test() as e:
    print('with')
enter
with
exit

またデコレータを使うことで関数形式でもコンテキストマネージャを作成できる。以下のuseA関数はクラスAを管理するマネージャである。

from contextlib import contextmanager

class A:
    def print(self):
        print("A")

@contextmanager
def useA():
    print("enter")
    a = A()
    try:
        yield a # yieldの前までがenterに相当する部分、yieldの後がexitに相当する部分
    finally:
        # Code to release resource, e.g.:
        print("exit")
        del a

with useA() as a:
    a.print()

ファイルオープン以外のコンテキストマネージャの例を示す。

用途構文サンプル
標準出力のリダイレクトredirect_stdout(new_target)with redirect_stdout(io.StringIO()) as f:
pass
s = f.getvalue()
作業ディレクトリの変更chdir(path)

文字列のフォーマット

変数の値を元にメッセージを出力したり、変数の値を決まった書式で出力したいことがある。その場合はフォーマットを使う。文字列のフォーマットには組み込み関数format、文字列strのメソッドstr.format()、フォーマット文字列(f-string)がある。なお、古くからある%演算子によるフォーマットもあるが、今はformatやf-stringのほうが推奨されているようだ。

str.format()では、値を挿入したい位置に{}を置き、引数で挿入する値を渡す。値を渡す方法は引数の順番で指定する方法とキーワードで指定する方法がある。

# 順番で指定する方法。引数の先頭から0,1,2,...となる
"{1}-{0}-{0}".format(10,"abc")

# キーワードで指定する方法
"{number}-{name}-{number}".format(number=10,name="abc")
abc-10-10
10-abc-10

引数の順番で指定する場合で、{}の出現順と引数の位置が1対1で対応する場合、順番の指定は省略できる。これは自動的なフィールドの番号付け(automatic field numbering)とよばれる。

"{0}-{1}-{2}".format(10,"abc","ABC")
"{}-{}-{}".format(10,"abc","ABC")
10-abc-ABC

引数の順番を一部だけ省略することはできないが、順番での指定とキーワードでの指定を混在させることはできる。

# 次はすべての順番指定が省略されていて、1対1対応しているのでOK。キーワード指定の{name}と{number}は省略には影響しない。
"{}-{name}-{number}-{}".format(100,200,number=10,name="abc") 
# 次は順番指定が省略されているものといないものが混在しているのでエラー
"{}-{1}".format(100,200,number=10,name="abc")

変換フラグ

変換フラグが指定されている場合、フォーマットを行う前に文字列に変換する。変換フラグは引数の位置またはキーワード指定の次に続く。

変換フラグ動作
!sstr()で文字列に変換
!rrepr()で文字列に変換
!aascii()で文字列に変換
value=datetime.datetime.now()

print(   "{0}".format(value) ) # 変換フラグなし
print( "{0!s}".format(value) )
print( "{0!r}".format(value) )
print( "{0!a}".format(value) )
2023-09-18 17:15:16.974392
2023-09-18 17:15:16.974392
datetime.datetime(2023, 9, 18, 17, 15, 16, 974392)
datetime.datetime(2023, 9, 18, 17, 15, 16, 974392)

書式指定子

フォーマットするときに値をどのように表示するか指定できる。この指定方法を書式指定子や書式指定文字列とよぶ。

フォーマットで使用する書式指定子の記法を以下に示す。書式指定子は変換フラグの次に続く。

:[[fill]align][sign][#][0][minimumwidth][grouping_option][.precision][type]

各項目の意味は以下の通り。

項目説明
fill空いた桁数を埋めるときに使うパディング文字。デフォルトは空白。
align<:左寄せ
>:右寄せ
^:中央寄せ
=:記号は左寄せ、値は右寄せ
sign-:負数のときだけ記号を表示(デフォルト)
+:正数のときも記号を表示
(半角空白):負数のときは-、正数のときは空白
#数値を2進数や16進数で表示するときに接頭辞(0bや0x)を表示
00で開いた桁を埋める。fillに0、alignに=を指定したのと同じ。
minimumwidth最小の桁数。この桁数には記号や0xなどの接頭辞も含まれる。
grouping_option3桁ごとの区切り文字を指定。指定できるのはカンマ(,)またはアンダーバー(_)
precision小数点以下何桁を表示するか
数値以外の場合は最大フィールドサイズ
整数の場合は無視される
typeデータをどの用に表示するか(別表参照)

typeはデータの型によって異なる。整数で使用できるtypeを示す。

type意味
b2進数
c文字
d整数(10進数)
o8進数
x16進数(小文字)
X16進数(大文字)
nロケール依存の桁区切り付き整数(10進数)
''dと同じ
整数で使用できるtype

続いて少数で使用できるtypeを示す。

e指数表現で表示。例:1.300025e+02
E指数表現で表示。大文字のEを使用。例:1.300025E+02
f固定少数点で表示。例:130.0025
F固定小数点で表示
g一般的なフォーマット。桁数が少なければ固定小数点、桁数が多ければ指数表現。
G一般的なフォーマット。桁数が少なければ固定小数点、桁数が多ければ指数表現。指数表現のときは大文字のEを使用。
nロケール依存の小数点記号使用の一般フォーマット。
%パーセント表示。
''gと同じ。
少数で使用できるtype

ロケール依存のnを指定すると桁区切りや小数点記号が変わることがある。以下にサンプルを示す。

valueint  =1000000
valuefloat=1.11111

locales = (
    "pt_PT.UTF-8", # ポルトガル語-ポルトガル
    "de_DE.UTF-8", # ドイツ語-ドイツ
    "ja_JP.UTF-8", # 日本語-日本
)

for _locale in locales:
    locale.setlocale(locale.LC_NUMERIC, _locale) # ロケールを切り替え
    print( f"=={_locale[:5]}==")
    print( "  {0} -> {0:n}".format(valueint) )
    print( "  {0} -> {0:n}".format(valuefloat) )
==pt_PT==
  1000000 -> 1 000 000
  1.11111 -> 1,11111
==de_DE==
  1000000 -> 1.000.000
  1.11111 -> 1,11111
==ja_JP==
  1000000 -> 1,000,000
  1.11111 -> 1.11111
オブジェクトの書式指定子

オブジェクトは独自の書式指定子を定義できる。たとえば日付を表すdatetimeでは以下のような指定ができる。

"{:%Y-%m-%d}".format( datetime.datetime.now() )

以下に独自のフォーマット書式を持つクラスのサンプルを示す。このサンプルはdatetimeのフォーマット書式を流用しているが、ようは__format__メソッドを実装すれば良い。

import datetime

class FormatTest:
    def __init__(self) -> None:
        self.datetime =datetime.datetime.now()
    
    def __format__(self, __format_spec: str) -> str:
        return self.datetime.strftime(__format_spec)


formattest=FormatTest()
value ="{:%Y%m%d}".format(formattest)

print(value)
20230918

docstring

クラスや関数の宣言の下でクオーテーション3つで囲む。VS Codeであれば自動補完する拡張機能を入れると良い。

def test(arg:Any, method:Callable):
    """_summary_

    Args:
        arg (Any): _description_
        method (Callable): _description_
    """

docstringの記法はいくつかあるが好きなものを選べば良い。

PEP 8

PEPとはPythonの機能追加や拡張などのディスカッションを行うために作成される文書(設計書)である。これらの文書には番号が割り振られており、PEP 8とは8番のPEP文書であることを意味している。

PEP 8はPEPや標準ライブラリの実装などで使用するPythonの実装スタイルのガイダンスとなっており、一般のPython利用者も参考になる。たとえば、空白を入れる/入れない位置などのガイドがある。

# PEP8基準で正しい
if x == 4: print(x, y); x, y = y, x
# PEP8基準で間違い
if x == 4 : print(x , y) ; x , y = y , x

このPEP8はあくまで標準ライブラリ向けのガイドであり、一般に順守しなくてはならないものではない。PEP 8にも以下のような記載がある。

Many projects have their own coding style guidelines. In the event of any conflicts, such project-specific guides take precedence for that project.

多くのプロジェクトには独自のコーディングスタイルのガイドラインがある。PEP8と独自のガイドラインが矛盾するなら、プロジェクト固有のガイドラインが優先される。

https://peps.python.org/pep-0008/

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です