從 JavaScript 角度學 Python(27) - 傳值?傳參考?

前言

已經快到鐵人賽的結尾了,但是我現在才想到我好像少講了一個東西,就是關於傳值與傳參考的部分,所以這一篇就來聊一下關於傳值與傳參考吧。

傳值

首先先簡單聊一下純粹的傳值,在 JavaScript 中我們知道原始型別是單純傳遞一個值給另一個變數,因此修改時並不會影響原有的變數:

1
2
3
4
5
var a = 'Ray';
var b = a;
b = 'QQ';

console.log(a, b); // Ray, QQ

那話題拉回到 Python 中,Python 與 JavaScript 也是相同的嗎?我們可以直接來看一次範例:

1
2
3
4
5
a = 'Ray'
b = a
b = 'QQ'

print(a, b) # Ray, QQ

從結果來看,我們可以看到 Python 與 JavaScript 的結果是一樣的。

但是如果今天採用的是另一種方式來呈現的話結果會相同嗎?這邊就讓我們來看看另一種範例程式碼:

1
2
3
4
5
6
7
8
9
10
11
def fn(x, y):
cache = 100
x = cache
y = x

x = 10
y = 20

fn(x, y)

print(x, y) # 10, 20

我們可以看到在上面的範例程式碼中,我將全域的變數 xy 傳入到 fn 函式內,接下來在 fn 中宣告一個 cache = 100 的變數,而後面我做了重新賦予 xy 的行為,但是你會發現 fn 內的調整並不會影響全域變數的結果,這代表著什麼呢?代表著我們只是傳遞值 (x, y) 給 fn 函式的參數使用,因此函式內部的變化它並不會影響原本的變數。

或許這個時候你會很聰明的想到我在「從 JavaScript 角度學 Python(6) - 變數作用域」章節中有介紹一個「global」的方式可以針對外部變數做一些ㄌ調整,但是基本上如果你在這邊使用 global 方法的話是會出現錯誤的:

1
2
3
4
5
6
7
8
9
10
11
12
13
def fn(x, y):
cache = 100
global x, y # SyntaxError: name 'x, y' is parameter and global
x = cache
y = x


x = 10
y = 20

fn(x, y)

print(x, y) # 10, 20

而會出現「SyntaxError: name 'x, y' is parameter and global」這一段錯誤訊息的主要原因在於,函式的參數如果與 global 指定的變數相同的話,是會無法正常運作的,因為它會不知道你到底是要使用哪一個參數還是變數唷~

你不能這樣玩唷~

所以我們這邊可以大膽的推測…

「或許 Python 的數值型別、字串型別與布林型別會與 JavaScript 的原始型別類似?」

那麼關於這個推測我們可以試著實踐一次或許會更精準,透過前面幾個章節我們了解到容器型別類似於 JavaScript 的陣列與物件,所以這邊我們可以試著撰寫字串、布林與數值型別來測試看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def fn(w, x, y, z):
w = False
x = 10
y = 1.1
z = 'Ray'
print(w, x, y, z) # False, 10, 1.1, Ray


w = True # 布林
x = 9.9 # 浮點數
y = 'Ray' # 字串
z = 10 # 整數


fn(w, x, y, z)

print(w, x, y, z) # True, 9.9, Ray, 10

透過上面的結果來看,基本上 Python 的字串、布林與數值型別確實是與 JavaScript 的基本型別雷同,那字典與串列呢?別急,我們先接著下去看,但是這邊要注意我們是在講 Python 不是 JavaScript:

是 Python 不是 JavaScript

傳參考

接下來聊一下剛剛沒有提到的字典與串列的部分,雖然我們知道 Python 的字典與串列就是對應著 JavaScript 的物件與陣列,但實際上運作模式會是一樣嗎?所以這邊也來快速聊一下這一塊。

首先我們先來一段 JavaScript 很常見的物件傳參考的基本範例程式碼:

1
2
3
4
5
6
7
8
var b = {
name: 'Ray',
};

var e = b;

e.name = 'QQ';
console.log(b.name); // QQ

上面的觀念我們都知道這是一個物件傳參考概念,如果對於這一段有興趣的話可以閱讀我這一篇文章「JavaScript 核心觀念(27)-物件-物件參考觀念的實際運作模式」會有更詳細的說明,因此這邊就不著墨了(笑)。

被拐

前面講那麼多,那 Python 的狀況又如何呢?我們將上面的 JavaScript 範例程式碼直接改寫成 Python 版本試試看:

1
2
3
4
5
6
7
8
b = {
'name': 'Ray'
}

e = b

e['name'] = 'QQ'
print(b['name']) # QQ

好吧…依照輸出的結果來看, Python 的字典跟 JavaScript 的物件傳參考狀況非常像沒有錯吧~

接下來讓我們看看另一個範例程式碼:

1
2
3
4
5
6
7
8
9
10
def fn (x):
x['name'] = 'QQ'

b = {
'name': 'Ray'
}

fn(b)

print(b['name']) # QQ

上面這個範例程式碼確實沒有任何問題,感覺也是物件傳參考有非常大的關聯性,但是如果範例程式碼變成了以下,那麼結果會是怎樣呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
def fn (x):
x = {
'name': 'Ray'
}


b = {
'name': 'Ray'
}

fn(b)

print(b['name']) # Ray

疑?奇怪了,為什麼結果不一樣了?

我頭痛了

