Pythonのハマりやすいポイントを学ぶ問題集を作ってみた - 第一弾

# はじめに

Python の基礎知識やハマりやすいポイントを学ぶ問題集（とりあえず第一弾）を作ってみました。Python 初学者や開発者向けに作成していますので、ぜひご活用ください。

- 注意事項
  - 本問題集は Python3.11 を基準に作成されています。
  - Python の基礎知識やハマりやすいポイントを全て網羅しているわけではありません。Python の学習や開発においては、他の情報源や公式ドキュメントを併せて活用することをお勧めします。
  - 問題の実行結果がエラーやベストプラクティスでない場合もありますので、解説を通じて理解を深めることをおすすめします。
  - 不明点や疑問点がある場合は、実際にコードを実行したり、公式ドキュメント等を参照することをおすすめします。

2023/06/11追加　第二弾も投稿しました！

https://qiita.com/to-fmak/items/81f7056fbaf0071d1510

# 問題 1

次のコードの実行結果を選択してください。

```python
a = 'Hello'
print(a[5])
```

```:選択肢
A: IndexError
B: l
C: o
D: None
```

<details><summary>解答</summary>
正解：A

解説：Python では、（str や list 等の）インデックスは`0`から始まります。a の最大有効インデックスは 4 までで、a[5]となるとインデックスが範囲外となります。そのため、IndexError が発生します。

</details>

# 問題 2

次のコードの実行結果を選択してください。

```python
a = 'Hello, World!'
print(a[::-2])
```

```:選択肢
A: Hlo ol!
B: Helo, World!
C: !lo olH
D: H,loWr
```

<details><summary>解答</summary>
正解：C

解説：この問題では、`スライス操作`における`ステップ数の指定`による文字列の取得を行おうとしています。

- スライス操作では、「[start:stop:step]」のようにステップ数を指定できますが、ステップ数を省略するとデフォルトで 1 となり、要素を順番に取得します。ステップ数を指定することで、指定した数だけ要素を飛ばしながら取得することができます。また、`ステップ数を負の値にすると、文字列を逆順に選択`することができます。
- スライス操作では開始位置・終了位置・ステップ数のいずれも省略することができます。省略した場合、開始位置は先頭、終了位置は末尾、ステップ数は 1 として扱われます。a[::-2]は文字列 a を末尾から先頭まで、2 つごとに逆順に要素を取得します。

</details>

# 問題 3

次のコードの実行結果を選択してください。

```python
a = 'Hello, World!'
a[0] = 'h'
print(a)
```

```:選択肢
A: TypeError
B: h
C: hello, World!
D: Hello, World!
```

<details><summary>解答</summary>
正解：A

解説：文字列（str）は Python において`不変（immutable）なオブジェクト`で、一度作成された文字列は変更できません。a[0] = 'h'で代入が行われると、TypeError が発生します。文字列を変更する場合は、新しい文字列を作成する必要があります。
※不変なオブジェクトを参照する変数の場合、変数の値を更新すると、オブジェクトの値が更新されるのではなく、新しいオブジェクトが生成され、そのオブジェクトを参照することになります。

```python:例
>>> a = 'Hello, World!'
>>> id(a)
4337149552
>>> a = 'hello, World!'
>>> id(a)
4337149488
```

</details>

# 問題 4

次のコードの実行結果を選択してください。

```python
a = 6 / 2
b = 5.0 // 2
c = 10 % 3
print(a + b * c)
```

```:選択肢
A: 5
B: 4.5
C: 5.0
D: 5.5
```

<details><summary>解答</summary>
正解：C

解説：

- まず、各変数の値を計算します。
  - 6 / 2 は通常の除算であり、結果は浮動小数点数です。この場合、6 を 2 で割った結果は 3.0 となります。
  - 5.0 // 2 は切り捨て除算であり、結果は整数部分のみを返します。ただし、切り捨て除算の結果が整数となるのは、除数と被除数がともに整数（int）の場合です。浮動小数点数（float）を除数や被除数とした場合、結果は浮動小数点数となります。したがって、5.0 を 2 で切り捨て除算した結果は 2.0 となります。同様に、加算（`+`）、減算（`-`）、剰余演算（`%`）も整数と浮動小数点数の組み合わせで同様の挙動を示します。
  - 10 % 3 は剰余演算であり、除算の余りを返します。この場合、10 を 3 で割った余りは 1 となります。
