Pythonコーディングにおける各種Tipsまとめ

 本投稿は,Pythonでのコーディングにおいて,筆者が躓いたトラブルとその解決方法などをまとめたものです.事あるごとに本投稿へ追記してゆく予定です.
 使用したPythonのバージョンは,主にv3.10.11です.しかし所々で,別バージョンも用いています.これに対し,特にバージョンを明示しない場合はv3.10.11を用いた場合であり,それ以外のバージョンを用いた場合には明示的にバージョンを記述します.

目次

  1. Python 2.x系と3.x系とにおける型に関する挙動の違い
  2. 変数の型とバイト数
  3. リスト型,タプル型,セット型,辞書型
  4. 仮引数が可変長位置引数である場合
  5. 四捨五入/桁数の制御
  6. 浮動小数点の精度問題
  7. == 演算子での比較における型違いによる不一致
  8. PandasのDataFrame型行列から任意の値を抽出する方法
  9. PandasのDataFrame型に対する処理の高速化

1. Python 2.x系と3.x系とにおける型に関する挙動の違い

 Pythonは明示的に型を宣言せずに,格納される値に応じ自動で変数の型が決定される.これに対し,Pythonのバージョン2.x系と3.x系とでは,挙動が異なるため注意が必要である.

 具体例として,以下に2.x系のコードを例示する.2.x系では,整数値で割った場合その結果は整数値となり,「小数点以下を切り捨てる」処理として利用することもできた.

# Python v2.7.13
x = 3/2
print("xの型は{},値は{}".format(type(x), x))

y = 3/2.0
print("yの型は{},値は{}".format(type(y), y))

"""
# 実行結果
xの型は<type 'int'>,値は1
yの型は<type 'float'>,値は1.5
"""

 これに対し,以下に3.x系の下記コードを例示する.3.x系では,整数値で割っても結果は整数値にはならない.
 さらには,例えば3/3のような整数値のみを扱った割り算であり,それが割り切れる場合であっても,この結果は1ではなく1.0になる.

# Python v3.10.11
x = 3/2
print("xの型は{},値は{}".format(type(x), x))

y = 3/2.0
print("yの型は{},値は{}".format(type(y), y))

"""
# 実行結果
xの型は<type 'float'>,値は1.5
yの型は<type 'float'>,値は1.5
"""

 これらのことから,例えばPython 2.x系でこの「小数点以下を切り捨てる」処理を利用し運用していたプログラムを,Python 3.x系に移植する際は,意図した挙動にならないため,注意が必要である.
 敷衍すると,「型を意識しなくて良いPython」であるはずが,型を意識していないと意図した挙動を実現できない場合がPythonにはある.

2. 変数の型とバイト数

 主な型のバイト数に関し,表1に示す.

表1 主な型のバイト数
バイト数
int型■最低28バイト
 ●最大値においては,オーバーフローはなく,ハード依存
■Python v3.10.9では,値が0の時24バイト.v3.13.0ではいずれも28バイト
bool型■28バイト
■Python v3.10.9では,値がFalseの時24バイト.v3.13.0ではいずれも28バイト
str型■最低49バイト(値が無文字のとき)
 ●1バイト/半角1字の割合で,文字数に応じ49バイトに加算されてゆく
None型■16バイト

3. リスト型,タプル型,セット型,辞書型

val = [1, 2, 3]
  • 要素の追加(末尾):val.append(値)
  • 要素の追加(任意の位置):val.insert(要素番号, 値)
val = (1, 2, 3)
  • 要素の変更,追加,削除が不可能
  • for文の処理はリストより速い
  • Keyにハッシュ値を使用できる
val = {1, 1, 2, 2, 3, 3}
  • 要素の重複を認めない.そのため上記のように宣言した場合, val = {1, 2, 3} となる.
  • 要素の追加(末尾):val.add(値)
val = {"A":1, "B":2, "C":3}
  • 名称と値の紐付け.上記の場合,val[“A”]で1が出力される

4. 仮引数が可変長位置引数である場合

 下記コードのように,関数の仮引数部に ”*”(アスタリスク)が付記されている場合,可変長位置引数として扱われる.この際,長さが2以上のlist型変数を与えても,関数内では長さ1のtuple型になってしまう.

