class Count:
def __init__(self, count=0):
self.count = count
def increment(c, times):
c.count += 1
print('c:', c.count)
times += 1
print('t:', times)
def main():
c = Count()
times = 0
for i in range(100):
increment(c, times)
main()
c是引用傳遞,times是值傳遞
class Count:
def __init__(self, count=0):
self.count = count
def increment(c, times):
c.count += 1
# print('c:', c.count)
print('c:', id(c))
times += 1
# print('t:', times)
print('t:', id(times))
if __name__ == '__main__':
c = Count()
times = 0
print('times:', id(times))
for i in range(100):
increment(c, times)
可以清晰的看到times和increment里的times不是同一個(gè)
同意 @dokelung 的答案,準(zhǔn)確來(lái)說(shuō)不是傳引用和傳值,是call by sharing(本質(zhì)上是傳值,不過(guò)這個(gè)值是引用的拷貝),所謂的傳值在這里不是發(fā)生在函數(shù)調(diào)用的時(shí)候,而是在遞增賦值,引用和內(nèi)存的變動(dòng)情況(使用id追蹤):
main:
c -> [一塊Count對(duì)象的內(nèi)存] main.c -> c1 內(nèi)存
c.count -> [默認(rèn)值初始化為常量0的內(nèi)存] main.c.count -> '0' 內(nèi)存
times -> [常量0的內(nèi)存] main.times -> '0' 內(nèi)存 (你沒(méi)有看錯(cuò),指向同一塊內(nèi)存)
increment: 第1次調(diào)用
increment.c -> c1 內(nèi)存
increment.c.count -> '0' 內(nèi)存
increment.times -> '0' 內(nèi)存
increment.c.count += 1 increment.c.count -> '1' 的 內(nèi)存
increment.times += 1 increment.times -> '1' 的 內(nèi)存 (你沒(méi)看錯(cuò),還真是這樣的)
第一次調(diào)用結(jié)束:
increment.c -> c1 內(nèi)存 咱沒(méi)變,main.c -> c1 內(nèi)存 沒(méi)變
increment.c.count -> '1' 內(nèi)存 變了
increment.times -> '1' 內(nèi)存 變了,main.times -> '0' 內(nèi)存 沒(méi)變
-------第一次調(diào)用結(jié)束-------
mian: 咱知道 c1 內(nèi)存,知道 '0' 內(nèi)存,但是 '1' 內(nèi)存???不存在的,咱不知道。
c1內(nèi)存: 咱知道'1'內(nèi)存,不告訴 mian。'0' 內(nèi)存?剛丟了。
-------第n次調(diào)用---------
increment: 第n次調(diào)用
increment.c -> c1 內(nèi)存 main 給我的
increment.c.count -> 'n-1' 內(nèi)存 c1 給我的
increment.times -> '0' 內(nèi)存 main 給我的
increment.c.count += 1 increment.c.count -> 'n' 的 內(nèi)存
increment.times += 1 increment.times -> '1' 的 內(nèi)存 (哎,還是這個(gè))
基本類型(java)和字面常量(python、java) 是不可變的,這就是為什么只能重分配內(nèi)存,而不是在原內(nèi)存上修改。times,c.count 被重分配了內(nèi)存。c卻只是在原內(nèi)存上修改,c是對(duì)象而不是基本類型或者字面常量。
那么為什么 c.count 和 times 都指向過(guò)同一個(gè) '0' 和 '1' 內(nèi)存呢?因?yàn)橛?strong>常量池,小范圍數(shù)字(小于127???)和字面字符串會(huì)被常量池緩存,而不會(huì)每一次都申請(qǐng)一塊新的內(nèi)存。
懶了幾天, 這次就詳細(xì)的談一下關(guān)於函數(shù)調(diào)用時(shí)的參數(shù)傳遞問(wèn)題, 我已經(jīng)預(yù)見(jiàn)這個(gè)回答會(huì)相當(dāng)長(zhǎng)了...
這是一個(gè)很重要的概念, variable 與其 儲(chǔ)存(參考)的 value (object) 是兩個(gè)完全不同的東西:
variable 是一個(gè)抽象的概念
value 是一個(gè)實(shí)際存在的資料
variable 不是其對(duì)應(yīng)的 value
vairable 儲(chǔ)存(參考)其對(duì)應(yīng)的 value
舉了例子:
a = 5
我們說(shuō) a 是一個(gè) variable, 他參考到 5 這個(gè)整數(shù), a 不是 5, 5 不是 a, 那究竟 variable a 是什麼呢?
我們可以把他想成一個(gè) box (在 c/c++ 中這樣的想像比較妥當(dāng)), 他 儲(chǔ)存 著 value,又或者我們可以把他想像成一個(gè)標(biāo)籤(在 python 中可以這樣想像), 他 參考 到對(duì)應(yīng)的 value。
variable 跟 value 是兩回事, 但他們有 指涉關(guān)係
id
id 常常會(huì)有直覺(jué)上的誤解, 因?yàn)槲覀兂3?huì)使用 variable 作為 id 的 argument:
>>> a = 5
>>> id(a)
140035495798656
>>> a = 6
>>> id(a)
140035495798688
>>> id(5)
140035495798656
>>> id(6)
140035495798688
但從上面的例子可以看出來(lái), 當(dāng)我們使用 id(a) 的時(shí)候, 我們?cè)儐?wèn)的不是 a 的 id, 而是 a 對(duì)應(yīng)到的 value 5 的 id, 這也是為什麼當(dāng) a = 6 的 assign 發(fā)生之後, id(a) 的結(jié)果不同了。
id 問(wèn)的是 value 的 id 不是 variable 的 id
(3) 這個(gè)標(biāo)題中提到兩種不同的動(dòng)作(這是我自己亂叫的, 並非專有名詞, 慎之):
variable assignment 指的是變數(shù)的指派跟重新繫結(jié):
>>> a = []
>>> id(a)
140035460871496
>>> a = [1, 2, 3]
>>> id(a)
140035460871560
上面我們讓 a 這個(gè) variable 指涉到另外一個(gè) list, 原本的 empty list 並沒(méi)有被動(dòng)到, a 對(duì)應(yīng)的內(nèi)容之所以改變, 是因?yàn)槲覀冏屗麉⒖嫉搅藙e的 data。
data manipulating 指的是 data 本身進(jìn)行了一些操作, 例如:
>>> a = []
>>> id(a)
140035460871496
>>> a.extend([1, 2, 3])
>>> id(a)
140035460871496
這種操作會(huì)使得 value(data) 的內(nèi)容被改變, 但是 variable 還是指涉到同一個(gè) data。
P.S. 關(guān)於這兩種概念的討論和注意事項(xiàng)請(qǐng)見(jiàn) 評(píng)論 處我與 @chaneyzorn 的討論。
其實(shí)整個(gè) function call 的 pass way 只是 求值策略 Evaluation strateg 的一部分, 但我們今天重點(diǎn)就放在 function call 上面。
最常見(jiàn)的傳遞方式有三種:
我們一種一種來(lái)分析:
此種傳遞方式會(huì)在函數(shù)內(nèi)另起一個(gè)新的 variable, 並將 caller 傳入的 argument value 複製一份傳入, 此新 variable 將會(huì)指涉到這個(gè)複製的 value, 這個(gè)行為牽涉到兩個(gè)不同的 variable, 而行為上是傳遞 value 本身, 所以叫做 pass by value。
想像: 在被呼叫的函數(shù)中製造一個(gè)新的 box, 並將 caller box 中的內(nèi)容 copy 一份存到新的 box 中
舉個(gè) c++ 的例子:
void foo(int a) {
a = 6;
return;
}
int main() {
int a = 1;
foo(a);
return 0;
}
此處, main 中的 a 和 foo 中的 a 是兩個(gè)完全不同的 variable, 所以當(dāng)我們進(jìn)行 variable assignment (a = b) 的時(shí)候, 外面的 a 完全不會(huì)受到影響。同樣地, 因?yàn)?value 也是複製的, 如果我們進(jìn)行 data manipulating 也完全不會(huì)影響到函數(shù)以外的世界。
對(duì)兩種操作的影響: 不論在 function 中進(jìn)行 variable assignment 或是 data manipulating 都不會(huì)影響外界。
此種傳遞方式並不一定會(huì)在函數(shù)內(nèi)另起一個(gè)新 variable (要看語(yǔ)言), 但是 caller
中的 argument value 一定不會(huì)被複製。這種傳遞方式通常是被 特殊的語(yǔ)法 或是 模擬方法 所實(shí)現(xiàn)。
比如說(shuō), 在 c++ 中, 預(yù)設(shè)的傳遞方法是 call by value, 但他提供了特殊的參照語(yǔ)法讓我們得以實(shí)現(xiàn) pass by referece, 讓我們看一個(gè)例子:
void foo(int& a) {
a = 6;
return;
}
int main() {
int a = 1;
foo(a);
return 0;
}
在這個(gè)例子中, foo 中的 a 與 main 中的 a 是同一個(gè) variable, 如果這邊是一個(gè) object 而不是整數(shù), 我們也能看見(jiàn)這個(gè) a 指涉到一樣的 value (這是當(dāng)然的, 因?yàn)檫@邊根本就只有一個(gè) variable a)。
另外像是 c, 預(yù)設(shè)的傳遞方法也是 call by value, 我們得利用指標(biāo)來(lái)模擬 pass by referece, 見(jiàn)下例 (出自 wiki):
void modify(int p, int* q, int* r) {
p = 27; // passed by value: only the local parameter is modified
*q = 27; // passed by value or reference, check call site to determine which
*r = 27; // passed by value or reference, check call site to determine which
}
int main() {
int a = 1;
int b = 1;
int x = 1;
int* c = &x;
// a is passed by value
// b is passed by reference by creating a pointer
// c is a pointer passed by value
// b and x are changed
modify(a, &b, c);
return 0;
}
這個(gè)例子展示了 c 中完整的 pass 方法, a 是個(gè)整數(shù), 使用 call by value 的方式傳遞, p 是一個(gè)全新的 variable, 於是我們對(duì)此 variable 重新指派不影響外面的 a 指涉的值。b 的傳遞透過(guò)指標(biāo)來(lái)進(jìn)行,此處的 q 雖然是全新的 variable, 但是 *q 卻是指涉到原本的值且對(duì) *q 的重指派等若對(duì)於 b 的重指派, 若此處 b 為一個(gè) object, 我們?cè)?modify 中對(duì) *q 指涉到的 object 作 data manipulating 一樣會(huì)影響到外面 b 指涉到的 object (因?yàn)樗麄兪峭粋€(gè))。x 跟 c 的部分就請(qǐng)大家自行參考, 不再多言。
這種藉由 pointer (address) 來(lái)模擬 pass by reference 的方法也被稱為 pass by address。
想像: 將 caller box 和其中的內(nèi)容直接傳遞到 function
對(duì)兩種操作的影響: 不論在 function 中進(jìn)行 variable assignment 或是 data manipulating 都會(huì)影響外界。
有人把這種傳遞方式稱作 pass by object reference, 但在 wiki 中是被叫做 call by sharing, 此方式由 Barbara Liskov 命名 ,並被 Python、Java(物件類型)、JavaScript、Scheme、OCaml 等語(yǔ)言使用。
此方式會(huì)在函數(shù)中製造一個(gè)新的 variable, 但是傳入的 value 卻不會(huì)複製, 而是讓裡面的新 variable 指涉到一樣的 value (data)。
想像: 在被呼叫的函數(shù)中製造一個(gè)新的 box, 但不複製 caller box 中的內(nèi)容, 而是裝載同一份內(nèi)容
請(qǐng)看以下例子:
def foo(lst):
lst.append(1)
lst = [2]
m = []
foo(m)
print(m)
你覺(jué)得 print(m) 的結(jié)果是什麼呢? 答案是 [1], 原因很簡(jiǎn)單, foo 中的 lst 一開(kāi)始指涉到外面?zhèn)魅氲?empty list, 所以當(dāng)我們 lst.appedn(1) 時(shí), lst 參考到的 list 跟外面的 m 參考到的 list 都被更動(dòng)為 [1], 但 lst = [2] 卻不會(huì)影響外界, 因?yàn)?lst 和 m 是兩個(gè)不同的 variable, 我們只是將 lst 重新指派到一個(gè)新的 list 而已。
對(duì)兩種操作的影響: 在 function 中進(jìn)行 variable assignment 的時(shí)候不會(huì)影響外界, 但是作 data manipulating 會(huì)影響外界。
所以 python 並不會(huì)因?yàn)?data 是可變還是不可變的因素去決定是傳值還是傳參, 因?yàn)樗疾皇? 他自有其特殊的傳遞方式:
class Count:
def __init__(self, count=0):
self.count = count
def increment(c, times):
c.count += 1
# print('c:', c.count)
print('c:', id(c))
times += 1
# print('t:', times)
print('t:', id(times))
if __name__ == '__main__':
c = Count()
times = 0
print('times:', id(times))
for i in range(100):
increment(c, times)
之所以 c.count 改變而 times 不變, 並非因?yàn)?c 是傳參, times 傳值, 而是對(duì)於 pass by sharing 來(lái)說(shuō), 以 argument c 的角度而言, c.count += 1 是一個(gè) data manipulating 的動(dòng)作, 我們操作了與外界同一份 data 所以影響了外面的 c, 而 times += 1 以 argument times 的角度而言 是一個(gè) variable assignment 的動(dòng)作, 我們操作了與外界不同一個(gè)的 variable, 所以不會(huì)影響外面。
我們來(lái)看一下上面的實(shí)驗(yàn), 在外面的 id(times) 問(wèn)的是 0 的 id, 而在 increment 中問(wèn)的都是 1 的 id, 所以才會(huì)第一個(gè) id(times) 與剩下來(lái)的 id(times) 不同, 但剩下來(lái)的 id(times) 皆相同。卻不是因?yàn)橥饷娴?times 跟裡面的 times 是不一樣的 variable 所以才會(huì)這樣的, 這裡的 times 沒(méi)有傳值, 而是共享值。
要怎麼突破這個(gè)盲點(diǎn)呢? 可以加上一行來(lái)測(cè)試:
def increment(c, times):
print('t begin:', id(times))
c.count += 1
...
結(jié)果:
times: 140303106782944
t begin: 140303106782944
c: 140303062368832
t: 140303106782976
t begin: 140303106782944
c: 140303062368832
t: 140303106782976
t begin: 140303106782944
c: 140303062368832
t: 140303106782976
t begin: 140303106782944
你會(huì)發(fā)現(xiàn) times 跟 t begin 一樣, 值沒(méi)有被複製, 至於 t 為何最後變了, 那是因?yàn)?times += 1 讓 times 指到別人了。
所以, 別再說(shuō) python 是 pass by value 或是 pass by reference 了, 都不是!!
python 是完完全全的 call by sharing!
我回答過(guò)的問(wèn)題: Python-QA
北大青鳥(niǎo)APTECH成立于1999年。依托北京大學(xué)優(yōu)質(zhì)雄厚的教育資源和背景,秉承“教育改變生活”的發(fā)展理念,致力于培養(yǎng)中國(guó)IT技能型緊缺人才,是大數(shù)據(jù)專業(yè)的國(guó)家
達(dá)內(nèi)教育集團(tuán)成立于2002年,是一家由留學(xué)海歸創(chuàng)辦的高端職業(yè)教育培訓(xùn)機(jī)構(gòu),是中國(guó)一站式人才培養(yǎng)平臺(tái)、一站式人才輸送平臺(tái)。2014年4月3日在美國(guó)成功上市,融資1
北大課工場(chǎng)是北京大學(xué)校辦產(chǎn)業(yè)為響應(yīng)國(guó)家深化產(chǎn)教融合/校企合作的政策,積極推進(jìn)“中國(guó)制造2025”,實(shí)現(xiàn)中華民族偉大復(fù)興的升級(jí)產(chǎn)業(yè)鏈。利用北京大學(xué)優(yōu)質(zhì)教育資源及背
博為峰,中國(guó)職業(yè)人才培訓(xùn)領(lǐng)域的先行者
曾工作于聯(lián)想擔(dān)任系統(tǒng)開(kāi)發(fā)工程師,曾在博彥科技股份有限公司擔(dān)任項(xiàng)目經(jīng)理從事移動(dòng)互聯(lián)網(wǎng)管理及研發(fā)工作,曾創(chuàng)辦藍(lán)懿科技有限責(zé)任公司從事總經(jīng)理職務(wù)負(fù)責(zé)iOS教學(xué)及管理工作。
浪潮集團(tuán)項(xiàng)目經(jīng)理。精通Java與.NET 技術(shù), 熟練的跨平臺(tái)面向?qū)ο箝_(kāi)發(fā)經(jīng)驗(yàn),技術(shù)功底深厚。 授課風(fēng)格 授課風(fēng)格清新自然、條理清晰、主次分明、重點(diǎn)難點(diǎn)突出、引人入勝。
精通HTML5和CSS3;Javascript及主流js庫(kù),具有快速界面開(kāi)發(fā)的能力,對(duì)瀏覽器兼容性、前端性能優(yōu)化等有深入理解。精通網(wǎng)頁(yè)制作和網(wǎng)頁(yè)游戲開(kāi)發(fā)。
具有10 年的Java 企業(yè)應(yīng)用開(kāi)發(fā)經(jīng)驗(yàn)。曾經(jīng)歷任德國(guó)Software AG 技術(shù)顧問(wèn),美國(guó)Dachieve 系統(tǒng)架構(gòu)師,美國(guó)AngelEngineers Inc. 系統(tǒng)架構(gòu)師。