ฉันจะส่งผ่านตัวแปรโดยอ้างอิงได้อย่างไร


2628

เอกสาร Python ดูเหมือนไม่ชัดเจนว่าพารามิเตอร์ถูกส่งผ่านโดยการอ้างอิงหรือค่าและรหัสต่อไปนี้สร้างค่าไม่เปลี่ยนแปลง 'ดั้งเดิม'

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.change(self.variable)
        print(self.variable)

    def change(self, var):
        var = 'Changed'

มีบางอย่างที่ฉันสามารถทำได้เพื่อส่งผ่านตัวแปรโดยการอ้างอิงจริงหรือไม่?


23
สำหรับคำอธิบายสั้น / ชี้แจงเห็นคำตอบแรกที่จะคำถาม StackOverflow นี้ เนื่องจากสตริงไม่เปลี่ยนรูปพวกเขาจะไม่ถูกเปลี่ยนแปลงและตัวแปรใหม่จะถูกสร้างขึ้นดังนั้นตัวแปร "outer" ยังคงมีค่าเดียวกัน
PhilS

10
รหัสในคำตอบของ BlairConrad นั้นดี แต่คำอธิบายของ DavidCournapeau และ DarenThomas นั้นถูกต้อง
Ethan Furman

70
ก่อนที่จะอ่านคำตอบที่เลือกโปรดพิจารณาอ่านข้อความสั้น ๆ นี้ภาษาอื่น ๆ ที่มี "ตัวแปร" งูใหญ่ "มีชื่อ" คิดเกี่ยวกับ "ชื่อ" และ "วัตถุ" แทน "ตัวแปร" และ "การอ้างอิง" และคุณควรหลีกเลี่ยงปัญหาที่คล้ายกันมากมาย
lqc

24
ทำไมสิ่งนี้ถึงไม่จำเป็นต้องใช้คลาส? นี่ไม่ใช่ Java: P
Peter R

2
วิธีแก้ปัญหาอื่นคือการสร้าง 'อ้างอิง' wrapper เช่นนี้: ref = type ('', (), {'n': 1}) stackoverflow.com/a/1123054/409638
เบิร์ต

คำตอบ:


2830

อาร์กิวเมนต์จะมีการส่งผ่านไปตามที่ได้รับมอบหมาย เหตุผลที่อยู่เบื้องหลังนี้คือสองเท่า:

  1. พารามิเตอร์ที่ส่งผ่านเป็นจริงการอ้างอิงไปยังวัตถุ (แต่การอ้างอิงถูกส่งผ่านตามค่า)
  2. ชนิดข้อมูลบางอย่างไม่แน่นอน แต่บางประเภทไม่

ดังนั้น:

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

  • หากคุณส่งวัตถุที่เปลี่ยนรูปไม่ได้ไปยังวิธีการคุณยังคงไม่สามารถเชื่อมโยงการอ้างอิงภายนอกและคุณไม่สามารถกลายพันธุ์วัตถุได้

เพื่อให้ชัดเจนยิ่งขึ้นลองมาตัวอย่าง

รายการ - ประเภทที่ไม่แน่นอน

ลองแก้ไขรายการที่ส่งผ่านไปยังเมธอด:

def try_to_change_list_contents(the_list):
    print('got', the_list)
    the_list.append('four')
    print('changed to', the_list)

outer_list = ['one', 'two', 'three']

print('before, outer_list =', outer_list)
try_to_change_list_contents(outer_list)
print('after, outer_list =', outer_list)

เอาท์พุท:

before, outer_list = ['one', 'two', 'three']
got ['one', 'two', 'three']
changed to ['one', 'two', 'three', 'four']
after, outer_list = ['one', 'two', 'three', 'four']

เนื่องจากพารามิเตอร์ที่ส่งผ่านเป็นการอ้างอิงouter_listไม่ใช่สำเนาเราจึงสามารถใช้วิธีรายการเปลี่ยนแปลงเพื่อเปลี่ยนแปลงและมีการเปลี่ยนแปลงที่สะท้อนในขอบเขตด้านนอก

ทีนี้มาดูกันว่าจะเกิดอะไรขึ้นเมื่อเราพยายามเปลี่ยนการอ้างอิงที่ผ่านเป็นพารามิเตอร์:

def try_to_change_list_reference(the_list):
    print('got', the_list)
    the_list = ['and', 'we', 'can', 'not', 'lie']
    print('set to', the_list)

outer_list = ['we', 'like', 'proper', 'English']

print('before, outer_list =', outer_list)
try_to_change_list_reference(outer_list)
print('after, outer_list =', outer_list)

เอาท์พุท:

before, outer_list = ['we', 'like', 'proper', 'English']
got ['we', 'like', 'proper', 'English']
set to ['and', 'we', 'can', 'not', 'lie']
after, outer_list = ['we', 'like', 'proper', 'English']

เนื่องจากthe_listพารามิเตอร์ถูกส่งผ่านตามค่าการกำหนดรายการใหม่ให้กับมันจึงไม่มีผลกระทบใด ๆ ที่โค้ดภายนอกเมธอดสามารถมองเห็นได้ นี่the_listเป็นสำเนาของouter_listข้อมูลอ้างอิงและเราได้the_listชี้ไปที่รายการใหม่ แต่ไม่มีวิธีการเปลี่ยนแปลงที่outer_listชี้ไป

String - ชนิดไม่เปลี่ยนรูป

มันไม่เปลี่ยนรูปดังนั้นจึงไม่มีอะไรที่เราสามารถทำได้เพื่อเปลี่ยนเนื้อหาของสตริง

ตอนนี้เราลองเปลี่ยนการอ้างอิง

def try_to_change_string_reference(the_string):
    print('got', the_string)
    the_string = 'In a kingdom by the sea'
    print('set to', the_string)

outer_string = 'It was many and many a year ago'

print('before, outer_string =', outer_string)
try_to_change_string_reference(outer_string)
print('after, outer_string =', outer_string)

เอาท์พุท:

before, outer_string = It was many and many a year ago
got It was many and many a year ago
set to In a kingdom by the sea
after, outer_string = It was many and many a year ago

อีกครั้งเนื่องจากthe_stringพารามิเตอร์ถูกส่งผ่านตามค่าการกำหนดสตริงใหม่ให้กับมันจะไม่มีผลกระทบใด ๆ ที่โค้ดภายนอกเมธอดสามารถมองเห็นได้ นี่the_stringเป็นสำเนาของการouter_stringอ้างอิงและเราได้the_stringชี้ไปที่สตริงใหม่ แต่ไม่มีวิธีใดที่จะเปลี่ยนแปลงouter_stringได้

ฉันหวังว่าสิ่งนี้จะช่วยล้างสิ่งเล็กน้อย

แก้ไข:มีการตั้งข้อสังเกตว่าสิ่งนี้ไม่ตอบคำถามที่ @David ถาม แต่เดิมว่า "มีสิ่งที่ฉันสามารถทำได้เพื่อส่งผ่านตัวแปรโดยการอ้างอิงจริงหรือไม่" มาทำงานกันดีกว่า

เราจะไปรอบนี้ได้อย่างไร

@ @ Andrea แสดงให้เห็นว่าคุณสามารถคืนค่าใหม่ สิ่งนี้จะไม่เปลี่ยนวิธีการส่งผ่านสิ่งต่างๆ แต่จะช่วยให้คุณได้รับข้อมูลที่คุณต้องการกลับมา:

def return_a_whole_new_string(the_string):
    new_string = something_to_do_with_the_old_string(the_string)
    return new_string

# then you could call it like
my_string = return_a_whole_new_string(my_string)

หากคุณต้องการหลีกเลี่ยงการใช้ค่าส่งคืนคุณสามารถสร้างคลาสเพื่อเก็บค่าของคุณและส่งไปยังฟังก์ชันหรือใช้คลาสที่มีอยู่เช่นรายการ:

def use_a_wrapper_to_simulate_pass_by_reference(stuff_to_change):
    new_string = something_to_do_with_the_old_string(stuff_to_change[0])
    stuff_to_change[0] = new_string

# then you could call it like
wrapper = [my_string]
use_a_wrapper_to_simulate_pass_by_reference(wrapper)

do_something_with(wrapper[0])

แม้ว่ามันจะดูยุ่งยากไปหน่อย


165
จากนั้นก็เหมือนกันใน C เมื่อคุณผ่าน "โดยการอ้างอิง" คุณจริง ๆ แล้วผ่านค่าอ้างอิง ... กำหนด "โดยการอ้างอิง": P
Andrea Ambu

