web-dev-qa-db-ja.com

x + = yのようなショートカットが良い習慣と見なされるのはなぜですか?

これらが実際に何と呼ばれているのかはわかりませんが、いつも目にします。 Python実装は次のようなものです:

x += 5の略記としてx = x + 5

しかし、なぜこれが良い習慣と考えられているのでしょうか?私は、Python、C、 [〜#〜] r [〜#〜] などについて読んだほぼすべての本またはプログラミングチュートリアルでこれに遭遇しました。スペースを含めて3つのキーストロークを節約できるので便利です。しかし、私がコードを読んでいるとき、彼らはいつも私をつまずかせるように見えます、そして少なくとも私の心には、それをより少なく読みやすくします。

これらが至る所で使用されている明確で明白な理由がないのですか?

309
Fomite

速記ではありません。

_+=_記号は、1970年代にC言語で登場しました。Cの「スマートアセンブラ」という考え方では、明らかに異なる機械語命令とアドレッシングモードに対応しています。

"_i=i+1_"、_"i+=1_ "、" _++i_ "などは、抽象的なレベルでは同じ効果を生み出しますが、低レベルではプロセッサの異なる動作方法に対応します。

特に、i変数がCPUレジスタに格納されているメモリアドレスに存在すると想定して、これらの3つの式(Dと名付けましょう-「intへのポインタ」と考えてください)と- [〜#〜] alu [〜#〜] プロセッサのパラメータを受け取り、結果を「アキュムレータ」に返します(Aと呼びましょう-intと考えてください)。

これらの制約により(その期間のすべてのマイクロプロセッサで非常に一般的)、翻訳はおそらく

_;i = i+1;
MOV A,(D); //Move in A the content of the memory whose address is in D
ADD A, 1;  //The addition of an inlined constant
MOV (D) A; //Move the result back to i (this is the '=' of the expression)

;i+=1;
ADD (D),1; //Add an inlined constant to a memory address stored value

;++i;
INC (D); //Just "tick" a memory located counter
_

それを行う最初の方法は最適ではありませんが、定数(_ADD A, B_またはADD A, (D+x))の代わりに変数を操作する場合、またはより複雑な式を変換する場合(それらはすべてPush lowで煮詰められます)スタック内の優先操作、高優先度を呼び出し、ポップして、すべての引数が削除されるまで繰り返します)。

2つ目は「ステートマシン」のより一般的なものです。「式を評価する」のではなく、「値を操作する」:ALUを引き続き使用しますが、パラメーターを置き換えることができる結果になるように値を移動することは避けます。これらの種類の命令は、より複雑な式が必要な場合は使用できません。iがさらに必要になるため、_i = 3*i + i-2_を適切に操作できません。

3つ目は、さらに単純ですが、「加算」の考えすら考慮していませんが、カウンターにはより「プリミティブ」な(計算の意味で)回路を使用しています。レジスタを改造してカウンタにするために必要な組み合わせネットワークが小さく、したがって全加算器よりも高速であるため、命令は短縮され、ロードが速く、すぐに実行されます。

最新のコンパイラー(現在はCを参照)を使用してコンパイラーの最適化を有効にすると、利便性に基づいて対応を交換できますが、セマンティクスにはまだ概念的な違いがあります。

_x += 5_は

  • Xで識別される場所を見つける
  • それに5を加える

ただし、_x = x + 5_の意味は次のとおりです。

  • X + 5を評価する
    • Xで識別される場所を見つける
    • Xをアキュムレータにコピーします
    • アキュムレータに5を加算します
  • 結果をx に保存します
    • Xで識別される場所を見つける
    • アキュムレータをそれにコピーします

もちろん、最適化は

  • 「xの検索」に副作用がない場合、2つの「検索」は一度に実行できます(xはポインターレジスタに格納されたアドレスになります)
  • aDDがアキュムレータではなく_&x_に適用されている場合、2つのコピーを省略できます。

したがって、最適化されたコードが_x += 5_のコードと一致するようにします。

しかし、これは「xを見つける」に副作用がない場合にのみ実行できます。

_*(x()) = *(x()) + 5;
_

そして

_*(x()) += 5;
_

x()の副作用(x()は奇妙なことを行って_int*_を返す関数です)が2回または1回生成されるため、意味的に異なります。

したがって、_x = x + y_と_x += y_の同等性は、_+=_と_=_が直接のl値に適用される特定のケースによるものです。

Pythonに移行するために、Cから構文を継承しましたが、インタプリタ言語で実行する前に変換/最適化がないため、物事は必ずしもそれほど密接に関連しているわけではありません(解析ステップが1つ少ないため)。ただし、インタープリターは、3つのタイプの式の異なる実行ルーチンを参照して、式の形成方法と評価コンテキストに応じて異なるマシンコードを利用できます。


詳細が好きな人のために...

すべてのCPUには、ALU(算術論理ユニット)があります。これは、本質的に、命令のオペコードに応じて、その入出力がレジスタやメモリに「プラグイン」される組み合わせネットワークです。

バイナリ演算は通常、「どこか」で行われる入力を持つ「アキュムレータレジスタの修飾子」として実装されます。どこかにある可能性があります-命令フロー自体の内部(マニフェスト定数の場合は通常:ADD A 5)-別のレジストリ内の一時:例:ADD AB)-メモリ内、レジスタによって指定されたアドレス(通常、データフェッチ例:ADD A(H))-H、この場合、逆参照ポインタのように機能します。

