我不知道这些实际上是什么,但我一直都在看到它们。Python的实现类似于:
x += 5
作为的简写x = x + 5
。
但是,为什么这被认为是好的做法?我在阅读过的每本有关Python,C,R等的书籍或编程教程中都涉及到它。我知道这很方便,可以节省三个按键,包括空格。但他们似乎总是我绊倒,当我阅读代码,至少在我看来,使它少可读性,而不是更多。
我是否遗漏了一些在所有地方都使用过的明显原因?
我不知道这些实际上是什么,但我一直都在看到它们。Python的实现类似于:
x += 5
作为的简写x = x + 5
。
但是,为什么这被认为是好的做法?我在阅读过的每本有关Python,C,R等的书籍或编程教程中都涉及到它。我知道这很方便,可以节省三个按键,包括空格。但他们似乎总是我绊倒,当我阅读代码,至少在我看来,使它少可读性,而不是更多。
我是否遗漏了一些在所有地方都使用过的明显原因?
Answers:
这不是速记。
该+=
符号在1970年代以C语言出现,并且-具有C的“智能汇编程序”思想,对应于明显不同的机器指令和地址模式:
像“ i=i+1
”,"i+=1
“”和“ ++i
”之类的东西虽然在抽象级别上产生相同的效果,但在较低级别上对应于处理器的不同工作方式。
特别是这三个表达式,假设i
变量驻留在存储在CPU寄存器中的内存地址中(以它的名字命名D
-将其视为“指向int的指针”),并且处理器的ALU接受一个参数并在一个“累加器”(我们称其为A-认为它是一个整数)。
有了这些限制(从那时开始,在所有微处理器中都很常见),转换很可能是
;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)
)进行操作或转换更复杂的表达式时(它们全部归结为堆栈中的推入低优先级操作,称为高优先级,弹出并重复进行,直到消除所有论点为止)。
第二个更典型的是“状态机”:我们不再是“求值表达式”,而是“运算值”:我们仍然使用ALU,但要避免在允许替换参数的结果周围移动值。在需要更复杂的表达的情况下,无法使用此类指令:i = 3*i + i-2
由于i
需要多次,因此无法就地操作。
第三个(甚至更简单)甚至没有考虑“加法”的概念,而是将更“原始”(在计算意义上)的电路用于计数器。由于对寄存器进行改造以使其成为计数器所需的组合网络较小,因此指令被缩短,加载更快并立即执行,因此比全加器之一更快。
使用当代的编译器(现在称为C),启用编译器优化,可以根据方便交换对应关系,但是语义上仍然存在概念上的差异。
x += 5
手段
但是x = x + 5
意味着:
当然,优化可以
&x
累加器,则可以删除两个副本因此,使优化的代码与该代码重合x += 5
。
但是,只有在“查找x”没有副作用的情况下才能这样做,否则
*(x()) = *(x()) + 5;
和
*(x()) += 5;
在语义上是不同的,因为将产生两次或一次x()
副作用(承认x()
是一个在周围做奇怪事情并返回的函数int*
)。
因此,x = x + y
和之间的等价关系x += y
是由于+=
和=
应用于直接I值的特殊情况。
要转向Python,它继承了C的语法,但是由于在解释语言中执行代码之前没有翻译/优化,因此事情不一定是如此紧密地联系在一起(因为减少了一个解析步骤)。但是,解释器可以针对三种类型的表达式引用不同的执行例程,这取决于表达式的形成方式和评估上下文,从而利用不同的机器代码。
对于谁喜欢更多细节...
每个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
操作码,则第一个代码将成为第二个:没有办法。
如果存在这样的操作码并启用了优化,则在消除了反向移动并调整了寄存器操作码之后,第二个表达式将成为第一个表达式。
根据您的想法,它实际上更容易理解,因为它更简单。举个例子:
x = x + 5
调用“取x,将x加5,然后将该新值分配回x”的心理处理。
x += 5
可以被认为是“ x增加5”
因此,这不仅仅是速记,它实际上更直接地描述了功能。当阅读大量的代码时,它更容易掌握。
x = x + 5
但仍然困扰着我。当我晚些时候上数学时,这让我更加困扰。使用x += 5
明显更具描述性,并且作为表达更有意义。
reallyreallyreallylongvariablename = reallyreallyreallylongvariablename + 1
某些情况下,变量的名称也很长:...哦,不!!!错字
至少在Python中,x += y
并且x = x + y
可以做完全不同的事情。
例如,如果我们这样做
a = []
b = a
然后a += [3]
将导致a == b == [3]
,而a = a + [3]
将导致a == [3]
和b == []
。也就是说,在创建新对象并将变量绑定到它的+=
同时,就地修改对象(可能会做到,您可以定义该__iadd__
方法执行几乎所有您喜欢的事情)=
。
在使用NumPy进行数值运算时,这一点非常重要,因为您经常会最终获得对数组不同部分的多个引用,并且确保您不要无意间修改了数组中其他引用的部分,这一点很重要,或不必要地复制数组(这可能非常昂贵)。
__iadd__
:在某些语言中,您可以创建对可变数据结构的不变引用,并在其中operator +=
定义,例如scala:val sb = StringBuffer(); lb += "mutable structure"
vs var s = ""; s += "mutable variable"
。进一步修改了数据结构的内容,而后者又将变量指向新的结构。
这叫做成语。编程习惯用语很有用,因为它们是编写特定编程结构的一致方式。
每当有人写的时候,x += y
您就会知道它x
会被递增y
而不是一些更复杂的操作(作为一种最佳实践,通常,我不会混合使用更复杂的操作和这些语法速记)。当增加1时,这是最有意义的。
++x
和x+=1
在C和Java(也许也为C#)中是等效的,但由于存在复杂的运算符语义,因此在C ++中不一定如此。关键是它们都评估x
一次,将变量加一,并且得到的结果就是评估后变量的内容。
++x + 3
与相同x += 1 + 3
。用括号括起来x += 1
,它是相同的。就其本身而言,它们是相同的。
++x + 3
,x += 1 + 3
或者(x += 1) + 3
具有未定义的行为(假定结果值“适合”)。
为了使@Pubby的观点更清楚一点,请考虑 someObj.foo.bar.func(x, y, z).baz += 5
没有+=
操作员,有两种方法:
someObj.foo.bar.func(x, y, z).baz = someObj.foo.bar.func(x, y, z).baz + 5
。这不仅冗长而且冗长,而且速度较慢。因此,人们将不得不tmp := someObj.foo.bar.func(x, y, z); tmp.baz = tmp.bar + 5
。可以,但是简单的事情会带来很多噪音。这实际上与运行时发生的情况非常接近,但是编写起来很繁琐,仅使用它就+=
会将工作转移到编译器/解释器。+=
和其他此类运营商的优势是不可否认的,而习惯它们只是时间问题。
的确,它更短,更容易,并且它确实是受底层汇编语言启发的,但它的最佳实践是,它可以防止整个类的错误,并且可以更轻松地检查代码并确保它能做什么。
用
RidiculouslyComplexName += 1;
由于只涉及一个变量名,因此您可以确定该语句的作用。
用 RidiculouslyComplexName = RidiculosulyComplexName + 1;
始终怀疑双方是完全一样的。您看到错误了吗?当存在下标和限定词时,情况甚至更糟。
有关为什么这些运算符以“ C风格”语言开头的一些见解,摘录自34年前的K&R 1st Edition(1978):
除了简洁之外,赋值运算符还具有与人们的思维方式更好地相对应的优势。我们说“将i加2”或“将i加2”,而不是“将i加2,然后将结果放回i”。这样
i += 2
。另外,对于像yyval[yypv[p3+p4] + yypv[p1+p2]] += 2
赋值运算符使代码更易于理解,因为读者不必费力地检查两个长表达式的确相同,或者想知道为什么它们不是这样。赋值运算符甚至可以帮助编译器生成更有效的代码。
我认为这段话很明显,Brian Kernighan和Dennis Ritchie(K&R)相信复合赋值运算符有助于提高代码的可读性。
自K&R撰写此书以来已有很长时间了,此后,关于人们应该如何编写代码的许多“最佳实践”已经改变或发展。但是这个程序员问题是我第一次回想起有人对复合赋值的可读性提出抱怨,所以我想知道是否有很多程序员认为它们是一个问题?再说一次,当我键入此命令时,该问题有95个否决票,因此也许人们在阅读代码时确实会感到不快。
除了可读性之外,它们实际上还做不同的事情:+=
不必两次评估其左操作数。
例如,expr = expr + 5
将expr
两次评估(假设expr
是不纯的)。
expr = expr + 5
和expr += 5
expr
没有副作用。
expr
有副作用,则expr = expr + 5
必须两次调用这些副作用。
volatile
的副作用,x=x+5
而x+=5
当具有相同的副作用x
是volatile
它的键入要短得多。它涉及较少的运算符。它具有较小的表面积和较少的混淆机会。
这是一个人为的示例,我不确定实际的编译器是否实现了这一点。x + = y实际上使用一个参数和一个运算符并在适当位置修改x。x = x + y可以具有x = z的中间表示形式,其中z是x + y。后者使用两个运算符(加法和赋值)以及一个临时变量。单个运算符非常清楚地表明,值方不能是y以外的任何值,并且不需要解释。从理论上讲,可能会有一些花哨的CPU,其正等号运算符的运行速度比串联的正号运算符和赋值运算符要快。
ADD
许多CPU上的指令都有一些变体,可以使用其他寄存器,内存或常量作为第二个加数直接在寄存器或内存上进行操作。并非所有组合都可用(例如,将内存添加到内存),但是有足够的用处。无论如何,有一个体面的优化的编译器会知道生成相同的代码x = x + y
,因为它会为x += y
。
这是一个很好的成语。是否更快取决于语言。在C语言中,它更快,因为它可以转换为在右侧增加变量的指令。包括Python,Ruby,C,C ++和Java在内的现代语言均支持op =语法。它体积小巧,您很快就会习惯。由于您会在其他人的代码(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.
x+=5
中,意义不如x=x+5
;例如在Haskell后者也不会引起x
由5递增-代替它即被一个无限递归循环。谁想要这样的简写?
+=
语言无关,而与处理器有关。例如,X86是两个地址的体系结构,它仅+=
本地支持。之所以a = b + c;
必须这样编译一条语句,是a = b; a += c;
因为没有指令可以将加法的结果放在除求和项之一之外的其他任何地方。相比之下,Power体系结构是三地址体系结构,没有用于的特殊命令+=
。在此体系结构上,语句a += b;
和a = a + b;
始终编译为相同的代码。
+=
当您将变量用作累加器(即运行总计)时,诸如此类的运算符非常有用:
x += 2;
x += 5;
x -= 3;
比以下内容容易阅读:
x = x + 2;
x = x + 5;
x = x - 3;
在第一种情况下,从概念上讲,您正在修改中的值x
。在第二种情况下,您要计算一个新值并将其分配给x
每次。尽管您可能永远也不会写出那么简单的代码,但是想法还是一样的……重点是您对现有值所做的工作,而不是创建一些新的值。
其他答案针对更常见的情况,但是还有另一个原因:在某些编程语言中,它可能会超载。例如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)”."
另外,运算符+
和+=
是两个单独的运算符(不仅是:+ a,++ a,a ++,a + ba + = b也是不同的运算符);在可能出现运算符重载的语言中,这可能会产生有趣的情况。如上所述,如果+
要使运算符重载以执行加法运算,请记住+=
也必须重载。
+=
然后定义a+b
为“制作副本a
,b
使用粘贴+=
,然后返回a
”。
x += 5
不是x = x + 5
?还是您所建议的只是语法糖?