標籤:

Python 中 a+=b 和 a=a+b 的區別有哪些?

問題詳細描述在這裡 python問題求助

在看apriori的演算法,決定自己練練手,寫寫代碼。最近又在,所以準備用python實現。其中一個子過程是要求候選項Ck的k項子集。在這個求子集的方法中遇到了問題了,並且很神奇。最開始一直找不到問題所在,當單步調試發現問題所在之處時卻倍感神奇。下面用代碼示例來說明下這個問題的神奇之處。

# coding=UTF-8

"""

Created on Mar 11, 2012

@author: tanglei|www.tanglei.name

"""

elements=[["1"],["2"],["3"]]

def getSubset(k,size):

subset=[]

if k == 1:

subset += (elements[0:size])

return subset

else:

i = size - 1

while i &>= k-1 :

set = getSubset(k-1,i)

j = 0

while j &< len(set): #Attention a+=b a=a+b #set[j] += (elements[i]) #Why Elements change here? set[j] = set[j] + (elements[i]) j += 1 subset += (set) i -= 1 return subset print("elements:",elements) test = getSubset(2,len(elements)) print(test) print("elements",elements)

本例中是求集合[["1"],["2"],["3"]]的2-itemset子集。 看如上代碼中,紅色那句先注釋掉,用a=a+b的形式,運行結果如下。這個結果是正確的,為{1,3}、{2,3}、{1,2}。

若將代碼中換成a+=b的形式,即去掉藍色代碼部分,換成紅色的代碼。意料中,應該結果一樣。但實際結果卻出乎意料。

不但子集沒求對,就連原始的集合elements都被改變了。看了好久搞不定是哪裡出了問題了。之前對a+=b和a=a+b的印象僅僅停留在涉及一個強制類型轉換的原因,難道這個也是嗎?搞不懂,像高人求助……


a+=b

&>&>&> a1 = range(3)
&>&>&> a2 = a1
&>&>&> a2 += [3]
&>&>&> a1
[0, 1, 2, 3]
&>&>&> a2
[0, 1, 2, 3]

a=a+b

&>&>&> a1 = range(3)
&>&>&> a2 = a1
&>&>&> a2 = a2 + [3]
&>&>&> a1
[0, 1, 2]
&>&>&> a2
[0, 1, 2, 3]

顯然,兩者是有區別的,而這種區別只出現在可變對象上(為什麼是可變對象後面再說),是什麼原因造成了兩者的區別呢?

+= 操作調用 __iadd__方法,沒有該方法時,再嘗試調用__add__方法

a1 = [0, 1, 2]
a1 += [3]
# 等價於
a1.__iadd__([3])
print(a1) #[0, 1, 2, 3]

__iadd__方法直接在原對象a1上進行更新,該方法的返回值為None

+ 操作調用__add__方法

a1 = [0, 1, 2]
a1 = a1 + [3]
# 等價於
a1 = a1.__add__([3])

__add__方法會返回一個新的對象,原對象不修改,因為這裡 a1被重新賦值了,a1指向了一個新的對象,所以出現了文章開頭a1不等於a2的情況

a1 = [0, 1, 2]
print(a1.__add__([3])) # [0, 1, 2, 3]
print(a1) # [0, 1, 2]

為什麼前面我說這種差異只會發生的可變對象身上?因為對於不可變對象,根本沒有 __iadd__方法,所以+=和+的效果是一樣的,因為調的都是 __add__ 方法

以上~,不知我寫清楚了沒有,不妨關注一下公眾號:Python之禪


綜合了幾位仁兄的回答和疑問,我想還是用以下代碼來回答吧,希望有幫助。

問題1. int和list是不一樣的,代碼為證:

&>&>&> a=1

&>&>&> b=a

&>&>&> a+=1

&>&>&> a,b

(2, 1)

&>&>&> a=[1,2,3,4]

&>&>&> b=a

&>&>&> a+=[5]

&>&>&> a,b

([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])

通俗地講,類型為int時,a和b是「不一樣的」;類型為list時,a和b是「一樣的」。術語叫做immutable和mutable,具體原理在這個節點不必深究。

問題1.1. 我們通常運行b=a這一語句時,會直覺地認為,b和a已經不一樣了。代碼為證:

&>&>&> a=[[1],[2],[3],[4]]

&>&>&> b+=a[0:2]

&>&>&> b

[1, 2, 3, 4, [1], [2]]

&>&>&> a=[[1],[2],[3],[4]]

&>&>&> b=[]

&>&>&> b+=a[0:2]

&>&>&> a,b

([[1], [2], [3], [4]], [[1], [2]])

