ทำไมฟังก์ชั่นสามารถแก้ไขข้อโต้แย้งบางอย่างตามที่รับรู้โดยผู้เรียก แต่ไม่ใช่ข้อโต้แย้งอื่น?


182

ฉันพยายามเข้าใจวิธีการของ Python ในการกำหนดขอบเขตตัวแปร ในตัวอย่างนี้คือเหตุผลที่f()สามารถที่จะปรับเปลี่ยนค่าของxเป็นที่รับรู้ภายในmain()แต่ไม่ได้ค่าของn?

def f(n, x):
    n = 2
    x.append(4)
    print('In f():', n, x)

def main():
    n = 1
    x = [0,1,2,3]
    print('Before:', n, x)
    f(n, x)
    print('After: ', n, x)

main()

เอาท์พุท:

Before: 1 [0, 1, 2, 3]
In f(): 2 [0, 1, 2, 3, 4]
After:  1 [0, 1, 2, 3, 4]

7
อธิบายกันที่นี่nedbatchelder.com/text/names.html
Roushan

คำตอบ:


212

คำตอบบางคำมีคำว่า "คัดลอก" ในบริบทของการเรียกใช้ฟังก์ชัน ฉันพบว่ามันทำให้สับสน

งูหลามไม่ลอกวัตถุคุณผ่านระหว่างการเรียกฟังก์ชั่นที่เคย

พารามิเตอร์ฟังก์ชั่นที่มีชื่อ เมื่อคุณเรียกใช้ฟังก์ชั่น Python จะผูกพารามิเตอร์เหล่านี้กับวัตถุใดก็ตามที่คุณผ่าน (ผ่านชื่อในขอบเขตผู้โทร)

วัตถุสามารถเปลี่ยนแปลงได้ (เช่นรายการ) หรือไม่เปลี่ยนรูป (เช่นจำนวนเต็มสตริงใน Python) วัตถุที่ไม่แน่นอนที่คุณสามารถเปลี่ยนได้ คุณไม่สามารถเปลี่ยนชื่อได้คุณสามารถผูกมันกับวัตถุอื่นได้

ตัวอย่างของคุณไม่เกี่ยวกับขอบเขตหรือเนมสเปซแต่เป็นเรื่องเกี่ยวกับการตั้งชื่อและการเชื่อมโยงและความไม่แน่นอนของวัตถุใน Python

def f(n, x): # these `n`, `x` have nothing to do with `n` and `x` from main()
    n = 2    # put `n` label on `2` balloon
    x.append(4) # call `append` method of whatever object `x` is referring to.
    print('In f():', n, x)
    x = []   # put `x` label on `[]` ballon
    # x = [] has no effect on the original list that is passed into the function

นี่คือภาพที่ดีเกี่ยวกับความแตกต่างระหว่างตัวแปรในภาษาอื่น ๆ และชื่อในหลาม


3
บทความนี้ช่วยให้ฉันเข้าใจปัญหาได้ดีขึ้นและแนะนำวิธีแก้ปัญหาและการใช้งานขั้นสูง: ค่าพารามิเตอร์เริ่มต้นใน Python
Gfy

@Gy ฉันเคยเห็นตัวอย่างที่คล้ายกันมาก่อน แต่สำหรับฉันมันไม่ได้อธิบายสถานการณ์ในโลกแห่งความเป็นจริง หากคุณกำลังแก้ไขบางสิ่งที่ผ่านไปแล้วมันไม่มีเหตุผลที่จะให้มันเป็นค่าเริ่มต้น
Mark Ransom

@MarkRansom def foo(x, l=None): l=l or []; l.append(x**2); return l[-1]ผมคิดว่ามันจะทำให้ความรู้สึกถ้าคุณต้องการที่จะให้ปลายทางออกตัวเลือกเช่น:
Janusz Lenar

สำหรับบรรทัดสุดท้ายของรหัสของเซบาสเตียนมันกล่าวว่า "# the above ไม่มีผลกับรายการต้นฉบับ" แต่ในความคิดของฉันมันไม่มีผลกับ "n" แต่เปลี่ยน "x" ในฟังก์ชั่น main () ฉันถูกไหม?
user17670