104
ฉันไม่แน่ใจว่าฉันเข้าใจเงื่อนไขของคุณหรือไม่ ฉันออกจากเกม C มาระยะหนึ่งแล้ว แต่กลับมาเมื่อฉันอยู่ในนั้นไม่มี "การส่งผ่านอ้างอิง" - คุณสามารถผ่านสิ่งต่าง ๆ ได้และมันก็ผ่านคุณค่าเสมอดังนั้นสิ่งที่อยู่ในรายการพารามิเตอร์ ถูกคัดลอกแล้ว แต่บางครั้งสิ่งนี้ก็คือตัวชี้ซึ่งสามารถติดตามชิ้นส่วนของหน่วยความจำ (ดั้งเดิม, อาร์เรย์, โครงสร้างหรืออะไรก็ได้) แต่คุณไม่สามารถเปลี่ยนตัวชี้ที่คัดลอกมาจากขอบเขตด้านนอก - เมื่อคุณทำกับฟังก์ชั่น ตัวชี้ต้นฉบับยังคงชี้ไปยังที่อยู่เดียวกัน C ++ แนะนำการอ้างอิงซึ่งมีพฤติกรรมแตกต่างกัน
Blair Conrad

34
@ Zac Bowling ฉันไม่เข้าใจว่าสิ่งที่คุณพูดนั้นเกี่ยวข้องกับคำตอบนี้อย่างไร หากผู้มาใหม่ Python ต้องการทราบเกี่ยวกับการส่งผ่านโดย ref / val คำตอบจากคำตอบนี้คือ: 1-คุณสามารถใช้การอ้างอิงที่ฟังก์ชั่นได้รับเป็นอาร์กิวเมนต์เพื่อแก้ไขค่า 'นอก' ของตัวแปรตราบใดที่ เนื่องจากคุณไม่ได้กำหนดพารามิเตอร์ใหม่เพื่ออ้างถึงวัตถุใหม่ 2-การกำหนดให้กับชนิดไม่เปลี่ยนรูปมักจะสร้างวัตถุใหม่ซึ่งแบ่งการอ้างอิงที่คุณมีกับตัวแปรภายนอก
Cam Jackson

11
@CamJackson คุณต้องการตัวอย่างที่ดีกว่า - ตัวเลขก็เป็นวัตถุที่ไม่เปลี่ยนรูปแบบใน Python นอกจากนี้จะไม่เป็นความจริงหรือที่จะบอกว่าการมอบหมายใด ๆโดยไม่ต้องห้อยที่ด้านซ้ายของเท่ากับจะมอบหมายชื่อใหม่ให้กับวัตถุใหม่ไม่ว่าจะไม่เปลี่ยนรูปหรือไม่ def Foo(alist): alist = [1,2,3]จะไม่แก้ไขเนื้อหาของรายการจากมุมมองผู้โทร
Mark Ransom

59
-1 รหัสที่แสดงนั้นดีอธิบายได้ว่าผิดอย่างไร ดูคำตอบของ DavidCournapeau หรือ DarenThomas สำหรับคำอธิบายที่ถูกต้องเกี่ยวกับสาเหตุ
Ethan Furman

685

ปัญหามาจากการเข้าใจผิดว่าตัวแปรอะไรใน Python หากคุณคุ้นเคยกับภาษาดั้งเดิมส่วนใหญ่คุณมีแบบจำลองทางจิตของสิ่งที่เกิดขึ้นตามลำดับดังต่อไปนี้:

a = 1
a = 2

คุณเชื่อว่าaเป็นที่ตั้งของหน่วยความจำที่เก็บค่าแล้วมีการปรับปรุงการจัดเก็บค่า1 2นั่นไม่ใช่สิ่งที่ทำงานใน Python แต่aเริ่มต้นที่อ้างอิงถึงวัตถุที่มีค่าที่แล้วได้รับพระราชเสาวนีย์ที่อ้างอิงถึงวัตถุที่มีค่า1 2วัตถุทั้งสองนั้นอาจยังคงอยู่ร่วมกันแม้ว่าจะaไม่ได้อ้างถึงวัตถุแรกอีกต่อไป ในความเป็นจริงพวกเขาอาจใช้ร่วมกันโดยการอ้างอิงอื่น ๆ ภายในโปรแกรม

เมื่อคุณเรียกใช้ฟังก์ชันที่มีพารามิเตอร์จะมีการสร้างการอ้างอิงใหม่ที่อ้างอิงถึงวัตถุที่ส่งผ่านซึ่งจะแยกจากการอ้างอิงที่ใช้ในการเรียกใช้ฟังก์ชั่นดังนั้นจึงไม่มีวิธีใดที่จะอัปเดตการอ้างอิงนั้น วัตถุใหม่ ในตัวอย่างของคุณ:

def __init__(self):
    self.variable = 'Original'
    self.Change(self.variable)

def Change(self, var):
    var = 'Changed'

self.variable'Original'คือการอ้างอิงไปยังวัตถุสตริง เมื่อคุณเรียกChangeคุณสร้างการอ้างอิงที่สองvarไปยังวัตถุ ภายในฟังก์ชันที่คุณมอบหมายการอ้างอิงvarไปยังวัตถุสตริง'Changed'อื่น แต่การอ้างอิงself.variableนั้นแยกต่างหากและไม่เปลี่ยนแปลง

วิธีเดียวที่จะทำให้สิ่งนี้ผ่านวัตถุที่ไม่แน่นอน เนื่องจากการอ้างอิงทั้งสองอ้างถึงวัตถุเดียวกันการเปลี่ยนแปลงใด ๆ กับวัตถุจึงมีผลในทั้งสองที่

def __init__(self):         
    self.variable = ['Original']
    self.Change(self.variable)

def Change(self, var):
    var[0] = 'Changed'

106
คำอธิบายสั้น ๆ ดี ย่อหน้าของคุณ "เมื่อคุณเรียกใช้ฟังก์ชัน ... " เป็นหนึ่งในคำอธิบายที่ดีที่สุดที่ฉันเคยได้ยินเกี่ยวกับวลีที่ค่อนข้างคลุมเครือว่า ฉันคิดว่าถ้าคุณเข้าใจวรรคนั้นเพียงอย่างเดียวทุกอย่างอื่นก็สมเหตุสมผลและสมเหตุสมผลเป็นข้อสรุปเชิงตรรกะจากที่นั่น จากนั้นคุณเพียงแค่ต้องระวังเมื่อคุณกำลังสร้างวัตถุใหม่และเมื่อคุณปรับเปลี่ยนวัตถุที่มีอยู่
Cam Jackson

3
แต่คุณจะมอบหมายการอ้างอิงอีกครั้งได้อย่างไร? ฉันคิดว่าคุณไม่สามารถเปลี่ยนที่อยู่ของ 'var' ได้ แต่ตอนนี้สตริงของคุณ "เปลี่ยนแล้ว" จะถูกเก็บไว้ในที่อยู่หน่วยความจำ 'var' คำอธิบายของคุณทำให้ดูเหมือน "เปลี่ยน" และ "ดั้งเดิม" เป็นของสถานที่ต่าง ๆ ในหน่วยความจำแทนและคุณเพียงแค่เปลี่ยน 'var' เป็นที่อยู่อื่น ถูกต้องไหม
Glassjawed

9
@Glassjawed ฉันคิดว่าคุณจะได้รับมัน "Changed" และ "Original" เป็นวัตถุสตริงสองแบบที่อยู่หน่วยความจำที่แตกต่างกันและการเปลี่ยนแปลง 'var' จากการชี้ไปที่หนึ่งเป็นการชี้ไปที่อื่น
Mark Ransom

การใช้ฟังก์ชั่น id () ช่วยอธิบายเรื่องต่าง ๆ ให้ชัดเจนเพราะมันชัดเจนเมื่อ Python สร้างวัตถุใหม่ (ดังนั้นฉันคิดว่าต่อไป)
ทิมริชาร์ดสัน

1
@MinhTran ในเงื่อนไขที่ง่ายที่สุดการอ้างอิงคือสิ่งที่ "อ้างอิง" กับวัตถุ การเป็นตัวแทนทางกายภาพของสิ่งนั้นน่าจะเป็นตัวชี้มากที่สุด แต่นั่นเป็นเพียงรายละเอียดการนำไปปฏิบัติ มันเป็นความคิดเชิงนามธรรมที่หัวใจจริงๆ
Mark Ransom

329

ฉันพบคำตอบอื่นที่ค่อนข้างยาวและซับซ้อนดังนั้นฉันจึงสร้างแผนภาพง่าย ๆ นี้เพื่ออธิบายวิธีที่ Python ปฏิบัติต่อตัวแปรและพารามิเตอร์ ป้อนคำอธิบายรูปภาพที่นี่