- 次に、演算の優先順位に従って計算を行います。乗算と除算は加算と減算よりも優先度が高いため、b _ c が先に計算されます。2.0 を 1 で乗算するため、結果は 2.0 となります。その後、a + (b _ c)が計算されます。a の値が 3.0 であるため、3.0 + 2.0 の結果は 5.0 となります。

</details>

# 問題 5

次のコードの実行結果を選択してください。

```python
a = 3.9
print(int(a))
```

```:選択肢
A: 3
B: 4
C: ValueError
D: None
```

<details><summary>解答</summary>
正解：A

解説：`int()`関数は与えられた引数を`整数に変換する`ための関数です。整数が指定された場合はそのままの値を返し、浮動小数点数が指定された場合は 0 に近い値に丸められて整数として返します。引数を省略した場合は 0 を返します。 したがって、print(int(a))の結果は 3 となります。

</details>

# 問題 6

次のコードの実行結果を選択してください。

```python
a = .1_0_1
print(a)
```

```:選択肢
A: SyntaxError
B: 101
C: 1.01
D: 0.101
```

<details><summary>解答</summary>
正解：D

解説：

- `アンダースコア`を数値リテラル内に挿入することで、`数字の桁区切り`を行うことができ、読みやすさを向上させることができます。
- ```python:例
  num = 1_000_000_000
  ```
- 変数 a に代入されている値.1_0_1 は浮動小数点数リテラルです。`浮動小数点数リテラルの前に整数部分がない場合、整数部分はゼロとみなされます`。そのため、.1_0_1 は 0.101 と同じ値を表します。

</details>

# 問題 7

次のコードの実行結果を選択してください。

```python
a = (1, 2)
b = (3, 4)
print(a - b)
```

```:選択肢
A: (2, 2)
B: (-2, -2)
C: (1, 2, 3, 4)
D: TypeError
```

<details><summary>解答</summary>
正解：D

解説：この問題では、タプル a とタプル b の引き算を行おうとしています。タプルは不変なオブジェクトであり、タプル同士の引き算がサポートされていないため、エラーが発生します。ただし、タプル同士の足し算では要素の結合を行い、新しいタプルを作成することができます。

```python:例
a = (1, 2)
b = (3, 4)
print(a + b) # (1, 2, 3, 4)
```

</details>

# 問題 8

次のコードの実行結果を選択してください。

```python
a = 'Hello, World!',
print(a)
```

```:選択肢
A: Hello, World!
B: Syntax Error
C: ['Hello, World!']
D: ('Hello, World!',)
```

<details><summary>解答</summary>
正解：D

解説：Python では、`()`がなくても、変数の値の末尾にカンマが付いている場合は、タプルとして解釈されます。（`a = ('Hello, World!',)`と同じ）`バグが発生しやすい書き方`なので、気をつけましょう。この問題では、変数 a に代入された文字列'Hello, World!'の末尾にカンマがあるため、print 文で表示される際にはタプルとして認識されます。

</details>

# 問題 9

次のコードの実行結果を選択してください。

```python
def sample_func(num1, num2):
    num1, num2 = num2, num1
    return num1, num2

a, b = sample_func(1, 2)
print(a)
```

```:選択肢
A: 1
B: 2
C: (1, 2)
D: (2, 1)
```

<details><summary>解答</summary>
正解：B

解説：タプルのアンパックの問題です。

- 関数 sample_func の中で、`num1とnum2の値を入れ替える`ために、次の行が実行されています: num1, num2 = num2, num1。
  この行では、Python のタプルのアンパックを使って、num1 と num2 の値を一時的なタプルにまとめ、その後で互いに値を入れ替えています。
- 関数 sample_func の戻り値はタプルとして返されますが、タプルのアンパックを使って a, b = sample_func(1, 2) のような形式で戻り値を変数に代入することで、`タプルの各要素を個別の変数に割り当て`ています。ただし、タプルのアンパックにおいては、タプルの要素数と変数の数が一致している必要があります。要素数が一致しない場合には ValueError が発生します。

