613
622

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

きれいなPythonプログラミングパむ゜ニックなコヌドを曞こうを読んでみた

613
Last updated at Posted at 2022-03-16

はじめに

日頃、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でファむル操䜜を行う際は以䞋の順序で凊理を蚘述したす。

  1. open()関数によるファむルオヌプン
  2. ファむルに察するなんらかの凊理
  3. 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の䞉項挔算子は他のほずんどのプログラミング蚀語ずは曞き方が異なりたす。

Javaの䞉項挔算子の曞き方
倉数 = 条件匏 ? 条件が成立したずきの倀 : 条件が成立しなかったずきの倀
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を孊ぶこずができるず思いたす。
䞀床手にずっお読んでみるこずをお勧めしたす

613
622
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
613
622

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?