2
น่ารักทำให้ง่ายต่อการมองเห็นความแตกต่างที่ละเอียดอ่อนซึ่งมีการมอบหมายระดับกลางไม่ชัดเจนสำหรับผู้มองเห็นแบบสบาย +1
user22866

9
ไม่สำคัญว่า A จะเปลี่ยนแปลงหรือไม่ หากคุณบางสิ่งบางอย่างที่แตกต่างกันกำหนดไป B ไม่เปลี่ยนแปลง หากวัตถุไม่แน่นอนคุณสามารถกลายพันธุ์ได้แน่นอน แต่นั่นไม่เกี่ยวอะไรกับการมอบหมายโดยตรงกับชื่อ ..
Martijn Pieters

1
@Martijn คุณพูดถูก ฉันลบส่วนหนึ่งของคำตอบที่กล่าวถึงความไม่แน่นอน ฉันไม่คิดว่ามันจะง่ายขึ้นกว่านี้
Zenadix

4
ขอบคุณสำหรับการอัปเดตที่ดีกว่ามาก! สิ่งที่คนส่วนใหญ่สับสนคือการมอบหมายให้สมัคร; เช่นเทียบกับที่ได้รับมอบหมายโดยตรงB[0] = 2 B = 2
Martijn Pieters

11
"A ถูกมอบหมายให้กับ B. " นั่นไม่ชัดเจนหรือเปล่า? ผมคิดว่าในภาษาอังกฤษธรรมดาที่อาจหมายถึงการอย่างใดอย่างหนึ่งหรือA=B B=A
Hatshepsut

240

มันไม่ได้เป็นแบบ pass-by-value หรือ pass-by-reference - มันเป็น call-by-object ดูสิ่งนี้โดย Fredrik Lundh:

http://effbot.org/zone/call-by-object.htm

นี่คือคำพูดที่สำคัญ:

"... ตัวแปร [ชื่อ] ไม่ใช่วัตถุ แต่ไม่สามารถแสดงตัวแปรอื่น ๆ หรืออ้างอิงโดยวัตถุได้"

ในตัวอย่างของคุณเมื่อChangeเมธอดถูกเรียก - เนมสเปซจะถูกสร้างขึ้น และvarกลายเป็นชื่อที่อยู่ใน namespace 'Original'ว่าสำหรับวัตถุสตริง วัตถุนั้นมีชื่อในสอง namespaces ถัดไปvar = 'Changed'ผูกvarกับวัตถุสตริงใหม่และทำให้ของวิธีการที่ลืม namespace 'Original'เกี่ยวกับ ในที่สุดเนมสเปซนั้นก็ถูกลืมไปแล้วและสตริงก็'Changed'พร้อมไปด้วย


21
ฉันพบว่ามันยากที่จะซื้อ สำหรับฉันนั้นเหมือนกับ Java พารามิเตอร์เป็นตัวชี้ไปยังวัตถุในหน่วยความจำและตัวชี้เหล่านั้นจะถูกส่งผ่านทางสแต็กหรือรีจิสเตอร์
Luciano

10
นี่ไม่เหมือนจาวา หนึ่งในกรณีที่มันไม่เหมือนกันคือวัตถุที่ไม่เปลี่ยนรูป ลองนึกถึงฟังก์ชั่นเล็ก ๆ น้อย ๆ แลมบ์ดา x: x ใช้สิ่งนี้สำหรับ x = [1, 2, 3] และ x = (1, 2, 3) ในกรณีแรกค่าที่ส่งคืนจะเป็นสำเนาของอินพุตและเหมือนกันในกรณีที่สอง
David Cournapeau

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

23
มันเหมือนกับใน Java การอ้างอิงวัตถุถูกส่งผ่านตามค่า ทุกคนที่คิดแตกต่างกันควรแนบรหัสไพ ธ อนสำหรับswapฟังก์ชั่นที่สามารถสลับการอ้างอิงสองแบบเช่นนี้: a = [42] ; b = 'Hello'; swap(a, b) # Now a is 'Hello', b is [42]
cayhorstmann

24
มันเหมือนกับ Java เมื่อคุณส่งวัตถุใน Java อย่างไรก็ตาม Java ยังมีตัวกำหนดดั้งเดิมซึ่งถูกส่งผ่านโดยการคัดลอกค่าของตัวดั้งเดิม ดังนั้นพวกเขาต่างกันในกรณีนั้น
Claudiu

174

นึกถึงสิ่งที่ถูกส่งผ่านโดยการมอบหมายแทนโดยการอ้างอิง / ตามค่า ด้วยวิธีนี้จะชัดเจนอยู่เสมอสิ่งที่เกิดขึ้นตราบใดที่คุณเข้าใจว่าเกิดอะไรขึ้นระหว่างการมอบหมายปกติ

ดังนั้นเมื่อส่งรายการไปยังฟังก์ชัน / วิธีรายการจะถูกกำหนดให้กับชื่อพารามิเตอร์ การผนวกเข้ากับรายการจะส่งผลให้รายการถูกแก้ไข การกำหนดรายการภายในฟังก์ชันใหม่จะไม่เปลี่ยนรายการเดิมเนื่องจาก:

a = [1, 2, 3]
b = a
b.append(4)
b = ['a', 'b']
print a, b      # prints [1, 2, 3, 4] ['a', 'b']

เนื่องจากไม่สามารถแก้ไขชนิดที่ไม่เปลี่ยนรูปแบบได้ดูเหมือนจะถูกส่งผ่านโดยการส่งค่า int ไปยังฟังก์ชันหมายถึงการกำหนดค่า int ให้กับพารามิเตอร์ของฟังก์ชัน คุณสามารถกำหนดใหม่ได้ แต่จะไม่เปลี่ยนค่าตัวแปรดั้งเดิม


3
ได้อย่างรวดเร็วก่อนคำตอบนี้ดูเหมือนจะหลีกเลี่ยงคำถามเดิม หลังจากอ่านครั้งที่สองฉันได้ตระหนักว่านี่ทำให้เรื่องค่อนข้างชัดเจน การติดตามแนวคิด "การกำหนดชื่อ" ที่ดีอาจพบได้ที่นี่: โค้ดเช่น Pythonista: Idiomatic Python
Christian Groleau

65

Effbot (หรือที่รู้จักกันในนาม Fredrik Lundh) ได้อธิบายถึงรูปแบบการส่งผ่านตัวแปรของ Python ว่าเป็นการโทรหาวัตถุ: http://effbot.org/zone/call-by-object.htm

วัตถุที่ได้รับการจัดสรรบนกองและตัวชี้ไปที่พวกเขาสามารถส่งผ่านไปได้ทุกที่

  • เมื่อคุณทำการกำหนดเช่นx = 1000รายการพจนานุกรมจะถูกสร้างขึ้นที่แมปสตริง "x" ในเนมสเปซปัจจุบันกับตัวชี้ไปยังวัตถุจำนวนเต็มที่มีหนึ่งพัน

  • เมื่อคุณอัปเดต "x" ด้วยจะมีx = 2000การสร้างวัตถุจำนวนเต็มใหม่และพจนานุกรมจะได้รับการอัพเดตให้ชี้ไปที่วัตถุใหม่ วัตถุเก่าหนึ่งพันนั้นไม่เปลี่ยนแปลง (และอาจมีชีวิตอยู่หรือไม่ก็ได้ขึ้นอยู่กับว่ามีสิ่งใดอ้างอิงถึงวัตถุ)

  • เมื่อคุณทำการมอบหมายใหม่เช่นy = xรายการพจนานุกรมใหม่ "y" จะถูกสร้างขึ้นซึ่งชี้ไปที่วัตถุเดียวกันกับรายการสำหรับ "x"

  • วัตถุเช่นสตริงและจำนวนเต็มมีไม่เปลี่ยนรูป นี่หมายความว่าไม่มีวิธีที่สามารถเปลี่ยนวัตถุหลังจากที่มันถูกสร้างขึ้น ตัวอย่างเช่นเมื่อวัตถุจำนวนเต็มหนึ่งพันถูกสร้างขึ้นมันจะไม่เปลี่ยนแปลง คณิตศาสตร์ทำได้โดยการสร้างวัตถุจำนวนเต็มใหม่

  • วัตถุเช่นรายการที่ไม่แน่นอน ซึ่งหมายความว่าเนื้อหาของวัตถุสามารถเปลี่ยนแปลงได้โดยสิ่งใดก็ตามที่ชี้ไปยังวัตถุ ยกตัวอย่างเช่นจะพิมพ์x = []; y = x; x.append(10); print y [10]สร้างรายการว่างเปล่า ทั้ง "x" และ "y" ชี้ไปที่รายการเดียวกัน ผนวกวิธีการแปรรูป (การปรับปรุง) วัตถุรายการ (เช่นการเพิ่มบันทึกไปยังฐานข้อมูล) และผลที่ได้คือสามารถมองเห็นได้ทั้ง "X" และ "Y" (เช่นเดียวกับการปรับปรุงฐานข้อมูลจะสามารถมองเห็นการเชื่อมต่อกับฐานข้อมูลที่ทุกครั้ง)