</details>

# 問題 10

次のコードの実行結果を選択してください。

```python
a = [1, 2, 3]
b = a
b[0] = 100
print(a)
```

```:選択肢
A: [1, 2, 3]
B: IndexError
C: [100, 2, 3]
D: [0, 2, 3]
```

<details><summary>解答</summary>
正解：C

解説：

- Python の`リスト`は`可変（mutable）なオブジェクト`です。つまり、リストの要素を変更することができます。
- この問題では、変数 a にリスト[1, 2, 3]が代入され、`変数bにはリストaの参照が代入`されています。つまり、変数 b も同じリストオブジェクトを指しています。
- b[0] = 100 という代入文が実行されることで、リスト b の最初の要素が 100 に変更されますが、`変数aと変数bは同じリストを参照`しているため、どちらの変数を介しても変更が反映されます。

</details>

# 問題 11

次のコードの実行結果を選択してください。

```python
import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a)
b[0][0] = 'hello'
print(a)
```

```:選択肢
A: [[1, 2], [3, 4]]
B: ['hello', [3, 4]]
C: [['hello', 2], [3, 4]]
D: IndexError
```

<details><summary>解答</summary>
正解：C

解説：浅いコピーとリスト内の要素への参照に関する理解を確認する問題です。

- Python の組み込みモジュールである copy モジュールは、オブジェクトのコピーを行うための関数やクラスを提供しています。
- `copy.copy()`関数は、`浅いコピー`（shallow copy）を行います。コピー元のオブジェクトとコピー先のオブジェクトが異なるものの、コピー先のオブジェクトがコピー元のオブジェクト内の要素への参照を共有する場合があります。
- 変数 a には、リスト[[1, 2], [3, 4]]が代入されています。copy.copy()関数を使って、a の浅いコピーを作成し、b に代入しています。次に、b[0][0] = 'hello'という代入文が実行され、b の最初の要素の最初の要素が'hello'に変更されます。b は a の浅いコピーであるため、a と b は別々のオブジェクトですが、`リスト内の要素は同じオブジェクトを参照`しています。そのため、b の変更が a にも反映されます。したがって、選択肢 C が正解です。
- 要素のコピーも行う場合は copy.deepcopy()関数を使います。

```python:例
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0][0] = 'hello'
print(a) # [[1, 2], [3, 4]]
```

</details>

# 問題 12

次のコードの実行結果を選択してください。

```python
def sample_func(num, l=[]):
    l.append(num)
    return l

a = sample_func(4)
b = sample_func(5)
print(a, b)
```

```:選択肢
A: [4] [5]
B: [4] [4, 5]
C: [4, 5] [4, 5]
D: [4] []
```

<details><summary>解答</summary>
正解：C

解説：この問題では、関数のデフォルト引数として可変（mutable）なオブジェクトを使う際の注意点を確認しています。

- sample_func という関数が定義されており、第二引数の l は`デフォルト引数として空のリスト`が与えられています。
- 最初の呼び出しでは、sample_func(4)が実行されます。この時、デフォルト引数のリスト l は空のリスト[]として初期化されます。そして、引数 num の値 4 がリスト l に追加されます。
- 2 回目の呼び出しでは、sample_func(5)が実行されます。この時、デフォルト引数のリスト l は`前の呼び出し時に変更された状態のリスト[4]を保持したままで、引数numの値5が追加`されます。
- デフォルト引数 l は関数の`定義時に一度だけ評価され、その後の関数の呼び出し時に再利用`されます。このような動作は、デフォルト引数として可変なオブジェクト（リストや辞書など）を使う場合によく見られる落とし穴です。
- この問題を回避するためには、デフォルト引数として可変なオブジェクトを使う代わりに、`Noneなどの不変オブジェクトを使って、関数内で条件分岐して新しいオブジェクトを作成する`方法を検討することが推奨されています。

```python:例
def sample_func(num, l=None):
    if l is None:
        l = []
    l.append(num)
    return l

a = sample_func(4)
b = sample_func(5)
print(a, b) # [4] [5]
```