1
@ user17670: x = []ในf()ไม่มีผลต่อรายการxในฟังก์ชั่นหลัก ฉันได้อัปเดตความคิดเห็นเพื่อให้เฉพาะเจาะจงมากขึ้น
jfs

15

คุณมีคำตอบจำนวนหนึ่งอยู่แล้วและฉันเห็นด้วยกับเจเอฟเซบาสเตียนในวงกว้าง แต่คุณอาจพบว่าสิ่งนี้มีประโยชน์เป็นทางลัด:

เมื่อใดก็ตามที่คุณเห็นvarname =คุณกำลังสร้างการผูกชื่อใหม่ภายในขอบเขตของฟังก์ชัน ไม่ว่ามูลค่าใดที่varnameผูกไว้กับก่อนจะหายไปภายในขอบเขตนี้

เมื่อใดก็ตามที่คุณเห็นคุณกำลังเรียกวิธีการในvarname.foo() varnameวิธีนี้อาจแก้ไข varname (เช่นlist.append) varname(หรือวัตถุที่varnameชื่อ) อาจมีอยู่ในขอบเขตมากกว่าหนึ่งขอบเขตและเนื่องจากเป็นวัตถุเดียวกันการเปลี่ยนแปลงใด ๆ จึงสามารถมองเห็นได้ในทุกขอบเขต

[โปรดทราบว่าglobalคำหลักสร้างข้อยกเว้นสำหรับกรณีแรก]


13

fไม่ได้แก้ไขค่าของx(ซึ่งเป็นการอ้างอิงเดียวกันกับอินสแตนซ์ของรายการเสมอ) ค่อนข้างจะเปลี่ยนแปลงเนื้อหาของรายการนี้

ในทั้งสองกรณีสำเนาของการอ้างอิงจะถูกส่งผ่านไปยังฟังก์ชัน ภายในฟังก์ชั่น

  • nรับค่าใหม่ เฉพาะการอ้างอิงภายในฟังก์ชั่นเท่านั้นที่จะได้รับการแก้ไขไม่ใช่การอ้างอิงภายนอก
  • xไม่ได้รับการกำหนดค่าใหม่: ไม่มีการอ้างอิงทั้งภายในและภายนอกฟังก์ชั่น แต่xของมูลค่าที่มีการแก้ไข

เนื่องจากทั้งxภายในและภายนอกฟังก์ชั่นมันอ้างถึงค่าเดียวกันทั้งสองเห็นการปรับเปลี่ยน ในทางกลับกันnฟังก์ชั่นด้านในและด้านนอกนั้นหมายถึงค่าที่แตกต่างกันหลังจากที่nได้รับการกำหนดภายในฟังก์ชัน


8
"คัดลอก" ทำให้เข้าใจผิด Python ไม่มีตัวแปรเช่น C ชื่อทั้งหมดใน Python เป็นการอ้างอิง คุณไม่สามารถแก้ไขชื่อได้คุณสามารถผูกมันกับวัตถุอื่นได้นั่นคือทั้งหมด มันเหมาะสมที่จะพูดคุยเกี่ยวกับวัตถุที่ไม่แน่นอนและไม่เปลี่ยนรูปแบบใน Python ไม่ใช่ชื่อ
jfs

1
@JF Sebastian: ข้อความของคุณทำให้เข้าใจผิดได้ดีที่สุด มันไม่มีประโยชน์ที่จะคิดว่าตัวเลขเป็นการอ้างอิง
Pitarou

9
@dysfunctor: ตัวเลขอ้างอิงถึงวัตถุที่ไม่เปลี่ยนรูป หากคุณคิดว่าเป็นวิธีอื่นคุณควรมีกรณีพิเศษแปลก ๆ ให้อธิบาย หากคุณคิดว่าสิ่งเหล่านี้ไม่สามารถเปลี่ยนแปลงได้จะไม่มีกรณีพิเศษ
S.Lott