หวังว่าจะช่วยแก้ปัญหาให้คุณ


2
ฉันซาบซึ้งในการเรียนรู้เกี่ยวกับสิ่งนี้จากผู้พัฒนา เป็นความจริงไหมที่id()ฟังก์ชั่นส่งคืนค่า (การอ้างอิงวัตถุ) ของตัวชี้ตามที่คำตอบของ pepr แนะนำ?
ซื่อสัตย์ Abe

4
@HonestAbe ใช่ใน CPython id ()ส่งคืนที่อยู่ แต่ในงูเหลือมอื่น ๆ เช่น PyPy และ Jython id ()เป็นเพียงตัวระบุวัตถุที่ไม่ซ้ำกัน
Raymond Hettinger

59

ในทางเทคนิคแล้วPython จะใช้การส่งผ่านค่าอ้างอิงเสมอ ฉันจะทำซ้ำคำตอบอื่น ๆ ของฉันเพื่อสนับสนุนคำสั่งของฉัน

Python ใช้ค่าการส่งผ่านข้อมูลอ้างอิงเสมอ ไม่มีข้อยกเว้นใด ๆ การกำหนดตัวแปรหมายถึงการคัดลอกค่าอ้างอิง ไม่มีข้อยกเว้น. ตัวแปรใด ๆ คือชื่อที่ถูกผูกไว้กับค่าอ้างอิง เสมอ.

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

นี่คือตัวอย่างที่พิสูจน์ว่า Python ใช้การส่งผ่านโดยอ้างอิง:

ตัวอย่างที่แสดงให้เห็นถึงการผ่านการโต้แย้ง

หากมีการส่งผ่านอาร์กิวเมนต์โดยค่าด้านนอกlstไม่สามารถแก้ไขได้ สีเขียวคือวัตถุเป้าหมาย (สีดำคือค่าที่เก็บไว้ภายในสีแดงคือประเภทวัตถุ) สีเหลืองคือหน่วยความจำที่มีค่าอ้างอิงอยู่ภายใน - วาดเป็นลูกศร ลูกศรทึบสีน้ำเงินคือค่าอ้างอิงที่ส่งผ่านไปยังฟังก์ชัน (ผ่านเส้นทางลูกศรสีน้ำเงินประ) สีเหลืองเข้มน่าเกลียดเป็นพจนานุกรมภายใน (อันที่จริงมันอาจถูกวาดเป็นวงรีสีเขียวสีและรูปร่างบอกว่ามันเป็นภายในเท่านั้น)

คุณสามารถใช้id()ฟังก์ชันในตัวเพื่อเรียนรู้ว่าค่าอ้างอิงคืออะไร (นั่นคือที่อยู่ของวัตถุเป้าหมาย)

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

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


1
จริงๆแล้วนี่เป็นการยืนยันการส่งผ่านโดยอ้างอิง +1 สำหรับคำตอบนี้ถึงแม้ว่าตัวอย่างจะไม่ดี
BugShotGG

30
การประดิษฐ์คำศัพท์ใหม่ (เช่น "การส่งผ่านค่าอ้างอิง" หรือ "การเรียกโดยวัตถุ" ไม่เป็นประโยชน์) "การโทรโดย (ค่า | การอ้างอิง | ชื่อ)" เป็นคำมาตรฐาน "การอ้างอิง" เป็นคำมาตรฐาน การส่งผ่านการอ้างอิงโดยค่าจะอธิบายพฤติกรรมของ Python, Java และโฮสต์ของภาษาอื่นอย่างถูกต้องโดยใช้คำศัพท์มาตรฐาน
cayhorstmann

4
@cayhorstmann: ปัญหาคือว่าตัวแปร Pythonไม่มีคำศัพท์ที่เหมือนกันกับภาษาอื่น วิธีนี้การโทรโดยการอ้างอิงไม่เหมาะที่นี่ นอกจากนี้คุณจะกำหนดคำอ้างอิงได้อย่างไร อย่างไม่เป็นทางการวิธี Python สามารถอธิบายได้อย่างง่ายดายว่าผ่านที่อยู่ของวัตถุ แต่มันไม่เหมาะกับการใช้งาน Python แบบกระจาย
pepr

1
ฉันชอบคำตอบนี้ แต่คุณอาจพิจารณาว่าตัวอย่างช่วยหรือทำร้ายผู้อื่นหรือไม่ นอกจากนี้หากคุณแทนที่ 'ค่าอ้างอิง' ด้วย 'การอ้างอิงวัตถุ' คุณจะใช้คำศัพท์ที่เราสามารถพิจารณา 'เป็นทางการ' ดังที่เห็นที่นี่: การกำหนดฟังก์ชั่น
ซื่อสัตย์ Abe

2
มีเชิงอรรถที่ระบุไว้ในตอนท้ายของเครื่องหมายคำพูดซึ่งอ่านว่า: "จริง ๆ แล้วการโทรโดยการอ้างอิงวัตถุจะเป็นคำอธิบายที่ดีกว่าเนื่องจากหากมีวัตถุที่ไม่แน่นอนถูกส่งผ่านผู้เรียกจะเห็นการเปลี่ยนแปลงใด ๆ "ฉันเห็นด้วยกับคุณว่าความสับสนนั้นเกิดจากการพยายามปรับให้เข้ากับคำศัพท์ที่ใช้กับภาษาอื่น ความหมายกันสิ่งที่ต้องเข้าใจคือ: พจนานุกรม / เนมสเปซการดำเนินการเชื่อมโยงชื่อและความสัมพันธ์ของชื่อ→ตัวชี้→วัตถุ (ตามที่คุณรู้อยู่แล้ว)
Abe ซื่อสัตย์

56

ไม่มีตัวแปรใน Python

กุญแจสำคัญในการทำความเข้าใจการส่งผ่านพารามิเตอร์คือหยุดคิดถึง "ตัวแปร" มีชื่อและวัตถุใน Python และมีลักษณะเหมือนตัวแปร แต่มีประโยชน์ที่จะแยกความแตกต่างของทั้งสาม

  1. Python มีชื่อและวัตถุ
  2. การกำหนดผูกชื่อกับวัตถุ
  3. การส่งอาร์กิวเมนต์ไปยังฟังก์ชันจะผูกชื่อ (ชื่อพารามิเตอร์ของฟังก์ชัน) ไปยังวัตถุ

นั่นคือทั้งหมดที่มีให้มัน ความไม่แน่นอนนั้นไม่เกี่ยวข้องกับคำถามนี้

ตัวอย่าง:

a = 1

สิ่งนี้จะผูกชื่อaกับวัตถุชนิดจำนวนเต็มที่เก็บค่า 1

b = x

สิ่งนี้จะผูกชื่อbกับวัตถุเดียวกันกับที่ชื่อxนั้นถูกผูกไว้กับ หลังจากนั้นชื่อbไม่เกี่ยวข้องกับชื่อxอีกต่อไป

ดูหัวข้อ3.1และ4.2ในการอ้างอิงภาษา Python 3

วิธีอ่านตัวอย่างในคำถาม

ในรหัสที่แสดงในคำถามคำสั่งself.Change(self.variable)ผูกชื่อvar(ในขอบเขตของฟังก์ชั่นChange) กับวัตถุที่เก็บค่า'Original'และการมอบหมายvar = 'Changed'(ในร่างกายของฟังก์ชั่นChange) กำหนดชื่อเดียวกันนั้นอีกครั้ง: กับวัตถุอื่น ๆ (ที่เกิดขึ้น ถือสายอักขระไว้เช่นกัน แต่อาจเป็นอย่างอื่นก็ได้)

วิธีการส่งต่อโดยการอ้างอิง

ดังนั้นหากสิ่งที่คุณต้องการเปลี่ยนเป็นวัตถุที่ไม่แน่นอนซึ่งไม่มีปัญหาเนื่องจากทุกอย่างผ่านการอ้างอิงอย่างมีประสิทธิภาพ

ถ้ามันเป็นวัตถุที่ไม่เปลี่ยนรูป (เช่นบูล, จำนวน, สตริง) วิธีที่จะไปคือการห่อมันในวัตถุที่ไม่แน่นอน
วิธีแก้ปัญหาที่รวดเร็วและสกปรกสำหรับรายการนี้เป็นรายการองค์ประกอบเดียว (แทนself.variableส่งผ่าน[self.variable]และแก้ไขฟังก์ชั่นvar[0]) วิธีการแบบpythonic ที่
มากขึ้นคือการแนะนำคลาสที่มีลักษณะเป็นเรื่องไม่สำคัญ ฟังก์ชั่นได้รับอินสแตนซ์ของคลาสและปรับเปลี่ยนแอตทริบิวต์