def sample_func(*args): # 関数と仮引数部
    print("仮引数argsの型:", type(args))
    print("仮引数argsの長さ:", len(args))

arg1 = 1
arg2 = 2
arg3 = 3
arg4 = 4
arg5 = 5
args_list = [arg1, arg2, arg3, arg4, arg5]
sample_func(args_list) # 関数の呼び出しと実引数部

"""
# 実行結果
仮引数argsの型: <class 'tuple'>
仮引数argsの長さ: 1
"""

 これに対し,実引数部にも ”*” を付記し要素ごとにアンパックすることで,関数内にて len() で求められる長さは,実引数部のlist型変数の長さと一致するようになる.

def sample_func(*args): # 関数と仮引数部
    print("仮引数argsの型:", type(args))
    print("仮引数argsの長さ:", len(args))

arg1 = 1
arg2 = 2
arg3 = 3
arg4 = 4
arg5 = 5
args_list = [arg1, arg2, arg3, arg4, arg5]
sample_func(*args_list) # 関数の呼び出しと実引数部

"""
# 実行結果
仮引数argsの型: <class 'tuple'>
仮引数argsの長さ: 5
"""

5. 四捨五入/桁数の制御

 下記コードのように,format()を用いて2.5を四捨五入をした場合,3ではなく2になる.これは,Pythonの四捨五入は「最も近い偶数値に丸め込む」処理がなされるためである.

arg = 2.5
result = format(arg, ".0f") # 小数点以下の桁が無くなるよう四捨五入
print("result: {}".format(result))

"""
# 実行結果
result: 2
"""

 これに対し,2.5を四捨五入したら3になることを期待するような一般的な四捨五入を行う場合,Python標準ライブラリであるdecimalを用いることで,実現できる.
 Decimal型変数は,その名称の通り10進数として数値を扱う型であり,小数点の演算において,精度の高い丸め込みが可能である.

from decimal import Decimal, ROUND_HALF_UP

arg = Decimal(2.5)
result = arg.quantize(Decimal("0"), rounding = ROUND_HALF_UP) # 小数点以下の桁が無くなるよう四捨五入
print("result: {}".format(result))

"""
# 実行結果
result: 3
"""

6. 浮動小数点の精度問題

第5章で扱った,より高精度なDecimal型を用いても,根本的な精度問題はついて回る.特定の値において,期待する値と異なる結果が出力される場合がある.例えば下記コードのように,2.55に対し四捨五入をすると,2.6ではなく2.5になる.

from decimal import Decimal, ROUND_HALF_UP

arg = Decimal(2.55)
result = arg.quantize(Decimal("0.1"), rounding = ROUND_HALF_UP) # 小数点第1位までの桁になるよう四捨五入
print("result: {}".format(result))

"""
# 実行結果
result: 2.5
"""

これに対し,一度str型にキャストし,数値ではなく数字として任意の値に落とし込むことで,誤差を回避できる.

from decimal import Decimal, ROUND_HALF_UP

arg = Decimal( str(2.55) ) # 一度str型にキャストし,数値ではなく数字として任意の値に落とし込む
result = arg.quantize(Decimal("0.1"), rounding = ROUND_HALF_UP) # 小数点第1位までの桁になるよう四捨五入
print("result: {}".format(result))

"""
# 実行結果
result: 2.6
"""

7. == 演算子での比較における型違いによる不一致

例えばDecimal型の小数値と,PandasのDataFrame型変数内の小数値(numpy・float64型)とでは,人間から見て数値としては同じでも,”==” 演算子で比較すると,異なる値として判定される.

from decimal import Decimal, ROUND_HALF_UP
import pandas as pd # 読み込んだ学習データを,DataFrame型(二次元配列)で扱うために使用

# Decimal型変数
arg = Decimal( str(2.55) )

#DataFrame型の変数
data = { # DataFrame用のサンプルデータを辞書で作成
    "fruits": ["Orange", "Banana", "Apple"],
    'Weight': [1.55, 2.55, 3.55]
}
df = pd.DataFrame(data)