この疑似コードでは、_x += 5_は

_ADD (X) 5
_

一方、_x = x+5_は

_MOVE A (X)
ADD A 5
MOVE (X) A
_

つまり、x + 5は一時的に割り当てられ、後で割り当てられます。 _x += 5_はxで直接動作します。

実際の実装は、プロセッサの実際の命令セットによって異なります。ADD (.) c opcodeがない場合、最初のコードは2番目になります。

そのようなオペコードがあり、最適化が有効になっている場合、2番目の式は、逆の動きを排除してレジスターのオペコードを調整した後、最初の式になります。

603

あなたの考え次第では、わかりやすくなるので、理解しやすくなります。例を挙げましょう:

x = x + 5は、「xを取り、それに5を加え、その新しい値をxに戻す」という精神処理を呼び出します。

x += 5は「xを5増やす」と考えることができます

したがって、これは単なる速記ではなく、実際には機能をより直接的に説明しています。コードの塊を読んでいると、把握しやすくなります。

285
Eric King

少なくとも Python では、x += yx = x + yはまったく異なることを実行できます。

たとえば、

a = []
b = a

その場合、a += [3]a == b == [3]となり、a = a + [3]a == [3]およびb == []となります。つまり、+=はオブジェクトをインプレースで変更します(そうすることもできますが、__iadd__メソッドを定義してほとんどすべてのことを実行できます)。一方、=は新しいオブジェクトを作成します。変数をそれにバインドします。

NumPy で数値計算を行う場合、これは非常に重要です。配列のさまざまな部分への複数の参照が頻繁に発生するため、配列の一部を誤って変更しないようにすることが重要です。配列への他の参照があるか、配列を不必要にコピーしている(非常に高価になる可能性があります)。

50
James

イディオム と呼ばれます。プログラミングイディオムは、特定のプログラミング構造を作成する一貫した方法であるため、便利です。

誰かがx += yあなたはxyだけインクリメントされており、より複雑な操作ではないことを知っています(ベストプラクティスとして、通常、より複雑な操作とこれらの構文の省略形を混在させません)。これは、1ずつ増やす場合に最も意味があります。

43
Joe

@Pubbyのポイントをもう少し明確にするには、someObj.foo.bar.func(x, y, z).baz += 5を検討してください