</details>

# 問題 13

次のコードの実行結果を選択してください。

```python
languages = ['Python', 'Python', 'Java', 'Go', 'Python']

for i, language in enumerate(languages):
    if language != 'Python':
        del languages[i]

print(languages)
```

```:選択肢
A: ['Python', 'Python', 'Go', 'Python']
B: ['Python', 'Python', 'Python', 'Python']
C: ['Python', 'Python', 'Python']
D: ['Python', 'Python', 'Java', 'Python']
```

<details><summary>解答</summary>
正解：A

解説：この問題では、languages というリストが定義されており、ループを使ってリスト内の要素をチェックしています。

- enumerate()関数を使って、`インデックスと要素を同時に取得`しています。各要素が language に代入され、そのインデックスが i に代入されます。
- ループの中で、要素が'Python'でない場合に`del文を使ってlanguagesリストから該当の要素を削除`しています。
- 要素を削除する際に、リストの要素へのインデックスで削除を行っています。しかし、ループ内で要素を削除する場合、リストの要素のインデックスが変化するため、予期しない結果が生じます。
- この問題では、`要素が削除された途端に、次の要素のインデックスが１つ前に移動する`ので、'Go'がスキップされてしまいます。したがって、選択肢 A が正解です。
- `ループ時にはリストの追加と削除をしない`ことが推奨されています。その代わりに、新しいリストを用意したり、削除したい要素を除く要素をコピーしたリストを作成し、元のリストを置き換えたりすることができます。

</details>

# 問題 14

次のコードの実行結果を選択してください。

```python
person = {'name': 'Fmak', 'age': 29, 'city': 'Tokyo'}
print(person.get('gender'))
```

```:選択肢
A: None
B: gender
C: TypeError
D: KeyError
```

<details><summary>解答</summary>
正解：A

解説：

- person.get('gender') という行では、辞書 person の get()メソッドを使って、'gender'というキーに対応する値を取得しようとしています。`指定したキーが辞書内に存在しない場合、デフォルトでNoneを返します`。キーが存在しない場合に None ではなく別の値を返したい場合は、get()メソッドの第二引数にデフォルト値を指定することができます。例えば、person.get('gender', 'Unknown')のように記述すると、キーが存在しない場合には'Unknown'が返されます。
- `person['gender']`のように記述すると、`キーが存在しない場合はKeyError`が発生しますので、ご注意ください。

</details>

# 問題 15

次のコードの実行結果を選択してください。

```python
from collections import defaultdict
pets = defaultdict(int)
pets['Cat'] += 1
print(pets['Cat'] + pets['Dog'])
```

```:選択肢
A: 0
B: 1
C: 2
D: KeyError
```

<details><summary>解答</summary>
正解：B

解説：この問題では、collections モジュールから defaultdict をインポートし、pets という名前の defaultdict オブジェクトを作成しています。

- defaultdict オブジェクトは、辞書（dict）の一種であり、`存在しないキーに対して自動的にデフォルトの値を返す`機能を持っています。この場合、int を指定しているため、存在しないキーに対しては整数のデフォルト値 0 が返されます。
- pets['Cat'] += 1 という行では、存在しないキーの'Cat'の値に 1 を加算しているため、値が 0 + 1 = 1 になります。
- 'Dog'というキーも存在しないため、デフォルトの値 0 が返されます。
- したがって、print(pets['Cat'] + pets['Dog'])の結果は 1 + 0 = 1 になります。

</details>


# 問題 16

次のコードの実行結果を選択してください。

```python
a = {1, 2, 3, 4}
a |= {3, 4, 5, 6}
print(a)
```

```:選択肢
A: {3, 4}
B: {1, 2, 3, 4, 5, 6}
C: {1, 2, 5, 6}
D: Syntax Error
```

<details><summary>解答</summary>
正解：B

解説：

- a |= {3, 4, 5, 6} という行は、a = a | {3, 4, 5, 6}とも書けます。`|`演算子は`和集合`（union）演算を行います。つまり、{1, 2, 3, 4}と{3, 4, 5, 6}の和集合を計算することにあります。
- 集合は`重複を許さない`データ構造であるため、和集合演算を行う際に重複した要素は自動的に取り除かれます。（集合は順序保証されないため、要素の表示順序は実行環境によって選択肢 B と異なる場合があります）

