今回はPythonの闇挙動についてです。
解説動画はこちら
Pythonの闇挙動10選
Pythonにはその独特の仕様があり
コードの書き方で思わぬ挙動を引き起こします。
そんな
変てこりんな挙動を10選んでみました。
1.is演算子の罠
(256 == 256 はTrueでも 257 == 257 はFalse!?)
2. ミュータブルなデフォルト引数の罠
イミュータブル :
作成後にその状態を変えることのできないオブジェクトのこと
デフォルト引数に空のリストを設定した
関数を作ります。
これを実行してみると
3. += と + の違い(リストの参照問題)
リストを代入して新しいリストを作って
元のリストに要素を加えると...
4. sorted() と sort() の違いに注意
リストの並び替えを行う方法は大きく2種類あります
sorted(a) は 新しいリストを返す ため
a 自体は変更されない
a.sort() は リストを直接変更する ため
a の内容が書き換わる
5. 0.1 + 0.2 == 0.3 が False になる!?
Pythonで小数点の値を比較すると...
0.30000000000000004
7.sum() で文字列を合計するとエラーになるのに max() は動く!?
8.range() の「スタート」には 0 が入るのに slice() には入らない!?
9.set の順番がランダムに見える!?
文字列のデータをSET型のデータにしてみると
10. dict.keys() の結果は list じゃない!?
< cla ss 'dict_keys'>
['a', 'b']
解説動画はこちら
Pythonの闇挙動10選
Pythonにはその独特の仕様があり
コードの書き方で思わぬ挙動を引き起こします。
そんな
変てこりんな挙動を10選んでみました。
1.is演算子の罠
(256 == 256 はTrueでも 257 == 257 はFalse!?)
a = 256 b = 256 print(a is b) # True c = 257 d = 257 print(c is d) # True or False !?!?
True
False
Pythonは 小さい整数(-5~256)のオブジェクトを
キャッシュ する最適化を行っています。
キャッシュ する最適化を行っています。
そのため、a と b は同じオブジェクトを参照しており
is が True になります。
is が True になります。
しかし、257 はキャッシュ対象外なので
c と d は別のオブジェクトとなり is が False になります。
c と d は別のオブジェクトとなり is が False になります。
対策:
値の比較には is ではなく == を使うべき
2. ミュータブルなデフォルト引数の罠
ミュータブル :
作成後にもその状態を変えることの出来るオブジェクトのこと
作成後にもその状態を変えることの出来るオブジェクトのこと
イミュータブル :
作成後にその状態を変えることのできないオブジェクトのこと
デフォルト引数に空のリストを設定した
関数を作ります。
これを実行してみると
# デフォルト引数に空のリストを設定した関数 def append_to_list(value, my_list=[]): my_list.append(value) return my_list print(append_to_list(1)) # [1] print(append_to_list(2)) # [1, 2] (???) print(append_to_list(3)) # [1, 2, 3] (!!!)
[1]
[1, 2]
[1, 2, 3]
Pythonの関数のデフォルト引数は
関数が定義されたときに評価 され
一度作られたオブジェクトが 再利用 されます。
関数が定義されたときに評価 され
一度作られたオブジェクトが 再利用 されます。
そのため my_list は毎回新しくなるわけではなく
前回の呼び出し時の変更が次の呼び出しに影響を与えます。
前回の呼び出し時の変更が次の呼び出しに影響を与えます。
対策:
デフォルト引数にはミュータブルな値
(list, dict など)を使わず、None を使うようにする
(list, dict など)を使わず、None を使うようにする
3. += と + の違い(リストの参照問題)
リストを代入して新しいリストを作って
元のリストに要素を加えると...
# a のリストをコピーして新しいリスト b を作ってみよう a = [1, 2, 3] b = a a += [4, 5] print(a) # [1, 2, 3, 4, 5] print(b) # [1, 2, 3] or [1, 2, 3, 4, 5] ?!?
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
a += [4, 5] は a.extend([4, 5]) のように
リスト自体を変更 するため
リスト自体を変更 するため
b も同じオブジェクトを参照しているので
b にも変更が反映される
b にも変更が反映される
対策:
新しいリストを作成したい場合は + を使う
a = [1, 2, 3] b = a a = a + [4, 5] # 新しいリストが作られる print(a) # [1, 2, 3, 4, 5] print(b) # [1, 2, 3] (bは変更されない)
[[1, 2, 3, 4, 5]
[1, 2, 3]
4. sorted() と sort() の違いに注意
リストの並び替えを行う方法は大きく2種類あります
a = [3, 1, 2] print(sorted(a)) # [1, 2, 3] print(a) # [1, 2, 3] or [3, 1, 2] (???) a.sort() print(a) # [1, 2, 3]
[1, 2, 3]
[3, 1, 2]
[1, 2, 3]
sorted(a) は 新しいリストを返す ため
a 自体は変更されない
a.sort() は リストを直接変更する ため
a の内容が書き換わる
対策:
リストをそのまま並び替えたい場合は リスト.sort()
元のリストを残したい場合は sorted(リスト) を使う
5. 0.1 + 0.2 == 0.3 が False になる!?
Pythonで小数点の値を比較すると...
print(0.1 + 0.2 == 0.3) # False (???) print(0.1 + 0.2) # 0.3 ?!?!False
0.30000000000000004
浮動小数点の計算誤差が原因。
0.1 や 0.2 は 2進数で正確に表現できないため
足すと誤差が生じる。
0.1 や 0.2 は 2進数で正確に表現できないため
足すと誤差が生じる。
対策:
誤差を考慮して math.isclose(値 , 比較値) を使う
6."" or "Hello" は "Hello" なのに "0" or "Hello" は "0" になる!?import math print(math.isclose(0.1 + 0.2, 0.3)) # TrueTrue
print("" or "Hello") # Hello print("0" or "Hello") # 0 or "Hello" (???)
Hello
0
or は 最初に「真」と評価された値を返すため
""(空文字)は False なので "Hello" が返る
""(空文字)は False なので "Hello" が返る
しかし "0" は 非空の文字列であり True と評価される ため
そのまま "0" が返る。
そのまま "0" が返る。
対策:
文字列の比較をする場合は bool(value) を明示的に使う
7.sum() で文字列を合計するとエラーになるのに max() は動く!?
print(max(["a", "b", "c"])) # cc
print(sum(["a", "b", "c"])) # TypeError (???)TypeError
sum() は 数値の合計を計算する関数 なので
文字列を足そうとするとエラーになる
文字列を足そうとするとエラーになる
max() は 「大きい方を返す」関数 なので
辞書順で "c" を返す
辞書順で "c" を返す
対策:
文字列の連結には sum() ではなく
"".join() を使う
"".join() を使う
8.range() の「スタート」には 0 が入るのに slice() には入らない!?
print(list(range(5))) # [0, 1, 2, 3, 4] print("hello"[slice(5)]) # hello (???) print("hello"[slice(None, 5)]) # hello (???) print("hello"[slice(5, None)]) # (空文字)
[0, 1, 2, 3, 4]
hello
hello
range(5) は デフォルトの開始値が 0 になるので
[0, 1, 2, 3, 4] になる
[0, 1, 2, 3, 4] になる
slice(5) は デフォルトの開始値が None になり
slice(None, 5) と解釈される
slice(None, 5) と解釈される
slice(5, None) は 5 以降の文字を取得しようとするが
範囲外なので空文字になる
範囲外なので空文字になる
9.set の順番がランダムに見える!?
文字列のデータをSET型のデータにしてみると
s = set("hello world") print(s){'r', 'd', 'o', 'l', 'h', 'e', 'w', ' '}
set は 順序を保持しないデータ構造 のため
出力される順番は内部のハッシュ値によって変わる
出力される順番は内部のハッシュ値によって変わる
対策:
順番を維持したいなら set ではなく
OrderedDict や list を使う
OrderedDict や list を使う
from collections import OrderedDict ordered_set = "".join(OrderedDict.fromkeys("hello world")) print(ordered_set)helo wrd
10. dict.keys() の結果は list じゃない!?
d = {"a": 1, "b": 2} print(d.keys()) # dict_keys(['a', 'b']) print(type(d.keys())) # print(list(d.keys())) # ['a', 'b'] (明示的にリスト化)dict_keys(['a', 'b'])
< cla ss 'dict_keys'>
['a', 'b']
d.keys() は 「ビューオブジェクト」 であり
リストではない
リストではない
そのため、リストと同じように扱えないことがある
対策:
リストとして扱いたい場合は
list(d.keys()) を使う
list(d.keys()) を使う
まとめ
リストの操作や比較演算子周りには
意外と知られていない挙動が多い
意外と知られていない挙動が多い
1文字違うだけで別の挙動になったり
操作の順番で意図しない結果になったりする
操作の順番で意図しない結果になったりする
細かい仕様を把握する必要ありますねー
ということで
今回はバグを生みやすい
Pythonの変な挙動10選についてでした。
それでは。
ということで
今回はバグを生みやすい
Pythonの変な挙動10選についてでした。
それでは。
コメントする