&>&>&> b[0]

[1]

&>&>&> b[0][0]="changed!"

&>&>&> # You don"t expect a to change

&>&>&> # However

&>&>&> a, b

([["changed!"], [2], [3], [4]], [["changed!"], [2]])

可以看到,a[0]的[1]和b[0]的[1]是「一樣的」,因為改變b[0]就會改變a[0](注意不是改變b,是改變b[0]。改變b不會對a有任何影響)

問題2. list的情況下,a+=b和a=a+b是不一樣的,代碼為證:

&>&>&> a=[1,2,3,4]

&>&>&> b=a

&>&>&> a+=[5]

&>&>&> a,b

([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])

&>&>&> a=[1,2,3,4]

&>&>&> b=a

&>&>&> a=a+[5]

&>&>&> a,b

([1, 2, 3, 4, 5], [1, 2, 3, 4])

同樣通俗地講,在+=的情況下,a還是原來的a,和b「一樣」;在+的情況下,a已經不是原來的a了,和b「不一樣」。

問題3. 如果要讓+=和+行為一致,應該怎麼做?代碼為證:

&>&>&> import copy

&>&>&> a=[1,2,3,4]

&>&>&> b=copy.deepcopy(a)

&>&>&> a+=[5]

&>&>&> a,b

([1, 2, 3, 4, 5], [1, 2, 3, 4])

這與問題2中a=a+b的情況結果一致了。當對list進行b=a時,實際上進行的是「引用」操作;只有使用b=copy.deepcopy(a)才是進行我們通常期望的「拷貝」操作。

問題4. 回到問題中的代碼,當k=1時,以下代碼:

subset += (elements[0:size])

根據問題1.1,subset與elements是「一樣的」,因此未來改變subset的元素的操作有可能改變elements的元素

到這行代碼時,注意set就是遞歸傳遞過來的subset:

#set[j] += (elements[i]) #Why Elements change here?

set[j] = set[j] + (elements[i])

根據問題2,+=中set[j]依然是原來的set[j],也就可能是elements的元素。因此

set[j] += elements[i]

可能會等價於

elements[*] += elements[i]

一旦改變了elements的元素,結果自然就不對了。

怎麼解決這個問題?根據問題3,只要保證set與elements是「不一樣的」,就符合程序的邏輯。因此將

subset += (elements[0:size])

改為(記得import copy)

subset += copy.deepcopy(elements[0:size])

就能在+=的情況下正常運行了。

總結:在python中,list類型的賦值b=a進行的引用操作,而非拷貝操作,在需要拷貝操作時,需要加上b=copy.deepcopy(a)。(copy.copy和copy.deepcopy的區別超出問題範疇,有興趣可以google)


check this

http://docs.python.org/reference/simple_stmts.html#assignment-statements

關於assignment-statements 和 augmented assignment-statements兩段解釋。

可以將=理解為名字綁定,= assignment-statements只是將右邊(文檔裡面的RHS, abbr. for right-hand-side)的表達式的結果(值或對象)綁定到 =左邊的名字上。 而+=之類augmented assignment-statements是對左邊的對象的in-place操作(when possible)。


沒有看python實現源碼, 不太清楚具體原理, 猜想應該和這幾個容器類對象實現的__add____iadd__++= 的實現方式不一樣. int 和 string 作為值不可變對象在這兩個方法的實現應該是一致的.

其實調試一下, 如下圖, 看id的變動就可以得知.

令有興趣可參見http://docs.python.org/reference/datamodel.html


在Python列表操作符中:

  1. "+"代表連接操作,其結果是創建了一個新的列表。
  2. 』+=『是在Python2.0中添加的替換連接操作,顧名思義,等價於extend()方法,實際上是把新列表添加到了舊有列表裡。


感覺上面的答案不是回答這道題啊;

在python中,不同的情況下,這兩個表達式有著很大的區別,如果a,b都是可變對象,例如list,a+=b實際是對a指向的地址上的值進行修改,即運算前後id(a)的值是不變的。而a=a+b是不同的,首先計算等號左邊a+b得到新的值,然後a再指向這個新的值。即運算後id(a)發生了改變; 但是對於不可變對象,例如int,str等,這兩個表達式是一樣的效果。即運算前後id(a)的值一定會發生改變。 道理很簡單,不可變對象指向的地址上的值不能發生改變,只能在新的地址上存運算後的值。


補充一個,

從位元組碼看的話,

&>&>&> def fun():
... a = 1
... a += 2
...

import dis
&>&>&> dis.dis(fun)
2 0 LOAD_CONST 1 (1)
3 STORE_FAST 0 (a)