20
"Python ไม่มีตัวแปร" เป็นคำขวัญที่งี่เง่าและสับสนและฉันหวังว่าผู้คนจะหยุดพูดว่า ... :( คำตอบที่เหลือนี้ดีมาก!
เน็ดแบทเชลเดอร์

9
มันอาจจะน่าตกใจ แต่ก็ไม่ได้โง่ และฉันก็ไม่คิดว่ามันสับสนเช่นกัน: หวังว่าจะเปิดใจของผู้รับสำหรับคำอธิบายที่กำลังจะมาถึงและทำให้เธอมีประโยชน์ "ฉันสงสัยว่าสิ่งที่พวกเขามีทัศนคติแทนตัวแปร" (ใช่ไมล์สะสมของคุณอาจแตกต่างกันไป)
Lutz Prechelt

13
คุณจะบอกว่า Javascript ไม่มีตัวแปรหรือไม่ พวกเขาทำงานเหมือนกับ Python นอกจากนี้ Java, Ruby, PHP, .... ฉันคิดว่าเทคนิคการสอนที่ดีกว่าคือ "ตัวแปรของ Python ทำงานแตกต่างจาก C"
Ned Batchelder

9
ใช่ Java มีตัวแปร ดังนั้น Python และ JavaScript, Ruby, PHP และอื่น ๆ คุณจะไม่พูดใน Java ที่intประกาศตัวแปร แต่Integerทำไม่ได้ พวกเขาทั้งสองประกาศตัวแปร Integerตัวแปรคือวัตถุที่intตัวแปรดั้งเดิม a = 1; b = a; a++ # doesn't modify bตัวอย่างเช่นคุณแสดงให้เห็นว่าตัวแปรของคุณทำงานโดยการแสดง นั่นเป็นเรื่องจริงใน Python เช่นกัน (ใช้+= 1เนื่องจากไม่มี++ใน Python)!
Ned Batchelder

1
แนวคิดของ "ตัวแปร" มีความซับซ้อนและมักจะคลุมเครือ: ตัวแปรเป็นคอนเทนเนอร์สำหรับค่าที่ระบุโดยชื่อ ใน Python ค่าเป็นวัตถุภาชนะบรรจุเป็นวัตถุ (ดูปัญหาหรือไม่) และชื่อเป็นสิ่งที่แยกจากกัน ผมเชื่อว่ามันจะรุนแรงมากที่จะได้รับความถูกต้องความเข้าใจของตัวแปรในลักษณะนี้ คำอธิบายชื่อและวัตถุปรากฏยากขึ้น แต่จริงๆแล้วง่ายกว่า
Lutz Prechelt

43

เคล็ดลับง่ายๆที่ฉันใช้ตามปกติคือห่อมันในรายการ:

def Change(self, var):
    var[0] = 'Changed'

variable = ['Original']
self.Change(variable)      
print variable[0]

(ใช่ฉันรู้ว่าสิ่งนี้อาจไม่สะดวก แต่บางครั้งมันก็ง่ายพอที่จะทำสิ่งนี้)


7
+1 สำหรับข้อความจำนวนเล็กน้อยที่ให้วิธีแก้ปัญหาที่จำเป็นสำหรับปัญหาของ Python ที่ไม่มีการอ้างอิงแบบอ้างอิงถึงกัน (ตามความคิดเห็นติดตาม / คำถามที่เหมาะกับที่นี่และที่ใดก็ได้ในหน้านี้: มันไม่ชัดเจนสำหรับฉันทำไมหลามไม่สามารถให้คำหลัก "อ้างอิง" เช่น C # ไม่เพียงแค่ตัดอาร์กิวเมนต์ของผู้โทรในรายการเช่น นี้และปฏิบัติต่อการอ้างอิงถึงการโต้แย้งภายในฟังก์ชั่นเป็นองค์ประกอบที่ 0 ของรายการ)
M Katz

5
ดี ที่จะผ่านโดยอ้างอิงห่อใน [] ของ
Justas

38

(แก้ไข - Blair ได้อัปเดตคำตอบที่ได้รับความนิยมอย่างมากของเขาเพื่อให้แม่นยำ

ฉันคิดว่ามันเป็นสิ่งสำคัญที่จะต้องทราบว่าโพสต์ปัจจุบันที่มีคะแนนมากที่สุด (โดยแบลร์คอนราด) ในขณะที่ถูกต้องเกี่ยวกับผลของมันคือการทำให้เข้าใจผิดและเป็นเส้นแบ่งที่ไม่ถูกต้องตามคำจำกัดความ ในขณะที่มีหลายภาษา (เช่น C) ที่อนุญาตให้ผู้ใช้ผ่านการอ้างอิงหรือการส่งผ่านค่า Python ไม่ใช่หนึ่งในนั้น

คำตอบของ David Cournapeau ชี้ไปที่คำตอบที่แท้จริงและอธิบายว่าทำไมพฤติกรรมในตำแหน่งของแบลร์คอนราดดูเหมือนจะถูกต้องในขณะที่คำจำกัดความไม่ได้

ในกรณีที่ Python ผ่านค่าทุกภาษาจะต้องผ่านค่าเนื่องจากบางส่วนของข้อมูล (ไม่ว่าจะเป็น "ค่า" หรือ "อ้างอิง") ​​จะต้องส่ง อย่างไรก็ตามนั่นไม่ได้หมายความว่า Python ผ่านค่าตามความหมายที่โปรแกรมเมอร์ C คิดไว้

หากคุณต้องการพฤติกรรมคำตอบของแบลร์คอนราดก็ใช้ได้ แต่ถ้าคุณต้องการรู้ว่าถั่วและสลักเกลียวของสาเหตุที่ Python ไม่ผ่านค่าหรือผ่านการอ้างอิงอ่านคำตอบของ David Cournapeau


4
มันไม่เป็นความจริงเลยที่ภาษาทั้งหมดจะถูกเรียกตามค่า ใน C ++ หรือ Pascal (และอื่น ๆ อีกมากมายที่ฉันไม่รู้) คุณมีการโทรโดยการอ้างอิง ตัวอย่างเช่นใน C ++ void swap(int& x, int& y) { int temp = x; x = y; y = temp; }จะสลับตัวแปรที่ส่งผ่านไป ในปาสกาลคุณใช้แทนvar &
cayhorstmann

2
ฉันคิดว่าฉันตอบไปนานแล้ว แต่ฉันไม่เห็น เพื่อความสมบูรณ์ - cayhorstmann เข้าใจผิดคำตอบของฉัน ผมก็ไม่ได้บอกว่าทุกอย่างเป็นไปด้วยค่าโทรในแง่ที่คนส่วนใหญ่เรียนรู้เกี่ยวกับ C / C ++ มันเป็นเพียงว่ามีการส่งค่าบางอย่าง (ค่าชื่อตัวชี้และอื่น ๆ ) และคำที่ใช้ในคำตอบดั้งเดิมของ Blair นั้นไม่ถูกต้อง
KobeJohn

24

คุณได้คำตอบที่ดีจริงๆที่นี่

x = [ 2, 4, 4, 5, 5 ]
print x  # 2, 4, 4, 5, 5

def go( li ) :
  li = [ 5, 6, 7, 8 ]  # re-assigning what li POINTS TO, does not
  # change the value of the ORIGINAL variable x

go( x ) 
print x  # 2, 4, 4, 5, 5  [ STILL! ]


raw_input( 'press any key to continue' )

2
ใช่ แต่ถ้าคุณทำ x = [2, 4, 4, 5, 5], y = x, X [0] = 1 พิมพ์ x # [1, 4, 4, 5, 5] พิมพ์ y # [1 , 4, 4, 5, 5]
laycat

19

ในกรณีนี้ตัวแปรที่มีชื่อvarในวิธีการChangeนั้นจะได้รับการอ้างอิงถึงself.variableและคุณจะกำหนดสตริงให้varทันที self.variableมันไม่ได้ชี้ไปที่ ตัวอย่างโค้ดต่อไปนี้แสดงสิ่งที่จะเกิดขึ้นหากคุณแก้ไขโครงสร้างข้อมูลที่ชี้ไปตามvarและself.variableในกรณีนี้จะมีรายการ:

>>> class PassByReference:
...     def __init__(self):
...         self.variable = ['Original']
...         self.change(self.variable)
...         print self.variable
...         
...     def change(self, var):
...         var.append('Changed')
... 
>>> q = PassByReference()
['Original', 'Changed']
>>> 

ฉันแน่ใจว่ามีคนอื่นสามารถชี้แจงเพิ่มเติมได้


18

รูปแบบการส่งต่อการมอบหมายงานของ Python นั้นไม่เหมือนกับตัวเลือกพารามิเตอร์การอ้างอิงของ C ++ แต่มันกลับกลายเป็นว่าคล้ายกับรูปแบบการส่งผ่านอาร์กิวเมนต์ของภาษา C (และอื่น ๆ ) ในทางปฏิบัติ:

  • อาร์กิวเมนต์ที่ไม่เปลี่ยนรูปจะถูกส่งผ่านอย่างมีประสิทธิภาพ“ ตามค่า ” วัตถุเช่นจำนวนเต็มและสตริงจะถูกส่งผ่านโดยการอ้างอิงวัตถุแทนโดยการคัดลอก แต่เนื่องจากคุณไม่สามารถเปลี่ยนวัตถุที่ไม่เปลี่ยนรูปได้ แต่อย่างใดเอฟเฟกต์จึงเหมือนกับการทำสำเนา
  • อาร์กิวเมนต์ที่ไม่แน่นอนถูกส่งผ่านอย่างมีประสิทธิภาพ“ โดยตัวชี้ ” วัตถุเช่นรายการและพจนานุกรมก็ถูกส่งผ่านโดยการอ้างอิงวัตถุซึ่งคล้ายกับวิธีที่ C ส่งผ่านอาร์เรย์เป็นพอยน์เตอร์ - วัตถุที่ไม่แน่นอนสามารถเปลี่ยนแปลงได้ในสถานที่ในฟังก์ชั่นเช่นเดียวกับอาร์เรย์ C

16

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

http://docs.python.org/3/faq/programming.html#what-are-the-rules-for-local-and-global-variables-in-python

ตัวอย่าง:

>>> def x(y):
...     global z
...     z = y
...

>>> x
<function x at 0x00000000020E1730>
>>> y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'y' is not defined
>>> z
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'z' is not defined

>>> x(2)
>>> x
<function x at 0x00000000020E1730>
>>> y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'y' is not defined
>>> z
2

5
ฉันถูกล่อลวงให้โพสต์คำตอบที่คล้ายกัน - ผู้ถามดั้งเดิมอาจไม่ทราบว่าสิ่งที่เขาต้องการคือการใช้ตัวแปรทั่วโลกร่วมกันระหว่างฟังก์ชั่น นี่คือลิงก์ที่ฉันจะแบ่งปัน: stackoverflow.com/questions/423379/… ในการตอบกลับถึง @Tim Stack Overflow ไม่ได้เป็นเพียงเว็บไซต์คำถามและคำตอบเท่านั้นมันเป็นแหล่งเก็บความรู้อ้างอิงขนาดใหญ่ที่แข็งแกร่งขึ้นและเหมาะสมยิ่งขึ้น เหมือนวิกิที่ใช้งานอยู่ที่มีอินพุตมากขึ้น
Max P Magee

13

ข้อมูลเชิงลึกมากมายในคำตอบที่นี่ แต่ฉันคิดว่าจุดเพิ่มเติมไม่ได้กล่าวถึงที่นี่อย่างชัดเจน การอ้างอิงจากเอกสารไพ ธ อนhttps://docs.python.org/2/faq/programming.html#what-are-the-rules-for-local-and-global-variables-in-python

"ใน Python ตัวแปรที่อ้างอิงภายในฟังก์ชั่นนั้นเป็นค่าโกลบอลโดยปริยายหากตัวแปรถูกกำหนดค่าใหม่ที่ใดก็ได้ภายในร่างกายของฟังก์ชันมันจะถือว่าเป็นค่าในท้องถิ่นหากตัวแปรได้รับการกำหนดค่าใหม่ภายในฟังก์ชัน ตัวแปรนั้นเป็นโลคัลโดยปริยายและคุณต้องประกาศอย่างชัดเจนว่า 'global' แม้ว่าในตอนแรกจะมีเรื่องที่น่าแปลกใจเล็กน้อย แต่การพิจารณาสักครู่อธิบายสิ่งนี้ในแง่หนึ่งการใช้ global สำหรับตัวแปรที่ได้รับมอบหมายนั้น ในทางกลับกันถ้าจำเป็นต้องใช้โกลบอลสำหรับการอ้างอิงโกลบอลทั้งหมดคุณจะต้องใช้โกลบอลตลอดเวลาคุณจะต้องประกาศว่าเป็นโกลบอลทุกการอ้างอิงถึงฟังก์ชันในตัวหรือส่วนประกอบของโมดูลที่นำเข้า จะกำจัดประโยชน์ของการประกาศระดับโลกสำหรับการระบุผลข้างเคียง "

แม้เมื่อผ่านวัตถุที่ไม่แน่นอนไปยังฟังก์ชั่นนี้ยังคงใช้ และให้ฉันอธิบายเหตุผลของความแตกต่างในพฤติกรรมระหว่างการกำหนดให้กับวัตถุและปฏิบัติการบนวัตถุในฟังก์ชั่น

def test(l):
    print "Received", l , id(l)
    l = [0, 0, 0]
    print "Changed to", l, id(l)  # New local object created, breaking link to global l

l= [1,2,3]
print "Original", l, id(l)
test(l)
print "After", l, id(l)

ให้:

Original [1, 2, 3] 4454645632
Received [1, 2, 3] 4454645632
Changed to [0, 0, 0] 4474591928
After [1, 2, 3] 4454645632

การกำหนดให้กับตัวแปรโกลบอลที่ไม่ได้ประกาศเป็นโกลบอลจึงสร้างอ็อบเจ็กต์โลคัลใหม่และแบ่งลิงก์ไปยังอ็อบเจ็กต์ดั้งเดิม


10

นี่คือคำอธิบายง่ายๆ (ฉันหวังว่า) ของแนวคิดที่pass by objectใช้ใน Python
เมื่อใดก็ตามที่คุณส่งวัตถุไปยังฟังก์ชันวัตถุนั้นจะถูกส่งผ่าน (วัตถุใน Python เป็นสิ่งที่คุณต้องการเรียกค่าในภาษาการเขียนโปรแกรมอื่น) ไม่ใช่การอ้างอิงถึงวัตถุนี้ กล่าวอีกนัยหนึ่งเมื่อคุณโทร:

def change_me(list):
   list = [1, 2, 3]

my_list = [0, 1]
change_me(my_list)

วัตถุจริง - [0, 1] (ซึ่งจะเรียกว่าค่าในภาษาการเขียนโปรแกรมอื่น ๆ ) จะถูกส่งผ่าน ดังนั้นในความเป็นจริงฟังก์ชั่นchange_meจะพยายามทำสิ่งที่ชอบ:

[0, 1] = [1, 2, 3]

ซึ่งแน่นอนจะไม่เปลี่ยนวัตถุที่ส่งผ่านไปยังฟังก์ชัน หากฟังก์ชั่นเป็นดังนี้:

def change_me(list):
   list.append(2)

จากนั้นการโทรจะส่งผลให้:

[0, 1].append(2)

ซึ่งเห็นได้ชัดว่าจะเปลี่ยนวัตถุ คำตอบนี้อธิบายได้ดี


2
ปัญหาคือการมอบหมายงานทำอย่างอื่นมากกว่าที่คุณคาดหวัง list = [1, 2, 3]สาเหตุการนำlistชื่ออย่างอื่นและลืมวัตถุผ่านเดิม อย่างไรก็ตามคุณสามารถลองlist[:] = [1, 2, 3](โดยวิธีlistนี้เป็นชื่อที่ไม่ถูกต้องสำหรับตัวแปรการคิดเกี่ยวกับ[0, 1] = [1, 2, 3]มันเป็นเรื่องไร้สาระที่สมบูรณ์อย่างไรก็ตามคุณคิดว่าหมายความว่าวัตถุจะถูกส่งผ่านไปยังสิ่งที่คัดลอกไปยังฟังก์ชั่นในความคิดของคุณ?
pepr

@pepr วัตถุไม่ใช่ตัวอักษร พวกเขาเป็นวัตถุ วิธีเดียวที่จะพูดคุยเกี่ยวกับพวกเขาคือให้ชื่อพวกเขา นั่นเป็นเหตุผลที่ง่ายมากเมื่อคุณเข้าใจ แต่มีความซับซ้อนอย่างมากที่จะอธิบาย :-)
2557

@Veky: ฉันรู้ว่า อย่างไรก็ตามรายการตัวอักษรจะถูกแปลงเป็นวัตถุรายการ ที่จริงแล้ววัตถุใด ๆ ใน Python สามารถมีอยู่ได้โดยไม่มีชื่อและสามารถใช้งานได้แม้ว่าจะไม่ได้รับชื่อก็ตาม และคุณสามารถคิดถึงพวกเขาเกี่ยวกับวัตถุนิรนาม คิดเกี่ยวกับวัตถุที่เป็นองค์ประกอบของรายการ พวกเขาไม่ต้องการชื่อ คุณสามารถเข้าถึงได้ผ่านการจัดทำดัชนีหรือวนซ้ำผ่านรายการ อย่างไรก็ตามฉันยืนยันว่า[0, 1] = [1, 2, 3]เป็นเพียงตัวอย่างที่ไม่ดี ไม่มีอะไรแบบนั้นใน Python
pepr

@pepr: ฉันไม่ได้หมายถึงชื่อ Python-definition เพียงชื่อสามัญ แน่นอนalist[2]ว่าเป็นชื่อขององค์ประกอบที่สามของอลิส แต่ฉันคิดว่าฉันเข้าใจผิดว่าปัญหาของคุณคืออะไร :-)
Veky

โอ๊ะ เห็นได้ชัดว่าภาษาอังกฤษของฉันแย่กว่า Python มาก :-) ฉันจะลองอีกครั้ง ฉันแค่บอกว่าคุณต้องตั้งชื่อบางชื่อเพื่อพูดคุยเกี่ยวกับพวกเขา จาก "ชื่อ" ฉันไม่ได้หมายถึง "ชื่อตามที่กำหนดโดย Python" ฉันรู้กลไกของหลามไม่ต้องกังวล
Veky

8

นอกเหนือจากคำอธิบายที่ยอดเยี่ยมทั้งหมดเกี่ยวกับวิธีการทำงานของสิ่งนี้ใน Python ฉันไม่เห็นคำแนะนำง่ายๆสำหรับปัญหา ในขณะที่คุณสร้างวัตถุและอินสแตนซ์วิธี pythonic ของการจัดการตัวแปรอินสแตนซ์และการเปลี่ยนแปลงดังต่อไปนี้:

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.Change()
        print self.variable

    def Change(self):
        self.variable = 'Changed'

ในวิธีการแบบอินสแตนซ์โดยปกติคุณจะอ้างถึงselfการเข้าถึงคุณลักษณะของอินสแตนซ์ เป็นเรื่องปกติที่จะตั้งค่าแอตทริบิวต์ของอินสแตนซ์ใน__init__และอ่านหรือเปลี่ยนแปลงในวิธีการอินสแตนซ์ นั่นคือเหตุผลว่าทำไมคุณผ่านselfALS def Changeอาร์กิวเมนต์แรก

อีกวิธีหนึ่งคือการสร้างวิธีการคงที่เช่นนี้:

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.variable = PassByReference.Change(self.variable)
        print self.variable

    @staticmethod
    def Change(var):
        var = 'Changed'
        return var

6

มีเคล็ดลับเล็กน้อยในการส่งวัตถุโดยการอ้างอิงถึงแม้ว่าภาษาจะไม่สามารถทำได้ มันใช้งานได้ใน Java เช่นกันมันเป็นรายการที่มีหนึ่งรายการ ;-)

