Pythonの罠10選

皆さんはプログラムを組んでいて、あれ？思った通りの出力結果にならないという経験はありますでしょうか。単純なエラーであればいいですが、文法の理解が食い違っている場合、中々ミスに気づかない場合もあります。

今回はそんな知らないと沼にハマるかもしれないPythonの文法を10個ご紹介します。
１つでも新しい知見があると幸いです。

それではいってみましょう！

## YouTube

Pythonチュートリアル（公式ドキュメント）を使って基礎文法を解説しています。
チャンネル登録いただけると励みになります。

https://www.youtube.com/channel/UCl_e49nQhOMoYkQOo_4ew2Q



## 罠1：アイテム1のタプルもカンマが必要

タプルはカンマで区切られた値からなるので、アイテムが1つでもカンマが必要です。

忘れるとstr型だったり、int型になり、意外と気づかないです。。。

~~~py:In
single_tuple = ("hello",)
print(single_tuple)
print(type(single_tuple))
~~~
~~~:Out
('hello',)
<class 'tuple'>
~~~
※入力の際は丸括弧が無くてもタプルと認識されます。（式の一部として使っている場合は必要。）
~~~py:In
single_tuple = "hello",
print(single_tuple)
print(type(single_tuple))
~~~
~~~:Out
('hello',)
<class 'tuple'>
~~~
※アイテムが0のタプルはカンマはいりません。
~~~py:In
empty_tuple = ()
print(empty_tuple)
print(type(empty_tuple))
~~~
~~~:Out
()
<class 'tuple'>
~~~

## 罠2：//を小数で使うと小数が返ってくる

//を使うと商の整数部分のみを出力することができます。

~~~py:In
15//2
~~~
~~~:Out
7
~~~

小数で//を使うと整数部分は小数で返ってくるので注意。

~~~py:In
3.0//2
~~~
~~~:Out
1.0
~~~

## 罠3： 代入はデータをコピーしない（オブジェクトを名前に束縛するだけ）

代入はデータをコピーしません。オブジェクトを名前に束縛するだけです。

つまり、

a = 〇
b = a

とすると、〇というオブジェクトがaとbに束縛されます。

実際に確認すると、
~~~py:In
a = [5]
b = a
print(id(a))
print(id(b))
~~~
~~~:Out
133711149467264
133711149467264
~~~

aとbが同じオブジェクトを持っていることが分かります。

では、例としてbに変更を加えるとaはどうなるでしょう。

~~~py:In
a = [5]
b = a
b.append(9)

print(a, b)
~~~

~~~:Out
[5, 9] [5, 9]
~~~

両方とも変わっていることが分かります。
これはミュータブルオブジェクトの値を改変できるという性質により起こるものです。

ミュータブル・イミュータブルオブジェクトの例はこちら

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2638073/ea997b95-9056-6a63-65c2-7cc531d7493d.png">

画像にもありますが、イミュータブルオブジェクトは値を改変できません。

~~~py:In
num = 5
print(id(num))
num = 5 + 10
print(id(num))
~~~
~~~:Out
140511759729008
140511759729328
~~~

## 罠4：浅いコピーと深いコピーが存在する
Pythonには浅いコピーと深いコピーがあります。

以下、公式ドキュメントからの抜粋です。
>浅いコピー (shallow copy) は新たな複合オブジェクトを作成し、その後 (可能な限り) 元のオブジェクト中に見つかったオブジェクトに対する 参照 を挿入します。
深いコピー (deep copy) は新たな複合オブジェクトを作成し、その後元のオブジェクト中に見つかったオブジェクトの コピー を挿入します。

浅いコピーはオブジェクト中に見つかったオブジェクトに対しては参照を挿入する（コピーを取らず、同じオブジェクトを参照・共有する）ため、値がミュータブルの場合に影響があります。