3 6 LOAD_FAST 0 (a)
9 LOAD_CONST 2 (2)
12 INPLACE_ADD
13 STORE_FAST 0 (a)
16 LOAD_CONST 0 (None)
19 RETURN_VALUE
==========================================================
&>&>&> def fun2():
... a = 1
... a = a+2
...

&>&>&> dis.dis(fun2)
2 0 LOAD_CONST 1 (1)
3 STORE_FAST 0 (a)

3 6 LOAD_FAST 0 (a)
9 LOAD_CONST 2 (2)
12 BINARY_ADD
13 STORE_FAST 0 (a)
16 LOAD_CONST 0 (None)
19 RETURN_VALUE

也可以看出=是binary add, +=是inplace add,也可以理解.

還有一個就是當不可變對象容器(tuple)中使用了可變對象元素的話,如

&>&>&> t = (1,2,[2,3])

&>&>&> t = (1,2,[2,3])
&>&>&> t[2] = t[2] + [3,3]
Traceback (most recent call last):
File "&", line 1, in &
TypeError: "tuple" object does not support item assignment
&>&>&> t
(1, 2, [2, 3])

============================
&>&>&> t[2] += [3,3]
Traceback (most recent call last):
File "&", line 1, in &
TypeError: "tuple" object does not support item assignment
&>&>&> t
(1, 2, [2, 3, 3, 3])

會發現雖然報錯結果是相同的,但是執行t[2]+= 之後t[2]在賦值操作之前已經被改變.

「增量賦值不是一個原子操作。雖然拋出了異常,但還是完成了操作」

摘錄來自: [巴西] Luciano Ramalho. 「流暢的Python」。

Visualize Python, Java, JavaScript, TypeScript, and Ruby code execution, 這個網站可以對 Python 運行原理進行可視化分析. 可以運行代碼查看右邊的可視化效果.

摘錄來自: [巴西] Luciano Ramalho. 「流暢的Python」。 iBooks.


&>&>&> def f(a,b): a+=b
...
&>&>&> def g(a,b): a=a+b
...
&>&>&> dis.dis(f)
1 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 INPLACE_ADD
7 STORE_FAST 0 (a)
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
&>&>&> dis.dis(g)
1 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_ADD
7 STORE_FAST 0 (a)
10 LOAD_CONST 0 (None)
13 RETURN_VALUE


《Python學習手冊》里賦值語法那一節已經講得很清楚了

+=對於可變對象來說,以最小動作為基礎,是在原處修改的

另外的是生成了一個新的對象


如下題主的問題無關,僅從這兩個表達式的性能上進行參考:

#對於a+=b和a=a+b 的不同點,從性能上可以參考下:

import time

#第一種

result=[]

start=time.time()

for i in range(30000):

result += [i]

print(len(result),",",time.time()-start)

#第二種

result=[]

start=time.time()

for i in range(30000):

result = result + [i]

print(len(result),",",time.time()-start)

得到的結果可以看出,運行3萬次,

第一種運行時間為:0.015625476837158203

第二種運行時間為:2.546924591064453

可見,在某些演算法場景下,需要選擇適當的表達式.


推薦一本《流暢的python》,目前正在學,博大精深啊


因為python的list上a+=b操作,是調用 a. __iadd__(b)實現,而 __iadd__在CPython里實現大約是:

list_inplace_concat(PyListObject *self, PyObject *other)
{
PyObject *result;
result = listextend(self, other);//調用extend,in-place擴展
if (result == NULL)
return result;
Py_DECREF(result);
Py_INCREF(self);
return (PyObject *)self;//返回值是擴展後的列表
}

所以實際上調用extend改變了列表本身。


Why does += behave unexpectedly on lists?


Python 3.6.0 (v3.6.0:41df79263a11, Dec 23 2016, 07:18:10) [MSC v.1900 32 bit (Intel)] on win32

Type "copyright", "credits" or "license()" for more information.

&>&>&> a1 = range(3)

&>&>&> a2 = a1

&>&>&> a2 += [3]

Traceback (most recent call last):

File "&

", line 1, in &

a2 += [3]

TypeError: unsupported operand type(s) for +=: "range" and "list"

&>&>&>


推薦閱讀:

現在哪些編程技術比較火爆。需求量很大且待遇看漲?
你用 Python 做過什麼有趣的數據挖掘/分析項目?
python程序員路線圖?
用 Python 可以建網站嗎?
為什麼在python3里b=a=1是合理表達式,而print(a=1)卻不是。a=1為什麼沒有返回值?

TAG:Python |