はじめに
日頃、Pythonを使う機会があるのですが、「もう一歩詳しくなり、中級者を目指したい!」という思いから、2022/2/15に発売された書籍「きれいなPythonプログラミング ~クリーンなコードを書くための最適な方法」を読みました。
特に、第6章にある「パイソニックなコードを書こう」が非常に勉強になったので自分なりの解釈/調査結果を含めてメモを残しておきます。
※解釈が誤っている箇所もあるかと思います。誤りがあればご指摘いただけると幸いです。
誤用の多い構文
Python以外の言語を使ったことがある人は、その言語と同じ考え方/手法でコードを書くかもしれません。
Pythonにおける標準的なアプローチを学ぶことで時間と労力を削減することができます。
ループ処理ではrange()
ではなくenumerate
を使う
慣習的にrange(len())
とインデックス番号でループを回すのは単純ですが、これは読みにくく理想的ではありません。
fruits = ["orange", "banana", "apple"]
for i in range(len(fruits)):
print(i, fruits[i])
#...出力結果...
0 orange
1 banana
2 apple
代わりにenumerate()
を使うとスッキリします。
fruits = ["orange", "banana", "apple"]
for i, fruit in enumerate(fruits):
print(i, fruit)
#...出力結果...
0 orange
1 banana
2 apple
なお、インデックス番号が不要でリストの要素だけ必要な場合は直接リストを走査することができます。
fruits = ["orange", "banana", "apple"]
for fruit in fruits:
print(fruit)
#...出力結果...
orange
banana
apple
open()
とclose()
ではなくwith文を使う
pythonでファイル操作を行う際は以下の順序で処理を記述します。
-
open()
関数によるファイルオープン - ファイルに対するなんらかの処理
-
close()
関数によるファイルクローズ
※open()
関数とclose()
関数は必ずセットで利用する。
しかし、close()
メソッドを記述し忘れてしまったり、以下のようにtryブロック内でエラーが発生してclose()
処理がスキップされてしまうとファイルが閉じられなくなってしまい、他のプログラムがファイルにアクセスできない状態となってしまいます。
try:
f = open("sample.txt", "w")
print(20 / 0) # <- ZeroDivisionError発生により、処理がexceptブロックに移る
f.close() # <- 実行されない
except:
print("エラー発生")
with
文を使えば、withブロックを抜けるときにcloseメソッドを自動的に呼び出してくれます。
with open("sample.txt", "w") as f:
f.write("hogehoge")
None
との比較は==
ではなくis
を使う
pythonで2つの値を比較する際は、等号演算子==
と恒等演算子is
を利用することができます。
等号演算子==
とは?
2つのオブジェクトの値(変数の値)を比較します。
# fruit_1とfruit_2に代入されている値は同じ
fruit_1 = "banana"
fruit_2 = "banana"
print(fruit_1 == fruit_2) # <- True
恒等演算子is
とは?
2つのオブジェクトの同一性を比較します。同一性とは、変数が持っている参照先(メモリのポインタ)が同じかどうかを意味します。
fruit_1 = "banana"
fruit_2 = fruit_1
# fruit_1とfruit_2は同じ参照先を持っているので同一オブジェクト
print(id(fruit_1)) # <- ID:22550541970544
print(id(fruit_2)) # <- ID:22550541970544
print(fruit_1 is fruit_2) # <- True
2つが演算子の違いがわかったところで、None
のID(参照先)ついて確認してみます。
fruit_1 = None
fruit_2 = None
print(id(fruit_1)) # <- ID:9484816
print(id(fruit_2)) # <- ID:9484816
print(fruit_1 is fruit_2) # <- True
None
はNoneTypeデータ型の唯一の値であるため、Pythonプログラム内ではNone
オブジェクトが1つだけ存在します。
そのため、変数の値がNoneに設定されている場合、上記の例のようにis None
の比較は常にTrue
と評価されます。
一方、等号演算子による== None
での比較の場合、以下のように比較ダンダーメソッドのオーバーロードを使うことで==
の挙動を変更することができます。
class BananaClass:
pass
class AppleClass:
# 比較ダンダーメソッドのオーバーロード
def __eq__(self, other):
if other is None:
return True
banana = BananaClass()
print(banana == None) # <- False
apple = AppleClass()
print(apple == None) # <- True
上記のように==
演算子をオーバーロードしている場合、正しくNoneか否かの判定ができない可能性があります。
よって結論としては、== None
ではなく、念のためis None
で比較するのが慣習となっています。
また、値がBoolean値の場合はis
演算子を使ってはいけません。
Booleanの場合は==
を使って判定する、もしくは、if apple:
や if not apple
のように記述するのが一般的です。
文字列のフォーマット
バックスラッシュ(\)が多い文字列の場合はraw文字列を使う
raw文字列とは、文字列の先頭にr
を付けたものであり、バックスラッシュをエスケープ文字として扱いません。
例えば、以下のようなWindowsのファイルパスを記述する場合は\\
と書いてバックスラッシュ文字をエスケープする必要があります。
# バックスラッシュ文字を文字列に含めたい場合は、\\と記述してエスケープする必要がある
print("C:\\Users\\admin\\Desktop\\sample.txt")
#...出力結果...
C:\Users\admin\Desktop\sample.txt
raw文字列を使えばエスケープ不要なのでコードが読みやすくなります。
# 文字列の前にr接頭辞を付与
print(r"C:\Users\admin\Desktop\sample.txt")
#...出力結果...
C:\Users\admin\Desktop\sample.txt
f-stringによる文字列のフォーマット
変数の値を含めた文字列を生成する手法として、元々は以下2つがありました。
# ① %sを使う
test1 = "apple"
test2 = "banana"
print("私は %s と %s が好きです。" % (test1, test2))
# ② format()メソッドを使う
test1 = "apple"
test2 = "banana"
print("私は {0} と {1} が好きです。".format(test1, test2))
python3.6以降では、f-string(フォーマットストリング) を使うことで文字列フォーマットが直感的で読みやすくなります。
前述のrow文字列が先頭にr
を付けるように、f-stringでは文字列の先頭にf
を付けます。
test1 = "apple"
test2 = "banana"
# 文字列の前にf接頭辞を付与
print(f"私は {test1} と {test2} が好きです。")
Listのコピーについて
以下のようにスライス構文を使うことで、既存のListから新しいListを作成することができます。
fruits = ["apple", "orange", "banana", "grape"]
# 既存のListから新しいListを作成
fruits_copy = fruits[2:] # <- 終了インデックスを省略すると、終了インデックスはデフォルトでListの最後になる。
print(fruits_copy)
#...出力結果...
# ['banana', 'grape']
スライス構文にて、開始インデックス/終了インデックスの両方を省略すると、実質的にリストのコピーを作成することと同じになります。
fruits = ["apple", "orange", "banana", "grape"]
# 既存のListから新しいListを作成
fruits_copy = fruits[:] # <- 開始インデックス/終了インデックスを省略
print(fruits_copy)
#...出力結果...
# ['apple', 'orange', 'banana', 'grape']
# (※1)2つのListは異なるオブジェクトであることがわかる
print(id(fruits) == id(fruits_copy)) # <- False
(※1)fruits_copy = fruits[:]
の行はfruits
の内容をコピーしますが、fruits_copy = fruits
はListの参照 をコピーする点に注意です。
ただ、[:]
によるListのコピーはあまり見た目がよくない(わかりづらい)ため、以下のようにcopy
モジュールのcopy()関数
を使うのがパイソニックなコードです。
import copy
fruits = ["apple", "orange", "banana", "grape"]
# 既存のListから新しいListを作成
fruits_copy = copy.copy(fruits) # <- copy()関数を使うとコピー処理を実行していることが一目瞭然
print(fruits_copy)
#...出力結果...
# ['apple', 'orange', 'banana', 'grape']
# 2つのListは異なるオブジェクトであることがわかる
print(id(fruits) == id(fruits_copy)) # <- False
浅いコピーと深いコピー
上記で紹介されている[:]
およびcopy.copy()
を使ったListのコピーはいずれも浅いコピーになります。
浅いコピーの場合、以下のように多次元配列のように入れ子になったオブジェクトはうまくコピーできません。
import copy
fruits = [["banana", "apple"], ["orange", "grape"]]
# 浅いコピーを実行
fruits_copy = copy.copy(fruits)
# 2次元リストの"banana"を"Lime"に変更する
fruits_copy[0][0] = "Lime"
# コピー元の「fruits」も"Lime"になっている
print(fruits) # [['Lime', 'apple'], ['orange', 'grape']]
print(fruits_copy) # [['Lime', 'apple'], ['orange', 'grape']]
# 内包されているオブジェクトは同じ
print(id(fruits[0])) # ID:22461722900352
print(id(fruits_copy[0])) # ID:22461722900352
上記のように、多次元配列に対して浅いコピーを行うと要素が同じインスタンスとなってしまいます。(浅いコピーは1次元のリストに対してのみ有効)
多次元配列を独立した異なるオブジェクトとしてコピーする場合は、深いコピーを行う必要があります。
深いコピーはcopy.deepcopy()
を使います。
import copy
fruits = [["banana", "apple"], ["orange", "grape"]]
# 浅いコピーを実行
fruits_copy = copy.deepcopy(fruits)
# 2次元リストの"banana"を"Lime"に変更する
fruits_copy[0][0] = "Lime"
# コピー元は変わらない
print(fruits) # [['banana', 'apple'], ['orange', 'grape']]
print(fruits_copy) # [['Lime', 'apple'], ['orange', 'grape']]
# 内包されているオブジェクトは異なる(同じインスタンスではない)
print(id(fruits[0])) # ID:22601618573056
print(id(fruits_copy[0])) # ID:22601618667328
@shiracamusさん、ご指摘ありがとうございます。
パイソニックな辞書の使い方
get()
を使う
Pythonでは、存在しない辞書のキーにアクセスした場合、KeyError
が発生します。
try-catch等でエラーハンドリングを実装していないとそこで処理が強制終了されてしまいます。
fruits = {"banana" : 1}
print(f"私はorangeを{fruits['orange']}つ持っています") # <- KeyError: 'orange'
これを回避するため、以下のようにif
文を使って特定の値が辞書に含まれているか確認することがありますが、これはパイソニックではありません。
fruits = {"banana" : 1}
if "orange" in fruits: # <- キーに"orange"が含まれているかを確認する
print(fruits["orange"])
else:
print("not orange")
上記のように、キーが存在しないパターンが頻繁に発生することから、Pythonの辞書にはget()
メソッドが用意されています。
get()
メソッドでは、キーが存在しない場合に返すデフォルト値を指定することができます。
fruits = {"banana" : 1}
print(f"私はorangeを{fruits.get('orange', 2)}つ持っています")
get()
メソッドでは、まずget()
の第1引数の値(orange
)が辞書(fruits
)内のキーに存在するかどうかをチェックします。
存在する場合、辞書内の第1引数の値(orange
)に対応する値を返します。
存在しない場合、第2引数に指定された値(2
)を返します。
これにより、if-else文で事前チェックするよりも短く読みやすくなります。
setdefault()
を使う
get()
メソッドと同様にsetdefault()
メソッドでもキーが存在しなかった場合にデフォルト値を返すことができます。
fruits = {}
print(fruits.setdefault("banana", 1)) # <- "banana"は辞書内に存在しないので第2引数の"1"を返す
ここでget()
メソッドとの違いですが、キーが存在しなかった場合、setdefault()
内で指定したkey-valueが新しい要素として辞書にセットされます。
fruits = {}
# get()メソッドの場合
print(fruits.get("banana", 1)) # <- 1
print(fruits) # <- {}
# setdefault()メソッドの場合
print(fruits.setdefault("banana", 1)) # <- 1
print(fruits) # <- {'banana': 1}
辞書にキーが存在するか否かをチェックし、キーがない場合はデフォルト値を設定するような処理がある場合はsetdefault()
で代用してみてください。
collections.defaultdict
を使う
collections.defaultdict
クラスを使用すると、KeyErrorを完全になくすことができます。
defaultdict
を使うためには、標準collectionsライブラリから別途importする必要があります。
from collections import defaultdict
defaultdict
にデフォルト値で使用するデータ型を渡すことで、デフォルトの辞書を作成します。
例えば、以下のようにdefaultdict
にintを渡すことで、キーが存在しない場合のデフォルト値として0
を返す辞書を作ることができます。
from collections import defaultdict
num_dict = defaultdict(int) # <- intを渡す
print(num_dict) # <- defaultdict(<class 'int'>, {})
num_dict["banana"] += 1 # <- キー"banana"に対する値をセットする必要がない
print(num_dict["banana"]) # <= 1
num_dict["orange"] # <- キー"orange"に対する値をセットする必要がない
# "orange"のvalueにデフォルト値"0"がセットされている
print(num_dict["orange"]) # <- 0
なお、defaultdict
にはint以外にもlistやdict、boolを渡すことができます。
考えられるすべてのキーに対してデフォルト値が必要な場合は、通常の辞書を使用して逐一setdefault()
メソッドを呼び出すよりも、collections.defaultdict
を使用する方がはるかに簡単です。
switch文の代わりに辞書を使う
Java等の言語にはswitch文がありますが、Pythonにはswitch文がありません。
⇒Python3.10からmatch文が使えるようになったようです。
Pythonでswitch文と同様の処理を行う場合はif-elif-else文を使って以下のように記述することがあります。
if fruits == "apple":
color = "red"
elif fruits == "banana":
color = "yellow"
elif fruits == "lime":
color = "green"
else:
color = "no color"
上記コードはパイソニックでないとまでは言えませんが、少し冗長です。
そこで、if-elif-else文の代わりに辞書を使って以下のように記述するのがパイソニックです。
color= {
"apple" : "red",
"banana" : "yellow",
"lime" : "green"
}.get(fruits, "no color")
上記コードは代入文が1つあるだけで非常に簡潔です。
get()
の第1引数に指定したキーの値を返しますが、キーが存在しない場合は第2引数にセットしたデフォルト値を返します。
しかしながら簡潔になる一方、コードが直感的ではなく読みづらくなることがあります。この方法を使うかどうかは検討の余地があります。
Pythonの三項演算子
条件に基づいて2つの値のどちらかを取得するような場合、通常はif-else
を使います。これはパイソニックです。
is_apple = True
message = ""
if is_apple:
message = "これはappleです"
else:
message = "これはappleではありません"
print(message) # <- これはappleです
三項演算子を使うことで上記パターンに当てはまるコードを1行で簡潔に表現できるのですが、Pythonの三項演算子は他のほとんどのプログラミング言語とは書き方が異なります。
変数 = 条件式 ? 条件が成立したときの値 : 条件が成立しなかったときの値
変数 = 条件が成立したときの値 if 条件式 else 条件が成立しなかったときの値
上記のように、Pythonではif
とelse
の間に条件式を挟む独特な配置で実装されています。具体的な記述例は以下です。
is_apple = True
true_value = "これはappleです"
false_value = "これはappleではありません"
message = true_value if is_apple else false_value
print(message) # <- これはappleです
なお、以下のように記述することで他のプログラミング言語のような擬似三項演算子を作ることができるのですが、これはパイソニックではありません。
is_apple = True
true_value = "これはappleです"
false_value = "これはappleではありません"
message = is_apple and true_value or false_value
print(message) # <- これはappleです
パイソニックでない理由として、上記の擬似三項演算子には微妙なバグがあります。
それは、以下のようにtrue_value
が偽の場合(0
, False
, None
, 空白文字など)、条件式(is_apple
)がTrue
であってもfalse_value
と評価されます。
is_apple = True
# 例として空白文字をセット
true_value = ""
false_value = "これはappleではありません"
message = is_apple and true_value or false_value
print(message) # <- これはappleではありません
よって、擬似三項演算子は使わないようにしましょう。
上記を踏まえた結論として、三項演算子はパイソニックというほどでもないですが、パイソニックでないというわけでもありません。
使用する場合は、条件式の中に条件式を入れるといったネスト構造にしないようにしましょう。
age = 30
age_range = "child" if age < 13 else "teenager" if age >= 13 and age < 18 else "adult"
print(age_range) # <- adult
上記のように、三項演算子のネストはコードの可読性が悪くなり、読んでいてイライラします。
変数の扱い
変数の値を確認したり変更したりすることはよくありますが、Pythonではこれを行う方法がいくつかあります。
比較演算子の連結
特定の数値が範囲内に収まっているかどうかを確認する場合は、以下のように比較演算子とand
演算子を使います。
num = 50
if 30 < num and num < 100:
しかし、Pythonでは比較演算子を連鎖させることができるため、and
演算子を使う必要がありません。
num = 50
if 30 < num < 100:
代入演算子の連結
代入演算子=
も連鎖させることが可能です。
1行のコードの中で複数の変数に同じ値を代入することができます。
banana = apple = orange = "fruits"
print(banana, apple, orange)
#...出力結果...
fruits fruits fruits
上記3つの変数がすべて同じかどうかを確認するにはand
演算子を使ってもよいですが、==
を連鎖する方法もあります。
banana = apple = orange = "fruits"
print(banana == apple == orange == "fruits")
#...出力結果...
True
演算子の連鎖はちょっとしたことですが便利なコード短縮方法です。
注意
banana = apple = orange = ["banana", "apple"]
上記のように連鎖を使ってリスト等のミュータブルな値を代入した場合、同じインスタンスとなってしまうため注意が必要です。
詳しくは本記事のコメント欄、@shiracamusさんの解説をご参照ください。
変数の値が複数の値のどれと等しいかを調べる
変数の値が複数候補のどれと等しいか調べることがあります。この場合、or
演算子使って以下のように書くこともできますが、パイソニックではありません。
if fruits == "banana" or fruits == "apple" or fruits == "orange": # <- # "fruits ==" を複数記述する必要があり冗長
そこで、以下のように比較対象の値をタプルにまとめ、そのタプル内に変数の値が存在するかどうかをin
演算子でチェックするのがパイソニックです。
fruits = "banana"
print(fruits in ("apple", "orange", "banana"))
#...出力結果...
True
さいごに
本書での学習を通じて今まで曖昧だった使い方をしっかり理解することができました。
本書では他にも、
- Pythonのよくある落とし穴
- Pythonの要注意コード
- 良い関数の書き方
など、Pythonの基礎的な使い方をある程度理解した人が中級者へステップアップするためのTipsを学ぶことができると思います。
一度手にとって読んでみることをお勧めします!