</details>

# 問題 17

次のコードの実行結果を選択してください。

```python
a = 'None'

result1 = a is None
result2 = a is not None

print(result1, result2)
```

```:選択肢
A: False True
B: False False
C: True True
D: True False
```

<details><summary>解答</summary>
正解：A

解説：この問題では、文字列'None'と None の比較を行っています。

- result1 は、a is None という式で a が None と同じオブジェクトを参照しているかどうかを判定しています。しかし、a は文字列'None'を参照しており、None とは異なるオブジェクトであるため、result1 の結果は False です。
- result2 は、a is not None という式で a が None と同じオブジェクトを参照していないかどうかを判定しています。結果は True です。
- `None`のようなシングルトンオブジェクトを比較する場合、`is演算子を使う`ことが推奨されています。
  - `is`演算子はオブジェクトの同一性を比較するため、メモリ上の実際のオブジェクトを比較します。一方、`==`演算子はオブジェクトの値の等価性を比較するため、値が同じかどうかを検査します。シングルトンオブジェクトの場合、is 演算子を使うことで、そのオブジェクトが同じメモリ位置を指しているかどうかを効率的に判定できます。
  - None は特別な値であり、シングルトンとして実装されています。Python のインタプリタは、プログラム内で使われる None オブジェクトに対しては、同じメモリ位置を再利用するように最適化されています。そのため、None オブジェクト同士の比較では、常に同じメモリ位置を指しているかどうかを確認する必要があります。この場合、is 演算子を使うことで、比較処理をより明確かつ効率的に行うことができます。

</details>

# 問題 18

次のコードの実行結果を選択してください。

```python
a = 1

def sample_func():
    a += 1
    return a

print(sample_func())
```

```:選択肢
A: 1
B: 2
C: UnboundLocalError
D: NameError
```

<details><summary>解答</summary>
正解：C

解説：この問題では、関数内でグローバル変数 a を操作しようとしています。

- 関数 sample_func 内で a += 1 という行がありますが、a がグローバル変数であるため、ローカルスコープ内で定義されていません。関数 sample_func のスコープ内で変数を更新しようとすると、UnboundLocalError が発生します。
- 関数 sample_func のスコープ内で変数 a を更新するには、global 宣言する必要があります。

```python:例
a = 1

def sample_func():
    global a
    a += 1
    return a

print(sample_func()) # 2
```

</details>

# 問題 19

次のコードの実行結果を選択してください。

```python
for i in range(10):
    pass

print(i)
```

```:選択肢
A: 10
B: 9
C: range(10)
D: NameError
```

<details><summary>解答</summary>
正解：B

解説：この問題では、for ループ内で変数 i を定義していますが、ループの終了後にも i の値が保持され、print(i)でその値が出力されます。

- Python の for ループは`ブロックスコープを持たず`、ループ内で定義された変数はループの外でも参照可能です。したがって、ループが終了した後でも変数 i の値は保持されており、最後にイテレーションされた値が出力されます。
- この挙動は、ループ内で変数を使用する場合に注意が必要です。ループの終了後に変数を使用する場合は、ループ外で事前に変数を初期化しておくなどの対策が必要です。

</details>

# 問題 20

次のコードの実行結果を選択してください。

```python
a = print('Hello, World!')
print(a)
```

```:選択肢
A: Hello, World!
B: print('Hello, World!')
C: Hello, World!
   None
D: None
   Hello, World!
```

<details><summary>解答</summary>
正解：C

解説：`print()`関数は、与えられた引数を標準出力に表示する関数です。この関数は値を返さず、`戻り値は常にNone`です。つまり、print()関数の結果を変数に代入しても、その値はNoneになります。

したがって、print(a)では、まずprint('Hello, World!')が実行され、メッセージが表示されます。戻り値としてはNoneが返されるので、aを出力すると、Noneが表示されます。

</details>

# 参考

- https://docs.python.org/ja/3/
- https://nostarch.com/beyond-basic-stuff-python
- https://www.youtube.com/@Indently