Pythonまとめ(4.その他の文・関数)

その他の文、関数

open

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

assert

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

assert condition[, message]

assertは関数ではなく式である。そのためassert(...)のように()をつけない。つけてしまうとSyntaxWarningとして警告されるが、エラーにはならない。次の例はassertを関数のように使用している。これはconditionに(False,"Error")と指定したと解釈される。空でないタプルは常にTrueであるため、このassertは常に成功し、AssertionErrorが発生することはない。

assert(False, "Error&quot")
(out)<stdin>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?

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

def check_input(value):
(con)    assert isinstance(value, int|float) and value > 0, \
(con)        f"NUMBER should be a positive number, but {value!r} was given.";
(con)
check_input(5.4)
check_input(-1)
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)  File "<stdin>", line 2, in check_input
(out)AssertionError: NUMBER should be a positive number, but -1 was given.
check_input("文字")
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)  File "<stdin>", line 2, in check_input
(out)AssertionError: NUMBER should be a positive number, but '文字' 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():
(con)    # 試し割りする値のジェネレータ
(con)    yield from (2,3,5)
(con)    value = 7
(con)    while True:
(con)        for d in (2,2,2,4):
(con)            yield value
(con)            value += d
(con)
def pf(number:int):
(con)    # 試し割りで素因数分解する
(con)    primelist = []
(con)    for v in factorlist():
(con)        if number <= v: break
(con)        while number%v == 0:
(con)            number /= v
(con)            primelist.append(v)
(con)    return primelist
(con)
pf(100)
(out)[2, 2, 5, 5]
pf(987945)
(out)[3, 5, 7, 97, 97]

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

パターン1:引数の値のチェック

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

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

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

pf("moji")
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)  File "<stdin>", line 5, in pf
(out)TypeError: '<=' not supported between instances of 'str' and 'int'

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

入力値をチェックするなら以下のようなコードになるだろう。この実装であれば、不正な値が渡された時点でValueErrorが送られ、正整数が期待されているのに文字列が渡されたのが分かるようになる。

def pf_with_inputcheck(number:int):
(con)    if not (isinstance(number, int) and number > 1):
(con)        raise ValueError(f"Number must be a integer larger than 1, but was {number!r}.")
(con)    primelist = []
(con)    for v in factorlist():
(con)        if number <= v: break
(con)        while number%v == 0:
(con)            number /= v
(con)            primelist.append(v)
(con)    return primelist
(con)
pf_with_inputcheck("moji")
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)  File "<stdin>", line 3, in pf_with_inputcheck
(out)ValueError: Number must be a integer larger than 1, but was 'moji'.

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

def pf_with_inputassert(number:int):
(con)    assert isinstance(number, int) and number > 1, \
(con)           f"Number must be a integer larger than 1, but was {number!r}."
(con)    primelist = []
(con)    currentvalue = number
(con)    while currentvalue > 1:
(con)        for i in range(2,currentvalue):
(con)            if currentvalue%i == 0:
(con)                currentvalue //= i
(con)                primelist.append(i)
(con)                break
(con)
(con)    return primelist
(con)
pf_with_inputassert("moji")
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)  File "<stdin>", line 2, in pf_with_inputassert
(out)AssertionError: Number must be a integer larger than 1, but was 'moji'.

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

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

パターン2:処理結果のチェック

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

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

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

from functools import reduce

def pf_with_outputassert(number:int):
(con)    # 試し割りで素因数分解する
(con)    originalnumber = number
(con)    primelist = []
(con)    for v in factorlist():
(con)        if number <= v: break
(con)        while number%v == 0:
(con)            number /= v
(con)            primelist.append(v)
(con)
(con)    assert (result := reduce(lambda x,y: x*y, primelist))==originalnumber, \
(con)        f"Prime factorization error. {originalnumber} != {"*".join(map(str,primelist))} = {result}."
(con)    return primelist
(con)
pf_with_outputassert(20)
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)  File "<stdin>", line 11, in pf_with_outputassert
(out)AssertionError: Prime factorization error. 20 != 2*2 = 4.

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

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

def factorlist():
(con)    # 試し割りする値のジェネレータ
(con)    yield from (2,3,5)
(con)    value = 7
(con)    while True:
(con)        for d in (2,2,2,4):
(con)            yield value
(con)            value += d
(con)
def pf_fixed(number:int):
(con)    # 試し割りで素因数分解する
(con)    originalnumber = number
(con)    primelist = []
(con)    for v in factorlist():
(con)        if number == 1: break
(con)        while number%v == 0:
(con)            number /= v
(con)            primelist.append(v)
(con)
(con)    return primelist
(con)
pf_fixed(20)
(out)[2, 2, 5]
パターン3:処理途中のチェック