@ S.Lott: ไม่ว่าจะเกิดอะไรขึ้นภายใต้ประทุน Guido van Rossum ได้ใช้ความพยายามอย่างมากในการออกแบบ Python เพื่อให้โปรแกรมเมอร์สามารถใช้ตัวเลขเป็นเพียง ... ตัวเลข
Pitarou

1
@JF อ้างอิงถูกคัดลอก
habnabit

7

ฉันจะเปลี่ยนชื่อตัวแปรเพื่อลดความสับสน n -> nfหรือnmain x -> xfหรือxmain :

def f(nf, xf):
    nf = 2
    xf.append(4)
    print 'In f():', nf, xf

def main():
    nmain = 1
    xmain = [0,1,2,3]
    print 'Before:', nmain, xmain
    f(nmain, xmain)
    print 'After: ', nmain, xmain

main()

เมื่อคุณเรียกใช้ฟังก์ชันรันไทม์หลามทำสำเนาของxmainและกำหนดที่จะXFและเช่นเดียวกันกำหนดสำเนาของ nmainเพื่อnf

ในกรณีของnค่าที่คัดลอกคือ 1

ในกรณีของxค่าที่จะถูกคัดลอกคือไม่ตัวหนังสือรายการ[0, 1, 2, 3] มันเป็นการอ้างอิงถึงรายการนั้น xfและxmainกำลังชี้ไปที่รายการเดียวกันดังนั้นเมื่อคุณแก้ไขxfคุณจะทำการปรับเปลี่ยนxmainด้วย

อย่างไรก็ตามหากคุณต้องเขียนสิ่งที่ชอบ:

    xf = ["foo", "bar"]
    xf.append(4)

คุณจะพบว่าxmainไม่เปลี่ยนแปลง นี่เป็นเพราะในบรรทัดxf = ["foo", "bar"]คุณมีการเปลี่ยนแปลง xfเพื่อชี้ไปยังรายการใหม่ การเปลี่ยนแปลงใด ๆ ที่คุณทำกับรายการใหม่นี้จะไม่มีผลกับรายการที่xmainยังคงชี้ไป

หวังว่าจะช่วย :-)


2
"ในกรณีของ n ค่าที่คัดลอก ... " - นี่เป็นความผิดไม่มีการคัดลอกที่นี่ (เว้นแต่คุณจะนับการอ้างอิง) หลามใช้ 'ชื่อ' ซึ่งชี้ไปที่วัตถุจริง nf และจุด XF จะ nmain และ xmain จนกว่าnf = 2ที่ชื่อจะถูกเปลี่ยนเป็นชี้ไปที่nf 2ตัวเลขไม่เปลี่ยนรูปรายการจะไม่แน่นอน
Casey Kuball

2

มันเป็นเพราะรายการเป็นวัตถุที่ไม่แน่นอน คุณไม่ได้ตั้งค่า x เป็นค่า [0,1,2,3] คุณกำลังกำหนดป้ายกำกับให้กับวัตถุ [0,1,2,3]

คุณควรประกาศฟังก์ชัน f () ดังนี้:

def f(n, x=None):
    if x is None:
        x = []
    ...

3
มันไม่มีส่วนเกี่ยวข้องกับความผันแปร หากคุณจะทำx = x + [4]แทนคุณx.append(4)จะไม่เห็นการเปลี่ยนแปลงในผู้โทรด้วยแม้ว่ารายการจะไม่แน่นอน มันจะต้องทำอย่างไรถ้ามันกลายพันธุ์แน่นอน
glglgl

1
OTOH ถ้าคุณทำเช่นx += [4]นั้นxจะกลายพันธุ์เหมือนที่เกิดขึ้นx.append(4)ดังนั้นผู้โทรจะเห็นการเปลี่ยนแปลง
PM 2Ring

2

n คือ int (ไม่เปลี่ยนรูป) และสำเนาถูกส่งผ่านไปยังฟังก์ชันดังนั้นในฟังก์ชันที่คุณกำลังเปลี่ยนสำเนา

