Pythonでそれを書いたら
for i in a:
i += 1
変数a
はi
の元の要素のコピーであることが判明したため、元のリストa
の要素は実際にはまったく影響を受けません。
元の要素を変更するには、
for index, i in enumerate(a):
a[index] += 1
必要になるでしょう。
私はこの行動に本当に驚いていました。これは非常に直感に反し、他の言語とは一見異なるようで、コードにエラーが発生し、今日は長い間デバッグする必要がありました。
私は前にPythonチュートリアルを読みました。念のため、本を今すぐもう一度チェックしましたが、この動作についてはまったく触れられていません。
この設計の背後にある理由は何ですか?チュートリアルが読者が自然に理解する必要があると信じるように、それは多くの言語での標準的な実践であると期待されていますか?他のどの言語で反復の同じ動作が存在するか、今後注意する必要がありますか?
私はすでに 同様の質問に答えました 最近、+=
が異なる意味を持つ可能性があることを理解することは非常に重要です:
データ型がインプレース加算を実装している場合(つまり、正しく機能する__iadd__
関数がある場合)、i
が参照するデータが更新されます(リスト内にあるか他の場所にあるかは関係ありません)。
データ型が__iadd__
メソッドを実装していない場合、i += x
ステートメントはi = i + x
の単なる構文シュガーなので、新しい値が作成され、変数名i
に割り当てられます。
データ型が__iadd__
を実装しているが、何か奇妙なことをする場合。更新されている可能性があります...かどうか-それはそこで実装されているものに依存します。
Pythonの整数、浮動小数点数、文字列は__iadd__
を実装していないため、これらはインプレースで更新されません。ただし、numpy.array
やlist
sなどの他のデータ型はそれを実装し、期待どおりに動作します。したがって、反復時にコピーまたはコピーなしの問題ではありません(通常、list
sとTuple
sのコピーは行いませんが、コンテナ__iter__
と__getitem__
メソッドの実装によっても異なります)。これは、 a
に保存したデータ型。
Pythonはreferenceとpointerの概念を区別しません。彼らは通常referenceという用語を使用しますが、C++のようなその区別がある言語と比較すると、pointer。
質問者は明らかにC++の背景に由来し、その区別-説明に必要なこと-存在しない Pythonであるため、C++の用語を使用することを選択しました。
void foo(int x);
は、整数値によるを受け取る関数のシグネチャです。void foo(int* x);
は、整数ポインタによるを受け取る関数のシグネチャです。void foo(int& x);
は、整数を受け取る関数のシグネチャ(参照により)です。「他の言語と違う」とはどういう意味ですか?特に指示がない限り、for-eachループのサポートが要素をコピーしていることを私が知っているほとんどの言語。
特にPythonの場合(ただし、これらの理由の多くは、同様のアーキテクチャーまたは哲学的概念を持つ他の言語に適用される可能性があります):
この振る舞いはそれを知らない人々にバグを引き起こすかもしれませんが、別の振る舞いはバグを引き起こすかもしれません気づいている人でさえそれの。変数(i
)を割り当てる場合、通常は停止せず、そのために変更される他のすべての変数を考慮しません(a
)。作業中のスコープを制限することは、スパゲッティコードを防ぐための主要な要素であるため、参照による反復をサポートする言語でも、コピーによる反復はデフォルトです。
Python変数は常に単一のポインターであるため、コピーによる反復の方が安く、参照による反復よりも安く、値にアクセスするたびに追加の遅延が必要になります。
Pythonには、C++などの参照変数の概念はありません。つまり、Python=のすべての変数は実際には参照ですが、ポインタであるという意味では、C++のような舞台裏のconstat参照ではありませんtype& name
引数。この概念はPythonには存在しないため、参照による反復の実装-言うまでもなくそれをデフォルトにします! -バイトコードをさらに複雑にする必要があります。
Pythonのfor
ステートメントは、配列だけでなく、ジェネレーターのより一般的な概念でも機能します。舞台裏では、Pythonは配列でiter
を呼び出してオブジェクトを取得します。オブジェクトをnext
で呼び出すと、次の要素を返すかraise
s a StopIteration
。Pythonでジェネレーターを実装するにはいくつかの方法があり、参照による繰り返しのためにそれらを実装することははるかに困難でした。
ここでの答えはどれも、Python土地で発生する理由理由を実際に示すために使用するコードを提供しません。そしてこれは見て楽しいです。より深いアプローチでここに行く。
これが期待どおりに機能しない主な理由は、Pythonでは次のように記述しているためです。
i += 1
それはあなたがしていると思っていることをしていません。整数は不変です。これは、Pythonでオブジェクトが実際に何であるかを調べるとわかります。
a = 0
print('ID of the first integer:', id(a))
a += 1
print('ID of the first integer +=1:', id(a))
id関数 は、オブジェクトの存続期間におけるオブジェクトの一意の定数値を表します。概念的には、C/C++のメモリアドレスに緩やかにマップされます。上記のコードを実行する:
ID of the first integer: 140444342529056
ID of the first integer +=1: 140444342529088
つまり、IDが異なるため、最初のa
は2番目のa
と同じではなくなります。事実上、それらはメモリ内の異なる場所にあります。
ただし、オブジェクトの場合、動作は異なります。ここで+=
演算子を上書きしました:
class CustomInt:
def __iadd__(self, other):
# Override += 1 for this class
self.value = self.value + other.value
return self
def __init__(self, v):
self.value = v
ints = []
for i in range(5):
int = CustomInt(i)
print('ID={}, value={}'.format(id(int), i))
ints.append(int)
for i in ints:
i += CustomInt(i.value)
print("######")
for i in ints:
print('ID={}, value={}'.format(id(i), i.value))
これを実行すると、次の出力が生成されます。
ID=140444284275400, value=0
ID=140444284275120, value=1
ID=140444284275064, value=2
ID=140444284310752, value=3
ID=140444284310864, value=4
######
ID=140444284275400, value=0
ID=140444284275120, value=2
ID=140444284275064, value=4
ID=140444284310752, value=6
ID=140444284310864, value=8
この場合のid属性は、オブジェクトの値が異なっていても、実際には両方の反復でsameであることに注意してください(id
ofオブジェクトが保持するint値。これは、変化すると変化します-整数は不変です)。
これを、不変オブジェクトを使用して同じ演習を実行した場合と比較してください。
ints_primitives = []
for i in range(5):
int = i
ints_primitives.append(int)
print('ID={}, value={}'.format(id(int), i))
print("######")
for i in ints_primitives:
i += 1
print('ID={}, value={}'.format(id(int), i))
print("######")
for i in ints_primitives:
print('ID={}, value={}'.format(id(i), i))
これは出力します:
ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
######
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
ID=140023258889408, value=5
######
ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
ここで注意すべき点がいくつかあります。まず、+=
のループでは、元のオブジェクトに追加していません。この場合、intは Pythonの不変の型 の中にあるため、pythonは異なるIDを使用します。また、Python =同じ不変値を持つ複数の変数に対して、同じ基礎となるid
を使用します:
a = 1999
b = 1999
c = 1999
print('id a:', id(a))
print('id b:', id(b))
print('id c:', id(c))
id a: 139846953372048
id b: 139846953372048
id c: 139846953372048
tl; dr-Pythonには、目に見える動作を引き起こす不変タイプがいくつかあります。すべての可変タイプ、あなたの期待は正しいです。
@Idanの答えは、PythonがCのようにループ変数をポインターとして扱わない理由を説明するのに役立ちますが、コードスニペットがどのように展開されるかをより深く説明する価値があります。 in Python多くの単純なコードのコードは、実際には built in methods への呼び出しになります。最初の例をとるには
_for i in a:
i += 1
_
アンパックするものは2つあります。_for _ in _:
_構文と__ += _
_構文です。他の言語と同様に、最初にforループを取るには、Pythonには、基本的にイテレーターパターンの構文糖である_for-each
_ループがあります。Pythonでは、イテレーターは-を定義するオブジェクトです .__next__(self)
シーケンスの現在の要素を返し、次の要素に進み、シーケンスに項目がなくなるとStopIteration
を発生させるメソッド。 Iterable は、イテレータを返す.__iter__(self)
メソッドを定義するオブジェクトです。
(NB:Iterator
もIterable
であり、.__iter__(self)
メソッドから自分自身を返します。)
Pythonには通常、カスタムの二重下線メソッドに委任する組み込み関数があります。したがって、それは iter(o)
に解決されてo.__iter__()
に解決され、next(o)
がo.__next__()
に解決されます。これらの組み込み関数は、デリゲートするメソッドが定義されていない場合、適切なデフォルト定義を試みることがよくあります。たとえば、len(o)
は通常o.__len__()
に解決されますが、そのメソッドが定義されていない場合はiter(o).__len__()
を試します。
Forループは、基本的にnext()
、iter()
、およびより基本的な制御構造によって定義されます。一般的にコード
_for i in %EXPR%:
%LOOP%
_
のようなものに解凍されます
__a_iter = iter(%EXPR%)
while True:
try:
i = next(_a_iter)
except StopIteration:
break
%LOOP%
_
したがって、この場合
_for i in a:
i += 1
_
解凍されます
__a_iter = iter(a) # = a.__iter__()
while True:
try:
i = next(_a_iter) # = _a_iter.__next__()
except StopIteration:
break
i += 1
_
この残りの半分は_i += 1
_です。一般的に_%ASSIGN% += %EXPR%
_は%ASSIGN% = %ASSIGN%.__iadd__(%EXPR%)
に解凍されます。ここで __iadd__(self, other)
はインプレース加算を実行し、それ自体を返します。
(注:メインメソッドが定義されていない場合、Pythonが代替を選択する別のケースです。オブジェクトが___iadd__
_を実装していない場合、___add__
_にフォールバックします。int
は___iadd__
_を実装していないため、実際にはこれを実行します-これは不変であり、その場で変更できないため意味があります。)
したがって、ここのコードは次のようになります
__a_iter = iter(a)
while True:
try:
i = next(_a_iter)
except StopIteration:
break
i = iadd(i,1)
_
定義できる場所
_def iadd(o, v):
try:
return o.__iadd__(v)
except AttributeError:
return o.__add__(v)
_
2番目のコードでは、もう少し進んでいます。私たちが知る必要がある2つの新しいことは、_%ARG%[%KEY%] = %VALUE%
_が_(%ARG%).__setitem__(%KEY%, %VALUE%)
_に解凍され、_%ARG%[%KEY%]
_が_(%ARG%).__getitem__(%KEY%)
_に解凍されることです。この知識をまとめると、_a[ix] += 1
_がa.__setitem__(ix, a.__getitem__(ix).__add__(1))
に展開されます(ここでも___add__
_ではなく___iadd__
_ ___iadd__
_はintによって実装されていないため)。最終的なコードは次のようになります。
__a_iter = iter(enumerate(a))
while True:
try:
index, i = next(_a_iter)
except StopIteration:
break
a.__setitem__(index, iadd(a.__getitem__(index), 1))
_
最初のスニペットがリストを変更するのに2番目のリストが変更しない理由に関する実際の質問に答えるために、最初のスニペットではi
をnext(_a_iter)
から取得しています。つまり、i
はint
になります。 int
はその場で変更できないため、_i += 1
_はリストに対して何も行いません。 2番目のケースでは、int
を変更していませんが、___setitem__
_を呼び出してリストを変更しています。
この全体的に複雑な演習の理由は、Pythonに関する次のレッスンを教えていると思うからです。
二重下線メソッドは、最初はハードルですが、Pythonの「実行可能な疑似コード」の評判を裏付けるために不可欠です。まともなPythonプログラマーは、これらのメソッドとそれらがどのように呼び出されるかを完全に理解し、そうすることが理にかなっているところはどこでもそれらを定義します。
Edit:@deltabは、「コレクション」という用語のずさんな用法を修正しました。
+=
の動作は、現在の値がmutableであるかimmutableであるかによって異なります。 Python開発者は混乱を招くのではないかと恐れていたため、これがPythonでの実装に時間がかかる主な理由でした。
i
がintの場合、それはcannotに変更できます。これは、intが不変であるため、i
の値が変更される場合は、必ず別のオブジェクトを指す必要があります。
>>> i=3
>>> id(i)
14336296
>>> i+=1
>>> id(i)
14336272 # Other object
ただし、左側がmutableの場合、+ =で実際に変更できます。それがリストであるかのように:
>>> i=[]
>>> id(i)
140257231883944
>>> i+=[1]
>>> id(i)
140257231883944 # Still the same object!
Forループでは、i
はa
の各要素を順番に参照します。それらが整数の場合、最初のケースが適用され、i += 1
の結果は別の整数オブジェクトを参照している必要があります。もちろん、リストa
には、以前と同じ要素がまだあります。
ここのループは関係ありません。関数のパラメーターや引数と同様に、そのようなforループを設定することは、基本的には空想に見える割り当てです。
整数は不変です。それらを変更する唯一の方法は、新しい整数を作成し、それを元の整数と同じ名前に割り当てることです。
割り当てに関するPythonのセマンティクスはCに直接マップされます(CPythonのPyObject *ポインターが与えられていれば当然です)。ただし、everythingはポインターであり、ダブルポインターを使用することはできません。次のコードを検討してください。
a = 1
b = a
b += 1
print(a)
何が起こるのですか? 1
を出力します。どうして?実際には、次のCコードとほぼ同じです。
i64* a = malloc(sizeof(i64));
*a = 1;
i64* b = a;
i64* tmp = malloc(sizeof(i64));
tmp = *b + 1;
b = tmp;
printf("%d\n", *a);
Cコードでは、a
の値がまったく影響を受けないことは明らかです。
リストが機能するように見える理由については、答えは基本的に同じ名前に割り当てているということです。リストは変更可能です。 a[0]
という名前のオブジェクトのIDは変更されますが、a[0]
は引き続き有効な名前です。これは次のコードで確認できます。
x = 1
a = [x]
print(a[0] is x)
a[0] += 1
print(a[0] is x)
しかし、これはリストにとって特別なことではありません。そのコードのa[0]
をy
に置き換えると、まったく同じ結果が得られます。