浅いコピー（例：リストの中身がイミュータブルの場合）
~~~py:In
num_list=[1,2,3,4]
num_list_copy=num_list.copy()
num_list_copy[0]=9
print(num_list_copy)
print(num_list)
~~~
~~~:Out
[9, 2, 3, 4]
[1, 2, 3, 4]
~~~
浅いコピー（例：リストの中身がミュータブルの場合）
~~~py:In
num_list=[[1,2],[3,4]]
num_list_copy=num_list.copy()
num_list_copy[0][1]=9
print(num_list_copy)
print(num_list)
~~~
~~~:Out
[[1, 9], [3, 4]]
[[1, 9], [3, 4]]
~~~
コピー元まで変更されていることが分かります。
これを避けるために深いコピーを使います。

深いコピー
~~~py:In
import copy
num_list=[[1,2],[3,4]]
num_list_deepcopy=copy.deepcopy(num_list)
num_list_deepcopy[0][1]=9
print(num_list_deepcopy)
print(num_list)
~~~
~~~:Out
[[1, 9], [3, 4]]
[[1, 2], [3, 4]]
~~~




## 罠5：同じオブジェクトを参照する時としない時がある
Pythonにおいてミュータブルオブジェクトは同じ値を持ったとき、違うオブジェクトを参照することが保証されていますが、イミュータブルオブジェクトは同じオブジェクトを参照するときもあるし、そうでないときもあります。

つまり、a = 1、b = 1としたとき、a と b は値 1 を持つ同じオブジェクトを参照する時としないときがあります。
しかし、c = []、d = [] とすると、 c と d は異なったリストを参照することが保証されているということです。

詳細はこちらをご確認ください。

https://docs.python.org/ja/3.9/reference/datamodel.html

## 罠6：デフォルト値は1回しか評価されない
関数のデフォルト値は1回しか評価されません。
こちらもミュータブルオブジェクトの場合に影響を受けます。

デフォルト値は以下の`num_lists=[]`の部分です。関数を呼び出したときに引数を与えないとデフォルト値が使用されます。
~~~py:In
def test_func(num, num_lists=[]):
     num_lists.append(num)
     return num_lists
print(test_func(1))
print(test_func(2))
print(test_func(3))
~~~
1回しか評価されないので出力結果は以下のようになります。
~~~:Out
[1]
[1, 2]
[1, 2, 3]
~~~

これを避けたい場合は関数の中で空のリストを生成してください。

~~~py:In
def test_func(num, num_lists=None):
    if num_lists is None:
        num_lists = []
    num_lists.append(num)
    return num_lists
print(test_func(1))
print(test_func(2))
print(test_func(3))
~~~

~~~:Out
[1]
[2]
[3]
~~~

## 罠7：小数にはずれが生じる

小数の計算にはズレが生じる場合があります。これは2進数によって計算されたものが、10進数で出力される際に上手く変換できないためです。
よって、以下のような計算を行うとズレが生じます。

~~~py:In
d1 = 0.1
d2 = 0.2
print(d1 + d2)
~~~

~~~:Out
0.30000000000000004
~~~
Trueにもなりません。
~~~py:In
0.1 + 0.1 + 0.1 == 0.3
~~~
~~~:Out
False
~~~

**これに対してdecimalモジュールを使うと正確に計算ができます**

~~~py:In
from decimal import Decimal

d1 = Decimal('0.1')
d2 = Decimal('0.2')
d3 = Decimal('0.3')
print(d1 + d2 == d3)
~~~

~~~:Out
True
~~~

## 罠8：round関数は四捨五入ではない
Pythonには組み込み関数のround関数というものがあります。

公式ドキュメントより抜粋。
>round(number, ndigits=None)は、numberを小数点以下ndigits桁の精度で丸めた値を返します。ndigits が省略されたり、None だった場合、入力値に最も近い整数を返します。

**このround関数は四捨五入ではなく、偶数への丸め処理をします。**

偶数への丸めについて、以下Wikipediaより抜粋です。

