1
0

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のfor文は書き方によって元のデータが変わったり変わらなかったりするのを詳しく解説

Last updated at Posted at 2022-02-02

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

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

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

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]が出力されるものと思いきや、

[1, 2, 3, 4]

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

どうすればいいのか

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

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を追いながら同じ命令を走らせてみます。

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)
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と変わっていません。

個別アクセス
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])
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が変数に代入される」**(追記:同上)**という処理がなされます。具体的には、inttuple等のイミュータブル(変更不可能、詳細はぐぐってください)なオブジェクトではそうなります。

# 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

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

逆にミュータブルな時は

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

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を使うか、インデックス指定して事実上のコピーをすることで対処できます。

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]] 元のリストは変わらない

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

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を取得してくれますが、二層目以降の参照は変わりません。なので、多重リスト全体をコピーしようとするとまた思わぬ罠にはまります。

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モジュールを用いて深いコピーをする必要があります。

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]] 改変されていない
1
0
6

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?