class PassByReference:
    def __init__(self, name):
        self.name = name

def changeRef(ref):
    ref[0] = PassByReference('Michael')

obj = PassByReference('Peter')
print obj.name

p = [obj] # A pointer to obj! ;-)
changeRef(p)

print p[0].name # p->name

มันแฮ็คที่น่าเกลียด แต่ก็ใช้ได้ ;-P


pobjคือการอ้างอิงไปยังวัตถุของรายการที่ไม่แน่นอนซึ่งในทางกลับเก็บวัตถุ อ้างอิง 'p' changeRefได้รับการผ่านเข้าสู่ ภายในchangeRefการอ้างอิงใหม่จะถูกสร้างขึ้น (การอ้างอิงใหม่ที่เรียกว่าref) ที่ชี้ไปที่วัตถุรายการเดียวกันที่pชี้ไปที่ แต่เนื่องจากรายการไม่แน่นอนการเปลี่ยนแปลงในรายการจึงสามารถมองเห็นได้โดยการอ้างอิงทั้งสอง ในกรณีนี้คุณใช้การrefอ้างอิงเพื่อเปลี่ยนวัตถุที่ดัชนี 0 เพื่อที่จะเก็บPassByReference('Michael')วัตถุในภายหลัง การเปลี่ยนแปลงวัตถุของรายการที่ได้รับการทำโดยใช้แต่การเปลี่ยนแปลงนี้จะมองเห็นref p
Minh Tran