>「偶数への丸め」（round to even）は、端数が0.5より小さいなら「切り捨て」、端数が0.5より大きいならば「切り上げ」、端数がちょうど0.5なら「切り捨て」と「切り上げ」のうち結果が偶数となる方へ丸める(つまり偶数+0.5なら「切り捨て」、奇数+0.5ならば「切り上げ」となる）

実際に確認すると、

~~~py:In
num1 = print(round(1.25, 1))
num2 = print(round(1.34, 1))
num3 = print(round(1.46, 1))
num4 = print(round(1.75, 1))
~~~
~~~:Out
1.2
1.3
1.5
1.8
~~~

このような出力結果になります。

ただし、ルール通りにならない場合もあります。
~~~py:In
num5 = print(round(1.15, 1))
~~~
~~~:Out
1.1
~~~

これは先ほども言ったように小数にズレが生じるためです。1.15と表示されていますが実際には1.149999999999999…となっており、1.1が出力されます。


## 罠9：クラスのスコープは注意が必要
5は出力されますか？
~~~py:In
class Test:
    x = 5
    def func(self):
        print(x)
t = Test()
t.func()
~~~
エラーになります。
~~~:Out
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-21-77d57046f44d> in <cell line: 6>()
      4         print(x)
      5 t = Test()
----> 6 t.func()

<ipython-input-21-77d57046f44d> in func(self)
      2     x = 5
      3     def func(self):
----> 4         print(x)
      5 t = Test()
      6 t.func()

NameError: name 'x' is not defined
~~~

一見エラーにならなそうですが、変数の宣言部分とメソッド部分は別のスコープになっており、メソッドから変数にアクセスすることができません。

上記コードはself を使うとアクセスできるようになります。

~~~py:In
class Test:
  x = 5
  def func(self):
    print(self.x)
t = Test()
t.func()
~~~
~~~:Out
5
~~~

以下PEP227からの抜粋です。

>クラス定義で名前バインディング操作が発生すると、結果のクラスオブジェクトに属性が作成されます。メソッドまたはメソッド内にネストされた関数でこの変数にアクセスするには、selfまたはクラス名を介して属性参照を使用する必要があります。

## 罠10：ローカル変数は静的な名前解決をしている

出力結果はどうなると思いますか？

~~~py:In
a = 10
def func():
    print(a)
    a = 20
func()
~~~
答えはこうなります。
~~~:Out
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-26-1b502f42ea82> in <cell line: 5>()
      3     print(a)
      4     a = 20
----> 5 func()

<ipython-input-26-1b502f42ea82> in func()
      1 a = 10
      2 def func():
----> 3     print(a)
      4     a = 20
      5 func()

UnboundLocalError: local variable 'a' referenced before assignment
~~~

10と出力されそうな気もしますがエラーになります。

これはPythonがスコープをテキスト上で決定しているためです。つまり、関数の中に変数aが存在するため、スコープが関数の外（グローバルスコープ）ではなく、関数の中（ローカルスコープ）に決定されます。そして、a = 20 が print(a)の後にきてしまっているのでエラーとなります。

:::note info
名前解決とは、プログラム上の変数名などの識別子が重複している場合に、どれを指し示しているのかを判定することを言います。
:::

クラスについて学びたい方はこちらもご活用ください。

https://youtube.com/playlist?list=PLHPmh91LsTgxKyHg5axHk8HVt97Hq76bh

## おわりに
知らないと沼にハマりそうな文法を10個紹介しました。とりあえずミュータブルとイミュータブルはきちんと理解しておいて損はないと思います。
参考になりましたら、是非いいね等で応援していただけると嬉しいです。

また、YouTubeにてPythonチュートリアル（公式ドキュメント）を使って基礎文法を解説しています。こちらもよろしければご活用ください！

https://www.youtube.com/channel/UCl_e49nQhOMoYkQOo_4ew2Q

**おすすめ記事**

https://arika-blog.com/python-threestep/


## 出典

PEP227 https://peps.python.org/pep-0227/
浅いコピーおよび深いコピー操作 https://docs.python.org/ja/3/library/copy.html
round関数 https://docs.python.org/ja/3/library/functions.html#round
端数処理 https://ja.wikipedia.org/wiki/%E7%AB%AF%E6%95%B0%E5%87%A6%E7%90%86