_+=_演算子なしでは、2つの方法があります。

  1. someObj.foo.bar.func(x, y, z).baz = someObj.foo.bar.func(x, y, z).baz + 5。これは非常に冗長で長いだけでなく、速度も遅くなります。したがって、人は
  2. 一時変数を使用します:tmp := someObj.foo.bar.func(x, y, z); tmp.baz = tmp.bar + 5。これは問題ありませんが、単純なことをするのに多くのノイズがあります。これは実際には実行時に何が起こるかに非常に近いですが、書くのは面倒で、_+=_を使用するだけで作業はコンパイラー/インタープリターにシフトします。

_+=_などの演算子の利点は否定できませんが、慣れるのは時間の問題です。

42
back2dos

それがより短くて簡単であるのは事実であり、それはおそらく基礎となるアセンブリ言語に触発されたのは事実ですが、それがベストプラクティスである理由は、クラス全体のエラーを防ぎ、それをより簡単にするからですコードを確認し、それが何をするかを確認します。

RidiculouslyComplexName += 1;

含まれる変数名は1つだけなので、ステートメントが何を行うかは確実です。

RidiculouslyComplexName = RidiculosulyComplexName + 1;

両者がまったく同じであることに疑問は常にあります。バグを見ましたか?下付き文字と修飾子が存在すると、さらに悪化します。

21
Jamie Cox

+ =表記は慣用的で短いものですが、これらは読みやすい理由ではありません。コードを読み取る際の最も重要な部分は、構文を意味にマッピングすることです。そのため、構文がプログラマの思考プロセスに近いほど、読みやすくなります(これは、定型コードが悪い理由でもあります:思考の一部ではありません)プロセスですが、コードを機能させるために必要です)。この場合の考えは、「変数xを5ずつインクリメント」することであり、「xにxプラス5の値を与える」ではありません。

たとえば、ifステートメントの方が適切な3項演算子を使用する場合など、短い表記が読みにくくなる場合もあります。

14
tdammers

これらの演算子が「Cスタイル」の言語である理由についての洞察のために、34年前の K&R 1st Edition(1978)からの抜粋があります。

簡潔さとは別に、代入演算子には、人々の考え方によく対応できるという利点があります。 「2をiに追加」または「iを2ずつ増やす」と言いますが、「iを取り、2を追加して、結果をiに戻す」ではありません。したがって、i += 2。さらに、次のような複雑な式の場合

yyval[yypv[p3+p4] + yypv[p1+p2]] += 2

代入演算子を使用すると、コードを理解しやすくなります。これは、2つの長い式が実際に同じであるか、なぜそうでないのか不思議に思う必要がないためです。また、代入演算子は、コンパイラがより効率的なコードを生成するのに役立ちます。

Brian Kernighan および Dennis Ritchie (K&R)は、この代入から、複合代入演算子がコードを読みやすくするのに役立つと信じていたことは明らかだと思います。

K&Rがそれを書いてから長い年月が経ち、人々がコードを書く方法についての多くの「ベストプラクティス」がその後変更または進化しました。しかし、このprogrammers.stackexchangeの質問は、複合代入の読みやすさについて苦情を言っている人を思い出すのが初めてなので、多くのプログラマーがそれらが問題であると思いますか?繰り返しますが、私がこれを入力すると、質問には95の賛成票があるため、コードを読むときに、人々がそれらを不快に感じるかもしれません。

12
Michael Burr

読みやすさ以外にも、実際にはさまざまなことをします:+=は、左側のオペランドを2回評価する必要はありません。

例えば、 expr = expr + 5exprを2回回避します(exprが不純であると想定)。

9
Pubby

他の人が非常によく説明した明らかなメリットに加えて、非常に長い名前を持っていると、よりコンパクトになります。

  MyVeryVeryVeryVeryVeryLongName += 1;

または

  MyVeryVeryVeryVeryVeryLongName =  MyVeryVeryVeryVeryVeryLongName + 1;
6

簡潔です。