ดังนั้นตอนนี้การอ้างอิงpและชี้ไปที่วัตถุของรายการที่ร้านค้าวัตถุเดียวref PassByReference('Michael')ดังนั้นมันจึงp[0].nameกลับMichaelมาที่ แน่นอนว่าrefตอนนี้ไม่ได้อยู่ในขอบเขตและอาจมีการรวบรวมขยะ แต่ทั้งหมดนี้เหมือนกัน
Minh Tran

คุณยังไม่ได้เปลี่ยนตัวแปรอินสแตนซ์ส่วนตัวnameของPassByReferenceวัตถุต้นฉบับที่เกี่ยวข้องกับการอ้างอิงobjแม้ว่า ในความเป็นจริงจะกลับมาobj.name Peterความคิดเห็นดังกล่าวข้างต้นถือว่าคำนิยามที่Mark Ransomให้
Minh Tran

ประเด็นคือฉันไม่เห็นด้วยว่ามันเป็นแฮ็ค (ซึ่งฉันใช้เพื่อหมายถึงสิ่งที่ใช้งานได้ คุณเพียงแค่แทนที่PassByReferenceวัตถุหนึ่งด้วยวัตถุอื่นPassByReferenceในรายการของคุณและอ้างอิงถึงหลังของวัตถุทั้งสอง
Minh Tran

5

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

a=0
b=0
c=0
def myfunc(a,b,c):
    a=1
    b=2
    c=3
    return a,b,c

a,b,c = myfunc(a,b,c)
print a,b,c

ใช่สิ่งนี้จะแก้ปัญหา 'การส่งต่ออ้างอิง' ในกรณีใช้งานของฉันด้วย ฉันมีฟังก์ชั่นที่พื้นทำความสะอาดขึ้นค่าในที่แล้วส่งกลับdict dictอย่างไรก็ตามในขณะที่ทำความสะอาดมันอาจเห็นได้ชัดว่าจำเป็นต้องสร้างส่วนของระบบขึ้นใหม่ ดังนั้นฟังก์ชั่นต้องไม่เพียง แต่คืนค่าการล้างdictแต่ยังสามารถส่งสัญญาณการสร้างใหม่ได้ ฉันพยายามผ่านการboolอ้างอิง แต่ ofc ที่ไม่ทำงาน การหาวิธีแก้ปัญหานี้ฉันพบวิธีแก้ปัญหาของคุณ (โดยทั่วไปแล้วคืนค่า tuple) ให้ทำงานได้ดีที่สุดในขณะที่ยังไม่ถูกแฮ็ค / วิธีแก้ปัญหาเลย (IMHO)
kasimir

3

ในขณะที่การส่งผ่านการอ้างอิงไม่มีสิ่งใดที่เหมาะกับงูหลามและควรใช้ไม่ค่อยมีการแก้ไขปัญหาบางอย่างที่จริง ๆ แล้วสามารถทำงานเพื่อให้ได้วัตถุที่กำหนดให้กับตัวแปรโลคัลหรือแม้แต่กำหนดตัวแปรท้องถิ่นจากภายในของฟังก์ชันที่เรียกอีกครั้ง

แนวคิดพื้นฐานคือการมีฟังก์ชั่นที่สามารถเข้าถึงและสามารถส่งผ่านเป็นวัตถุไปยังฟังก์ชั่นอื่น ๆ หรือเก็บไว้ในชั้นเรียน

วิธีหนึ่งคือใช้global(สำหรับตัวแปรโกลบอล) หรือnonlocal(สำหรับตัวแปรโลคัลในฟังก์ชัน) ในฟังก์ชัน wrapper

def change(wrapper):
    wrapper(7)

x = 5
def setter(val):
    global x
    x = val
print(x)

แนวคิดเดียวกันนี้ใช้สำหรับการอ่านและdelการใช้ตัวแปร

สำหรับการอ่านมีวิธีที่สั้นกว่าในการใช้เพียงแค่lambda: xคืนค่า callable ที่เมื่อเรียกคืนค่าปัจจุบันของ x นี่เป็นเหมือน "การโทรตามชื่อ" ที่ใช้ในภาษาในอดีตอันไกลโพ้น

การส่ง 3 wrappers เพื่อเข้าถึงตัวแปรนั้นค่อนข้างยากลำบากดังนั้นจึงสามารถห่อเป็นคลาสที่มีแอตทริบิวต์ proxy:

class ByRef:
    def __init__(self, r, w, d):
        self._read = r
        self._write = w
        self._delete = d
    def set(self, val):
        self._write(val)
    def get(self):
        return self._read()
    def remove(self):
        self._delete()
    wrapped = property(get, set, remove)

# left as an exercise for the reader: define set, get, remove as local functions using global / nonlocal
r = ByRef(get, set, remove)
r.wrapped = 15

การสนับสนุน Pythons "reflection" ทำให้สามารถรับวัตถุที่มีความสามารถในการกำหนดชื่อ / ตัวแปรใหม่ในขอบเขตที่กำหนดโดยไม่ต้องกำหนดฟังก์ชันอย่างชัดเจนในขอบเขตนั้น:

class ByRef:
    def __init__(self, locs, name):
        self._locs = locs
        self._name = name
    def set(self, val):
        self._locs[self._name] = val
    def get(self):
        return self._locs[self._name]
    def remove(self):
        del self._locs[self._name]
    wrapped = property(get, set, remove)

def change(x):
    x.wrapped = 7

def test_me():
    x = 6
    print(x)
    change(ByRef(locals(), "x"))
    print(x)

ที่นี่ByRefชั้นเรียนล้อมการเข้าถึงพจนานุกรม ดังนั้นการเข้าถึงแอททริบิวจะwrappedถูกแปลเป็นการเข้าถึงไอเท็มในพจนานุกรมที่ส่งผ่าน ด้วยการส่งผลลัพธ์ของ builtin localsและชื่อของตัวแปรท้องถิ่นทำให้การเข้าถึงตัวแปรท้องถิ่นสิ้นสุดลง เอกสารหลามจาก 3.5 แนะนำว่าการเปลี่ยนพจนานุกรมอาจไม่ทำงาน แต่ดูเหมือนว่าจะทำงานให้ฉันได้


3

ด้วยวิธีที่หลามจัดการกับค่าและการอ้างอิงถึงมันวิธีเดียวที่คุณสามารถอ้างอิงแอตทริบิวต์ของอินสแตนซ์โดยพลการคือชื่อ:

class PassByReferenceIsh:
    def __init__(self):
        self.variable = 'Original'
        self.change('variable')
        print self.variable

    def change(self, var):
        self.__dict__[var] = 'Changed'

ในรหัสจริงที่คุณต้องการเพิ่มการตรวจสอบข้อผิดพลาดในการค้นหา dict


3

เนื่องจากพจนานุกรมถูกส่งผ่านโดยการอ้างอิงคุณสามารถใช้ตัวแปร dict เพื่อเก็บค่าอ้างอิงใด ๆ ที่อยู่ภายใน

# returns the result of adding numbers `a` and `b`
def AddNumbers(a, b, ref): # using a dict for reference
    result = a + b
    ref['multi'] = a * b # reference the multi. ref['multi'] is number
    ref['msg'] = "The result: " + str(result) + " was nice!" # reference any string (errors, e.t.c). ref['msg'] is string
    return result # return the sum

number1 = 5
number2 = 10
ref = {} # init a dict like that so it can save all the referenced values. this is because all dictionaries are passed by reference, while strings and numbers do not.

sum = AddNumbers(number1, number2, ref)
print("sum: ", sum)             # the return value
print("multi: ", ref['multi'])  # a referenced value
print("msg: ", ref['msg'])      # a referenced value

3

Pass-By-Reference ใน Python ค่อนข้างแตกต่างจากแนวคิดของ pass โดยการอ้างอิงใน C ++ / Java

  • Java & C #:ประเภทดั้งเดิม (รวมสตริง) ผ่านค่า (คัดลอก) ประเภทการอ้างอิงจะถูกส่งผ่านโดยการอ้างอิง (คัดลอกที่อยู่) ดังนั้นการเปลี่ยนแปลงทั้งหมดที่เกิดขึ้นในพารามิเตอร์ในฟังก์ชั่นที่เรียกว่าสามารถมองเห็นได้โทร
  • C ++: อนุญาตให้ใช้ได้ทั้งการอ้างอิงผ่านหรือการผ่านค่า หากพารามิเตอร์ถูกส่งผ่านโดยการอ้างอิงคุณสามารถแก้ไขได้หรือไม่ขึ้นอยู่กับว่าพารามิเตอร์ถูกส่งเป็น const หรือไม่ อย่างไรก็ตาม const หรือไม่พารามิเตอร์รักษาการอ้างอิงไปยังวัตถุและการอ้างอิงไม่สามารถกำหนดให้ชี้ไปที่วัตถุที่แตกต่างภายในฟังก์ชั่นที่เรียกว่า
  • Python: Python คือ "การอ้างอิงแบบวัตถุต่อครั้ง" ซึ่งมักจะกล่าวว่า: "การอ้างอิงวัตถุถูกส่งผ่านโดยค่า" [อ่านที่นี่] 1. ทั้งผู้เรียกและฟังก์ชั่นอ้างถึงวัตถุเดียวกัน แต่พารามิเตอร์ในฟังก์ชั่นเป็นตัวแปรใหม่ซึ่งเป็นเพียงการถือสำเนาของวัตถุในการโทร เช่นเดียวกับ C ++ พารามิเตอร์สามารถแก้ไขได้หรือไม่ได้อยู่ในฟังก์ชั่น - ขึ้นอยู่กับประเภทของวัตถุที่ส่งผ่าน เช่น; ไม่สามารถแก้ไขชนิดของวัตถุที่ไม่เปลี่ยนรูปแบบในฟังก์ชันที่เรียกได้ในขณะที่วัตถุที่ไม่แน่นอนสามารถอัปเดตหรือเริ่มต้นใหม่ได้ ความแตกต่างที่สำคัญระหว่างการอัพเดตหรือการกำหนดค่าใหม่ / การเริ่มต้นตัวแปรที่ไม่แน่นอนคือค่าที่อัพเดตจะได้รับการสะท้อนกลับในฟังก์ชั่นที่เรียกในขณะที่ค่าเริ่มต้นไม่ได้ ขอบเขตของการมอบหมายวัตถุใหม่ให้กับตัวแปรที่ไม่แน่นอนนั้นเป็นของท้องถิ่นในฟังก์ชันของไพ ธ อน ตัวอย่างที่มีให้โดย @ blair-conrad

1
เก่า แต่ฉันรู้สึกว่าจำเป็นต้องแก้ไข สตริงถูกส่งผ่านโดยการอ้างอิงทั้งใน Java และ C # ไม่ใช่ตามค่า
John

2

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

class PassByReference:
    def __init__(self):
        self.variable = 'Original'
        self.change('variable')
        print(self.variable)

    def change(self, var):
        setattr(self, var, 'Changed')

# o.variable will equal 'Changed'
o = PassByReference()
assert o.variable == 'Changed'

1
ถึงแม้ว่างานนี้ มันไม่ได้ผ่านการอ้างอิง มันคือ 'ผ่านการอ้างอิงวัตถุ'
Bishwas Mishra

1

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

class RefsObj(object):
    "A class which helps to create references to variables."
    pass

...

# an example of usage
def change_ref_var(ref_obj):
    ref_obj.val = 24

ref_obj = RefsObj()
ref_obj.val = 1
print(ref_obj.val) # or print ref_obj.val for python2
change_ref_var(ref_obj)
print(ref_obj.val)

1

เนื่องจากดูเหมือนว่าไม่มีที่ใดที่กล่าวถึงวิธีการจำลองการอ้างอิงที่รู้จักจากเช่น C ++ คือการใช้ฟังก์ชั่น "update" และส่งผ่านแทนที่จะเป็นตัวแปรจริง (หรือมากกว่า "ชื่อ"):

def need_to_modify(update):
    update(42) # set new value 42
    # other code

def call_it():
    value = 21
    def update_value(new_value):
        nonlocal value
        value = new_value
    need_to_modify(update_value)
    print(value) # prints 42

สิ่งนี้มีประโยชน์เป็นส่วนใหญ่สำหรับ "การอ้างอิงภายนอกเท่านั้น" หรือในสถานการณ์ที่มีหลายเธรด / กระบวนการ (โดยทำให้ฟังก์ชันการอัพเดตเธรด / มัลติโปรเซสเซอร์ปลอดภัย)

เห็นได้ชัดว่าข้างต้นไม่อนุญาตให้อ่านค่าเพียงอัปเดต

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