長い処理では途中で条件を確認することで、どの部分で異常が発生したか発見しやすくなるだろう。例として素因数分解には時間がかかることから、値を100,000未満に限定するように改変してみよう。

def factorlist_bug():
(con)    yield 3
(con)    value = 5
(con)    while value < 10000/2:
(con)        yield value
(con)        value += 2
(con)
def pf_bug(number:int):
(con)    if not( isinstance(number,int) and 1 < number < 100000 ):
(con)        raise ValueError(f"Number must be integer and from 2 to 99999. Got {number}({type(number)}).")
(con)    originalnumber = number
(con)    primelist = []
(con)    for v in factorlist_bug():
(con)        if number == 1: break
(con)        while number%v == 0:
(con)            number /= v
(con)            primelist.append(v)
(con)    else:
(con)        assert False, "You should not encounter this message. " \
(con)            f"{originalnumber=}, {number=}, {v=}, {primelist=}"
(con)    return primelist
(con)
pf_bug(67432)
(out)[2, 2, 2, 8429]
pf_bug(67434)
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)  File "<stdin>", line 12, in pf_bug
(out)AssertionError: You should not encounter this message. originalnumber=67434, number=11239.0, v=9999, primelist=[2, 3]

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

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

del

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

x = 5
del x
x
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)NameError: name 'x' is not defined

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

data = [0,1,2,3]
del data[2]
data
(out)[0, 1, 3]

global

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

x = 5 # グローバル変数

def method1(): # ローカルで定義していない変数xを参照
(con)    return x
(con)
def method2(): # グローバルと重複している変数xに代入
(con)    x = 7
(con)    return x
(con)
def method3(): # global宣言の上で、変数xに代入
(con)    global x
(con)    x = 7
(con)    return x
(con)
method1()
(out)5
x
(out)5

method2()
(out)7
x
(out)5

method3()
(out)7
x
(out)7

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')
(out)9
exec('4+5')

with

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

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

with open('test.txt', mode='r') as f:
(con)    print( "with内部" )
(con)    print( f.readline() )
(con)
(out)with内部
(out)
print( "with外部" )
(out)with外部
print( f.readline() )
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)ValueError: I/O operation on closed file.

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

class Test():
(con)    def __enter__(self):
(con)        print("enter")
(con)        return self
(con)
(con)    def __exit__(self, exc_type, exc_value, traceback): # with内部で発生した例外を受け取る引数がある
(con)        print("exit")
(con)
with Test() as e:
(con)    print('with')
(con)
(out)enter
(out)with
(out)exit

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

from contextlib import contextmanager

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

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

用途構文サンプル
標準出力のリダイレクト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")
(out)'abc-10-10'
# キーワードで指定する方法
"{number}-{name}-{number}".format(number=10,name="abc")
(out)'10-abc-10'

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

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

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

# 次はすべての順番指定が省略されていて、1対1対応しているのでOK。キーワード指定の{name}と{number}は省略には影響しない。
"{}-{name}-{number}-{}".format(100,200,number=10,name="abc")
(out)'100-abc-10-200'
# 次は順番指定が省略されているものといないものが混在しているのでエラー
"{}-{1}".format(100,200,number=10,name="abc")
(out)Traceback (most recent call last):
(out)  File "<stdin>", line 1, in <module>
(out)ValueError: cannot switch from automatic field numbering to manual field specification

変換フラグ

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

変換フラグ動作
!sstr()で文字列に変換
!rrepr()で文字列に変換
!aascii()で文字列に変換
import datetime

value=datetime.datetime.now()
"{0}".format(value)
(out)'2024-12-22 23:16:46.101459'
"{0!s}".format(value)
(out)'2024-12-22 23:16:46.101459'
"{0!r}".format(value)
(out)'datetime.datetime(2024, 12, 22, 23, 16, 46, 101459)'
"{0!a}".format(value)
(out)'datetime.datetime(2024, 12, 22, 23, 16, 46, 101459)'

書式指定子

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

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

:[[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を指定すると桁区切りや小数点記号が変わることがある。以下にサンプルを示す。

import locale

valueint  =1000000
valuefloat=1.11111

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

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

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

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

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

import datetime

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

"{:%Y%m%d}".format(formattest)
(out)'20241222'

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/

コメントを残す

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