Pythonのfor文は書き方によって元のデータが変わったり変わらなかったりするのを詳しく解説

　Pythonの基礎的な話、というか仕様にまつわる話です。個別の要素については既にたくさんの記事がありますが、「どうすればいいか」「なぜそうなるのか」という点をまとめて解説した記事はなさそうなので（あったらすいません）、そのあたりを知識の整理も兼ねて解説していきたいと思います。

# for文でリストを改変できない

　想定している罠は以下のようなものです。

```python
nums = list(range(1,5)) #[1, 2, 3, 4]

for num in nums:
    num *= 2

print(nums)
```

　`[1, 2, 3, 4]`をそれぞれ2倍するという命令です。当然`[2, 4, 6, 8]`が出力されるものと思いきや、

```python
[1, 2, 3, 4]
```

　元のリストと変わらないものが出てきます。

# どうすればいいのか

　なぜそうなるのかは置いといて、これはとりあえず以下のようにすれば望む結果が出てきます。

```python
nums = list(range(1,5)) #[1, 2, 3, 4]

for i in range(4):
    nums[i] *= 2

print(nums) #[2, 4, 6, 8]
```

# なぜそうなるのか

　両者の違いは`for`文で直接アクセスするか（以降、便宜上`foreach`と呼びます。JavaScript や C# ではこのような名前がつきます）、`i`で個別にアクセスするかの違い（以降、便宜上「個別アクセス」と呼びます）でした。ここで重要なのは、「`foreach`では改変できずに、個別アクセスでは改変できる」**という話ではない**ということです。

　上のプロセスをより丁寧に追っていくために、オブジェクトIDを追いながら同じ命令を走らせてみます。

```python:foreach
nums = list(range(1,5)) #[1, 2, 3, 4]

for num in nums:
    print('before',id(num))
    num *= 2
    print('after',id(num))

for num in nums:
    print(id(num),num)
```

```python
before 140722392559872
after 140722392559904
before 140722392559904
after 140722392559968
before 140722392559936
after 140722392560032
before 140722392559968
after 140722392560096
140722392559872 1
140722392559904 2
140722392559936 3
140722392559968 4
```

　`nums`のそれぞれの要素のオブジェクトIDが、`before`と変わっていません。

```python:個別アクセス
nums = list(range(1,5)) #[1, 2, 3, 4]

for i in range(4):
    print('before',id(nums[i]))
    nums[i] *= 2
    print('after',id(nums[i]))

for i in range(4):
    print(id(nums[i]),nums[i])
```
```python
before 140722388562176
after 140722388562208
before 140722388562208
after 140722388562272
before 140722388562240
after 140722388562336
before 140722388562272
after 140722388562400
140722388562208 2
140722388562272 4
140722388562336 6
140722388562400 8
```

　`nums`のそれぞれの要素のオブジェクトIDが、`after`のものに変わっています。これで直接的な原因がわかりました。

# なぜこの違いが出るのか

　**参照渡し**と**値渡し**という概念があります。前者は、あるオブジェクトから別のオブジェクトに値を持ってくる時に、元の値と連動させるもの（つまり、別名を与えているだけ）で、後者は、全く新しいオブジェクトとして切り離すもの（つまり、コピーする）です。

　なるほど、じゃあ`foreach`の時は値渡しをしているのか！**……とはなりません**。だったら`foreach`と個別アクセスの時で、`before`が同じオブジェクトIDを指さないですからね。

　実は、Pythonは基本的に~~すべて参照渡しです~~。（**追記：この点、@shiracamus さんから指摘いただきまして、正確には「参照の値渡し」になります）**

　しかし、一部のオブジェクトについては~~「最初は参照渡しだけど、別の値が代入された時には新しいオブジェクトIDを取得する」~~「別の値が代入されると、その値のオブジェクトIDが変数に代入される」**（追記：同上）**という処理がなされます。具体的には、`int`や`tuple`等のイミュータブル（変更不可能、詳細はぐぐってください）なオブジェクトではそうなります。