入力する方がはるかに短いです。オペレーターが少なくて済みます。表面積が少なく、混乱の可能性が低くなります。

より具体的な演算子を使用します。

これは不自然な例であり、実際のコンパイラがこれを実装しているかどうかはわかりません。 x + = yは実際には1つの引数と1つの演算子を使用し、xを適切に変更します。 x = x + yは、x = zの中間表現を持つことができます。ここで、zはx + yです。後者は、加算と代入の2つの演算子、および一時変数を使用します。単一の演算子は、値側をy以外にすることはできず、解釈する必要がないことを非常に明確にします。理論的には、プラス演算子と代入演算子を直列に接続した場合よりも高速に実行されるプラス等号演算子を備えた豪華なCPUが存在する可能性があります。

6
Mark Canlas

素敵なイディオムです。それがより速いかどうかは言語に依存します。 Cでは、右側で変数を増やす命令に変換されるため、より高速です。 Python、Ruby、C、C++およびJava=を含む最新の言語はすべてop =構文をサポートしています。コンパクトであり、すぐに慣れます。 Peoples's Code(OPC)を使用している場合は、それに慣れて使用することもできます。

Pythonでは、x += 5は、整数オブジェクト1(プールから描画される場合もあります)の作成と5を含む整数オブジェクトの孤立化を引き起こします。

Javaでは、暗黙のキャストが発生します。入力してみてください

int x = 4;
x = x + 5.2  // This causes a compiler error
x += 5.2     // This is not an error; an implicit cast is done.
5
ncmathsadist

+=などの演算子は、変数をアキュムレータとして使用している場合に非常に役立ちます。

x += 2;
x += 5;
x -= 3;

次のものよりもはるかに読みやすいです:

x = x + 2;
x = x + 5;
x = x - 3;

最初のケースでは、概念的に、xの値を変更しています。 2番目のケースでは、新しい値を計算し、その値を毎回xに割り当てます。そして、おそらくそれほど単純なコードを書くことはおそらくないでしょうが、考え方は同じです...いくつかの新しい値を作成するのではなく、既存の値に対して何をしているのかに焦点を当てています。

4
Caleb

このことを考慮

(some_object[index])->some_other_object[more] += 5

あなたが本当に書きたいD0

(some_object[index])->some_other_object[more] = (some_object[index])->some_other_object[more] + 5
1
S.Lott

一度だけ言ってください:x = x + 1、私は「x」を2回言います。

しかし、「a = b + = 1」と書いてはいけません。そうしないと、10匹の子猫、27匹のマウス、犬とハムスターを殺さなければなりません。


変数の値を変更しないでください。コードが正しいことを証明しやすくなるためです(関数型プログラミングを参照)。ただし、そうする場合は、一度だけ言うべきではありません。

1
ctrl-alt-delor

他の回答はより一般的なケースを対象としていますが、別の理由もあります。一部のプログラミング言語では、オーバーロードされる可能性があります。例えば スカラ


小さなScalaレッスン:

var j = 5 #Creates a variable
j += 4    #Compiles

val i = 5 #Creates a constant
i += 4    #Doesn’t compile

クラスが+演算子のみを定義する場合、x+=yは確かにx=x+yのショートカットです。

ただし、クラスが+=をオーバーロードした場合は、次のようにはなりません。

var a = ""
a += "This works. a now points to a new String."

val b = ""
b += "This doesn’t compile, as b cannot be reassigned."

val c = StringBuffer() #implements +=
c += "This works, as StringBuffer implements “+=(c: String)”."

さらに、演算子+および+=は2つの別個の演算子です(これらだけではありません:+ a、++ a、a ++、a + b a + = bも異なる演算子です)。演算子のオーバーロードが利用可能な言語では、これは興味深い状況を作成する可能性があります。上記のように、+演算子をオーバーロードして追加を実行する場合は、+=もオーバーロードする必要があることに注意してください。

1
flying sheep