print("decimal型変数:型は{},値は{}".format(type(arg), arg))
print("DataFramga内の値:型は{},値は{}".format(type(df.at[1, "Weight"]), df.at[1, "Weight"]))

if arg == df.at[1, "Weight"]:
    print("同じ値")
else:
    print("異なる値")

"""
# 実行結果
decimal型変数:型は<class 'decimal.Decimal'>,値は2.55
DataFramga内の値:型は<class 'numpy.float64'>,値は2.55
異なる値
"""

 これに対し,比較する変数同士の型を揃えると,”==” 演算子で比較した際の結果が期待した通りの挙動に修正される.
 DataFrame型変数は,主として膨大な量のデータを扱うため,その中の一部だけ型が異なる状態は避けたい.そこで本章では,単体であるDecimal型変数の方を,DataFrame型変数内の値(numpy・float64型)の型に合わせている.

from decimal import Decimal, ROUND_HALF_UP
import pandas as pd # 読み込んだ学習データを,DataFrame型(二次元配列)で扱うために使用
import numpy as np

# Decimal型変数
arg = Decimal( str(2.55) )

#DataFrame型の変数
data = { # DataFrame用のサンプルデータを辞書で作成
    "fruits": ["Orange", "Banana", "Apple"],
    'Weight': [1.55, 2.55, 3.55]
}
df = pd.DataFrame(data)

arg = np.float64(arg) # Decimal型変数を,numpy・float64型にキャストし,DataFrame内の数値と型を揃える
print("decimal型変数:型は{},値は{}".format(type(arg), arg))
print("DataFramga内の値:型は{},値は{}".format(type(df.at[1, "Weight"]), df.at[1, "Weight"]))

if arg == df.at[1, "Weight"]:
    print("同じ値")
else:
    print("異なる値")

"""
# 実行結果
decimal型変数:型は<class 'numpy.float64'>,値は2.55
DataFramga内の値:型は<class 'numpy.float64'>,値は2.55
同じ値
"""

 本章の内容では非標準ライブラリを扱っていることもあるが,「型を意識しなくて良いPython」であるはずが,型を意識していないと意図した挙動を実現できない場合がPythonにはある.

8. PandasのDataFrame型行列から任意の値を抽出する方法

  • aug = df[“hoge”]
    • ラベル ”hoge” の値だけではなく,列全体を抽出してしまい,ラベル名や値の型まで,変数augに代入される
  • aug = df.at[0, “hoge”]
    • ラベル “hoge” における0行目の値のみが,変数augに代入される
    • .atにおける1つめの引数(上記例では0)は,Pandasの行列において,自動生成されるIndex番号を指している
  • 複数行あるDataFrame型変数df_origがあったとする.このdf_origからIndex番号が10である1行を抽出し,それをdfとしたとする.この場合,dfは1行しか持っていなかったとしても,そのIndex番号は0ではなく,df_origでのIndex番号である10が代入されている
    • つまり df.at[0, “hoge”] を実行しても,dfにIndex番号が0である行は存在せず,エラーとなる
    • これに対し,df.reset_index( inplace = True, drop = True ) を実行することで,dfのIndex番号を0から連番にリセットすることで解決できる

9. PandasのDataFrame型に対する処理の高速化

 DataFrame型変数に対し,for文を用いて1行1行に処理を行うと,オーバーヘッド等によって処理が遅くなる.オーバーヘッドとは,システムやプログラムが目的している計算や処理を達成するために,その目的の処理以外に追加で発生する処理やリソース消費のことである.
 DataFrame型変数には,DataFrame全体に一括で処理を施すベクトル化した操作を行うことで,for分に比べ,処理の高速化が期待できる.PandasやNumpyにおいては低レベル言語で実装されており,ベクトル化による操作では内部的に効率的な処理が行える.
 ベクトル化による操作例として,以下にコードを例示する.

# ある列を一括でキャストする場合
df["val"] = df["val"].astype(str)

# Decimal型など基本型以外へのキャストは,astype()非対応であり,.apply()とlambaでキャスト
df["val"] = df["val"].apply( lamba x: Decimal(x) )