```python:例
# immutable
a = 1 # int
b = a # この時点ではbとaは同じオブジェクトID
b = 2 # bに違うオブジェクトIDが割り当てられる
print(a) # 1
# aも一緒に2に変わってしまうようなことはない

# mutable
a = [1] # list
b = a # この時点ではbとaは同じオブジェクトID
b.append(2) # この状態でも同一ID
print(a) # [1, 2]
# aも一緒に変わってしまう
```

　つまり、`foreach`の場合には、`int`型の要素に直接代入しているから、単に別オブジェクトの`int`のIDを示しているだけで、個別アクセスの場合には、リストのどの要素がどの`int`型の要素を参照しているかを変更しているので、結果的にリストが改変できるという訳です。

　図で表すとこのようになります。**（追記：@shiracamus さんのコメントで使用された図をそのまま使用させていただいております。元の図は削除し、以下の説明も変更しました）**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/d9f35709-5410-0342-f365-582980888976.png)

　`foreach`の時は、`num`の参照先は`int`型のオブジェクトであり、それに別の値を代入しても`num`が参照する`int`が変更されるだけで、`nums`の参照先である元のリストは変わりません。対して、個別アクセスの時は、`nums`の参照先である`list`型のオブジェクト内の参照を変更しているので、結果として元のリストが改変できます。

# 逆にミュータブルな時は

　ここら辺を理解していないと、`list`等のミュータブルなオブジェクトを使った時に、逆に**改変したくないのに改変してしまう**という悲劇が起きることになります。

```python
nums = [[1],[2],[3],[4]]
for num in nums:
    num.append(5)
print(nums) # [[1, 5], [2, 5], [3, 5], [4, 5]]

nums = [[1],[2],[3],[4]]
for i in range(4):
    nums[i].append(5)
print(nums) # [[1, 5], [2, 5], [3, 5], [4, 5]] 結果は全く同じ
```

　これは`foreach`の時でも、`num`の参照先が`list`型であることから同一IDになるためです。

# ミュータブルでも値渡ししたい

　前にも述べた通り、Pythonは基本的に~~参照渡し~~参照の値渡し**（追記：同上）**なので、明示的な値渡しはできません。しかし、値渡しのような動きをさせたい時は出てくると思います。そのような時には、`copy`を使うか、インデックス指定して事実上のコピーをすることで対処できます。

```python
nums = [[1],[2],[3],[4]]
for num in nums:
    new_num = num.copy()
    new_num.append(5)
print(nums) # [[1], [2], [3], [4]] 元のリストは変わらない

nums = [[1],[2],[3],[4]]
for num in nums:
    new_num = num[:]
    new_num.append(5)
print(nums) # [[1], [2], [3], [4]] 元のリストは変わらない
```

　復習みたいになりますが、これではダメです。

```python
nums = [[1],[2],[3],[4]]
for num in nums:
    new_num = num # 同一ID
    new_num.append(5)
print(nums) # [[1, 5], [2, 5], [3, 5], [4, 5]]
```

# 浅いコピーと深いコピー

　ちなみに、リストのメソッドである`copy()`やインデックス指定は**浅いコピー**と呼ばれるものです。多重リストの場合は、最初の一層目だけは別オブジェクトIDを取得してくれますが、二層目以降の参照は変わりません。なので、多重リスト全体をコピーしようとするとまた思わぬ罠にはまります。

```python
nums = [[1],[2],[3],[4]]
new_nums = nums.copy()
for num in new_nums:
    num.append(5) # 二層目以降は同一ID
print(nums) # [[1, 5], [2, 5], [3, 5], [4, 5]] 改変されてしまう
```

　多重リストで、全ての要素で新しいコピーを作るためには`copy`モジュールを用いて**深いコピー**をする必要があります。

```python
import copy

nums = [[1],[2],[3],[4]]
new_nums = copy.deepcopy(nums)
for num in new_nums:
    num.append(5)
print(nums) #[[1], [2], [3], [4]] 改変されていない