X คือรายการ (ไม่แน่นอน) และสำเนาของตัวชี้ถูกส่งผ่านไปยังฟังก์ชันดังนั้น x.append (4) เปลี่ยนเนื้อหาของรายการ อย่างไรก็ตามคุณพูดว่า x = [0,1,2,3,4] ในฟังก์ชั่นของคุณคุณจะไม่เปลี่ยนเนื้อหาของ x ใน main ()


3
ชมการใช้ถ้อยคำ "คัดลอกของตัวชี้" ทั้งสองแห่งได้รับการอ้างอิงถึงวัตถุ n คือการอ้างอิงถึงวัตถุที่ไม่เปลี่ยนรูป x คือการอ้างอิงไปยังวัตถุที่ไม่แน่นอน
S.Lott

2

หากฟังก์ชั่นถูกเขียนซ้ำด้วยตัวแปรที่แตกต่างกันอย่างสิ้นเชิงและเราเรียกidบนพวกเขามันจะแสดงให้เห็นถึงจุดที่ดี ฉันไม่ได้รับสิ่งนี้ในตอนแรกและอ่านโพสต์ของ jfs พร้อมคำอธิบายที่ดีดังนั้นฉันจึงพยายามเข้าใจ / โน้มน้าวตัวเอง:

def f(y, z):
    y = 2
    z.append(4)
    print ('In f():             ', id(y), id(z))

def main():
    n = 1
    x = [0,1,2,3]
    print ('Before in main:', n, x,id(n),id(x))
    f(n, x)
    print ('After in main:', n, x,id(n),id(x))

main()
Before in main: 1 [0, 1, 2, 3]   94635800628352 139808499830024
In f():                          94635800628384 139808499830024
After in main: 1 [0, 1, 2, 3, 4] 94635800628352 139808499830024

z และ x มีรหัสเดียวกัน แท็กที่แตกต่างกันเพียงอย่างเดียวสำหรับโครงสร้างพื้นฐานเดียวกันกับที่บทความบอกไว้


0

Python เป็นภาษา Pass-by-value ที่แท้จริงหากคุณคิดว่าถูกต้อง ตัวแปร python เก็บตำแหน่งของวัตถุในหน่วยความจำ ตัวแปร Python ไม่ได้จัดเก็บวัตถุเอง เมื่อคุณส่งตัวแปรไปยังฟังก์ชันคุณกำลังส่งสำเนาที่อยู่ของวัตถุที่ชี้ไปโดยตัวแปร

ตรงกันข้ามทั้งสองฟังก์ชั่น

def foo(x):
    x[0] = 5

def goo(x):
    x = []

ตอนนี้เมื่อคุณพิมพ์ลงในเปลือก

>>> cow = [3,4,5]
>>> foo(cow)
>>> cow
[5,4,5]

เปรียบเทียบสิ่งนี้กับสารที่หนา

>>> cow = [3,4,5]
>>> goo(cow)
>>> goo
[3,4,5]

ในกรณีแรกเราส่งสำเนาที่อยู่ของวัวไปที่ foo และ foo แก้ไขสถานะของวัตถุที่อยู่ที่นั่น วัตถุได้รับการแก้ไข

ในกรณีที่สองคุณส่งสำเนาที่อยู่ของวัวไปยังสารที่หนา จากนั้นสารที่หนาจะไปเปลี่ยนสำเนา ผลกระทบ: ไม่มี

ฉันเรียกสิ่งนี้ว่าหลักการบ้านสีชมพูหลักการบ้านสีชมพูหากคุณทำสำเนาที่อยู่ของคุณและบอกช่างทาสีให้ทาสีบ้านด้วยสีชมพูที่อยู่นั้นคุณจะได้พบกับบ้านสีชมพู หากคุณให้สำเนาที่อยู่ของคุณแก่จิตรกรและบอกให้เขาเปลี่ยนเป็นที่อยู่ใหม่ที่อยู่บ้านของคุณจะไม่เปลี่ยนแปลง