好吧,這邊要講一下關於物件(字典)傳參考的時候會有一個很特別的地方,雖然我們知道 Python 的字典基本上與 JavaScript 非常雷同,因此再將字典賦予到變數時,其實是賦予一個記憶體空間位置,我知道這邊有點難懂,所以我想換個方式形容,我們試著把容器型別與數值型別、字串型別以生活化的角度去理解看看。

而這邊會用車子去當作舉例,因此數值型別與字串型別你可以把它想像成是車子內的某些東西,例如:輪胎、方向盤,又或者收音機等等,字典與串列的話則是一個僅有車殼的車子(連引擎都沒有),所以透過這樣的舉例來講,可能就會像這樣:

1
2
3
4
5
6
# 把字典看成一個車殼,裡面放著車子的東西
car = {
'steeringWheel': 1, # 方向盤
'wheels': 4, # 輪胎
'radio': 1, # 收音機
}

那麼當我將 car 字典賦予到另一個變數時,其實概念類似我把車子借給他人使用,例如我借來了一台車:

1
2
3
4
5
6
7
car = {
'steeringWheel': 1,
'wheels': 4,
'radio': 1,
}

ray = car

所以當我對著車子有任何調整,可能拆掉一個輪胎,那就會影響到原本的車子,畢竟車子是借來的:

1
2
3
4
5
6
7
8
9
10
car = {
'steeringWheel': 1,
'wheels': 4,
'radio': 1,
}

ray = car
ray['wheels'] = 3

print(car['wheels']) # 3

那出借東西的行為就是 JavaScript 的物件傳參考,我只是借給你用而已的概念。

那麼有一種狀況下我們不會影響到原有的車子,也就是我後來終於有錢買自己的車子:

1
2
3
4
5
6
7
8
9
10
11
12
13
car = {
'steeringWheel': 1,
'wheels': 4,
'radio': 1,
}

ray = car

ray = {
'steeringWheel': 1,
'wheels': 4,
'radio': 1,
}

因為後來自己買了一台車子的關係,所以我就不再跟人家借車子,所以我接下來不管怎麼調整自己的車子都不會影響到原本 car 變數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
car = {
'steeringWheel': 1,
'wheels': 4,
'radio': 1,
}

ray = car

ray = {
'steeringWheel': 1,
'wheels': 4,
'radio': 1,
}

ray['radio'] = 0

print(car['radio']) # 1

上面的舉例只是希望讀者在閱讀時可以比較好理解,因此只要你看到變數被重新賦予一個字典 or 物件,就代表他會重新指向到一個新的記憶體空間(空車殼),如果這樣子還不好理解的話,以後你只要看到變數被重新賦予一個 {} or [] 就代表著它被重新指定了位址,因此就不會發生所謂的物件傳參考問題。

當然你也可以活用前面章節所學的 id 來查看彼此的記憶體空間是否相同:

1
2
3
4
5
6
7
8
9
car = {
'steeringWheel': 1,
'wheels': 4,
'radio': 1,
}

ray = car

print(id(ray), id(car)) # 4463682048, 4463682048

如果是重新賦予的話,則是不同的記億體位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
car = {
'steeringWheel': 1,
'wheels': 4,
'radio': 1,
}

ray = car

ray = {
'steeringWheel': 1,
'wheels': 4,
'radio': 1,
}

print(id(ray), id(car)) # 4418249408 4418249216

透過前面章節所學的某些技巧也可以幫助到你理解,因此前面章節所分享的觀念都是非常重要的唷~

老鐵記得三連

call by xxx

最後其實你應該會發現我一直刻意閃過一些東西不講,例如…call by xxx or pass by xxx 什麼的,最主要原因是我自己對於 Python 沒有非常的熟悉,所以自己也不敢多講什麼,主要也是怕講錯。

但是如果真的硬要問我的話,我可能會依據 wiki 所說的來講,因為以 wiki 的描述來講,Python 是比較接近 Call by sharing

傳共享物件呼叫(Call by sharing)的方式由Barbara Liskov命名[1],並被Python、Java(物件類型)、JavaScript、Scheme、OCaml等語言使用。

而這一點也剛好跟 JavaScript 非常像,因此我自己覺得你想把 Python 看成 JavaScript 似乎也可以?!

最後的最後也想聊個好玩的東西,也就是以下程式碼:

1
2
3
4
5
a = 'Ray'
b = 'Ray'

print('a', a, id(a)) # a Ray 4351631984
print('b', b, id(b)) # b Ray 4351631984

上面你可以發現我並沒有 a = b,但是兩者的記憶體位置卻是相同,因此代表著字串在建立時都是指向同一個記憶體位置,也因此才會導致 a == b 是一個 True,而這一段我覺得可以理解下面這張圖:

記憶體空間

但是如果重新賦予其他值的時候,就會是直接指向到另一個記憶體位置:

重新指向記憶體空間

如果是字典的話,那麼也是類似,但是字典則是看到一個新的 {} or [] 才會重新指向,這邊就讓我偷懶一下用之前 「JavaScript 核心觀念(27)-物件-物件參考觀念的實際運作模式」的文章圖片偷懶一下:

物件參考觀念的實際運作模式

那今天也差不多介紹到這邊就告一個段落囉~

參考文獻

作者的話

中秋節那段時間因為回老家團圓的關係,所以訊號一直都很差,幾乎只能打電話不能上網的等級,所以中秋節的晚上我都只能站在馬路正中央收發訊息,收發完就要快點閃離馬路,不然我現在應該不會在這邊寫文章了…

關於兔兔們

兔法無邊

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