คำอธิบายช่วยลดความสับสนได้มากมาย Python ส่งผ่านตัวแปรที่อยู่ที่จัดเก็บตามค่า


ผ่านไปบริสุทธิ์ด้วยค่าตัวชี้จะไม่แตกต่างจากที่ผ่านมาโดยอ้างอิงถ้าคุณคิดเกี่ยวกับเรื่องนี้ทางที่ถูกต้อง ...
galinette

ดูสารที่หนา คุณบริสุทธิ์โดยการอ้างอิงมันจะเปลี่ยนอาร์กิวเมนต์ของมัน ไม่ Python ไม่ใช่ภาษาแบบพาส - บาย - อ้างอิงที่แท้จริง มันผ่านการอ้างอิงตามค่า
ncmathsadist

0

Python ถูกคัดลอกตามมูลค่าของการอ้างอิง วัตถุครอบครองเขตข้อมูลในหน่วยความจำและการอ้างอิงเกี่ยวข้องกับวัตถุนั้น แต่ตัวเองครอบครองเขตข้อมูลในหน่วยความจำ และชื่อ / ค่าเชื่อมโยงกับการอ้างอิง ในฟังก์ชั่น python มันจะคัดลอกค่าของการอ้างอิงเสมอดังนั้นในโค้ดของคุณ n จะถูกคัดลอกไปเป็นชื่อใหม่เมื่อคุณกำหนดมันจะมีช่องว่างใหม่ใน caller stack แต่สำหรับรายการชื่อก็ถูกคัดลอกไปด้วย แต่มันอ้างถึงหน่วยความจำเดียวกัน (เนื่องจากคุณไม่เคยกำหนดค่าใหม่ให้กับรายการ) นั่นคือเวทย์มนตร์ในไพ ธ อน!


0

ความเข้าใจทั่วไปของฉันคือตัวแปรวัตถุใด ๆ (เช่นรายการหรือ dict เป็นต้น) สามารถแก้ไขได้ผ่านฟังก์ชั่น สิ่งที่ฉันเชื่อว่าคุณไม่สามารถทำได้คือกำหนดพารามิเตอร์ใหม่ - เช่นกำหนดโดยอ้างอิงภายในฟังก์ชัน callable

ที่สอดคล้องกับภาษาอื่น ๆ อีกมากมาย

เรียกใช้สคริปต์สั้น ๆ ต่อไปนี้เพื่อดูว่ามันทำงานอย่างไร:

def func1(x, l1):
    x = 5
    l1.append("nonsense")

y = 10
list1 = ["meaning"]
func1(y, list1)
print(y)
print(list1)

-3

ฉันได้แก้ไขคำตอบของฉันหลายครั้งและตระหนักว่าฉันไม่ต้องพูดอะไรเลยหลามได้อธิบายตัวเองแล้ว

a = 'string'
a.replace('t', '_')
print(a)
>>> 'string'

a = a.replace('t', '_')
print(a)
>>> 's_ring'

b = 100
b + 1
print(b)
>>> 100

b = b + 1
print(b)
>>> 101

def test_id(arg):
    c = id(arg)
    arg = 123
    d = id(arg)
    return

a = 'test ids'
b = id(a)
test_id(a)
e = id(a)

# b = c  = e != d
# this function do change original value
del change_like_mutable(arg):
    arg.append(1)
    arg.insert(0, 9)
    arg.remove(2)
    return

test_1 = [1, 2, 3]
change_like_mutable(test_1)



# this function doesn't 
def wont_change_like_str(arg):
     arg = [1, 2, 3]
     return


test_2 = [1, 1, 1]
wont_change_like_str(test_2)
print("Doesn't change like a imutable", test_2)

มารนี้ไม่ใช่การอ้างอิง / ค่า / ไม่แน่นอนหรือไม่ / ตัวอย่าง, พื้นที่ชื่อหรือตัวแปร / รายการหรือ str, มันเป็นสัญญาณ, สัญญาณเท่ากับ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.