การส่งอ้างอิงใน Python: กรณีการใช้แบบเปลี่ยนแปลงได้และไม่เปลี่ยนแปลงได้

目次

1. ความแตกต่างระหว่างการส่งค่าโดยค่าและการส่งค่าโดยอ้างอิง

ใน Python มีสองวิธีในการส่งอาร์กิวเมนต์ให้ฟังก์ชัน: การส่งค่าโดยค่า (pass‑by‑value) และการส่งค่าโดยอ้างอิง (pass‑by‑reference).

  • Pass-by-value : วิธีที่ทำการคัดลอกค่ามาส่งให้ฟังก์ชันเป็นอาร์กิวเมนต์; การแก้ไขพารามิเตอร์ภายในฟังก์ชันจะไม่ส่งผลต่อค่าตัวแปรต้นฉบับ.
  • Pass-by-reference : วิธีที่ส่งอ้างอิง (ที่อยู่) ของตัวแปรให้ฟังก์ชัน, ดังนั้นการเปลี่ยนแปลงภายในฟังก์ชันจะสะท้อนกลับไปยังตัวแปรต้นฉบับ.

ใน Python พฤติกรรมนี้จะแตกต่างกันตามลักษณะของอ็อบเจ็กต์ พฤติกรรม “pass‑by‑reference” ของ Python จะใช้กับประเภทข้อมูลที่เปลี่ยนแปลงได้ (mutable) อย่างเด่นชัดและสามารถส่งผลอย่างมากต่อการทำงานของโค้ด ดังนั้นจึงสำคัญที่จะต้องเข้าใจอย่างถูกต้อง.

2. ลักษณะของอ็อบเจ็กต์ใน Python

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

  • Immutable objects : อ็อบเจ็กต์ที่ไม่สามารถแก้ไขได้หลังจากสร้างแล้ว ตัวอย่างเช่น จำนวนเต็ม (int), จำนวนทศนิยม (float), สตริง (str), ทูเพิล (tuple). เนื่องจากประเภทข้อมูลเหล่านี้ไม่สามารถเปลี่ยนแปลงได้หลังจากสร้าง การดำเนินการบนมันภายในฟังก์ชันจะไม่ส่งผลต่อผู้เรียกแม้จะส่งโดยอ้างอิง.
  • Mutable objects : อ็อบเจ็กต์ที่สามารถแก้ไขได้หลังจากสร้าง ตัวอย่างเช่น รายการ (list), พจนานุกรม (dict), เซต (set). เนื่องจากการเปลี่ยนแปลงในประเภทข้อมูลเหล่านี้ภายในฟังก์ชันจะสะท้อนกลับไปยังผู้เรียก จึงได้รับผลกระทบอย่างมากจากพฤติกรรมการส่งอ้างอิงของ Python.

RUNTEQ(ランテック)|超実戦型エンジニア育成スクール

3. วิธีที่ Python ส่งอาร์กิวเมนต์

ใน Python เมื่อคุณส่งอาร์กิวเมนต์ให้ฟังก์ชัน จะส่งอ้างอิงของอ็อบเจ็กต์ไปด้วย พฤติกรรมนี้บางครั้งเรียกว่า “pass‑by‑object‑reference” จากนี้เราจะเดินผ่านตัวอย่างที่เป็นรูปธรรมเพื่อแสดงพฤติกรรมของ Python ขึ้นอยู่กับว่าอ็อบเจ็กต์เป็น immutable หรือ mutable.

3.1 อ็อบเจ็กต์ที่ไม่เปลี่ยนแปลงได้ (Immutable Objects)

เมื่อคุณส่งอ็อบเจ็กต์ที่ไม่เปลี่ยนแปลงได้ให้ฟังก์ชัน การกำหนดค่ใหม่ภายในฟังก์ชันจะสร้างอ็อบเจ็กต์ใหม่และจะไม่กระทบต่ออ็อบเจ็กต์ต้นฉบับ.

ตัวอย่าง: การส่งจำนวนเต็มที่ไม่เปลี่ยนแปลงได้

def modify_number(num):
    num = num * 2
    print("Number inside the function:", num)

original_num = 5
modify_number(original_num)
print("Number outside the function:", original_num)

Output:

Number inside the function: 10
Number outside the function: 5

Explanation

ภายในฟังก์ชัน modify_number ตัวแปร num ถูกคูณสอง แต่การกระทำนี้ไม่ส่งผลต่อ original_num ที่อยู่นอกฟังก์ชัน เนื่องจากจำนวนเต็มเป็น immutable เมื่อกำหนดค่ใหม่จะสร้างอ็อบเจ็กต์ใหม่และ original_num จึงคงเดิม.

3.2 อ็อบเจ็กต์ที่เปลี่ยนแปลงได้ (Mutable Objects)

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

ตัวอย่าง: การส่งรายการ (List) ที่เปลี่ยนแปลงได้

def add_item(my_list):
    my_list.append("Added element")
    print("List inside the function:", my_list)

original_list = ["Element 1", "Element 2"]
add_item(original_list)
print("List outside the function:", original_list)

Output:

List inside the function: ['Element 1', 'Element 2', 'Added element']
List outside the function: ['Element 1', 'Element 2', 'Added element']

Explanation

เนื่องจากรายการเป็น mutable การเปลี่ยนแปลงที่ทำภายในฟังก์ชัน add_item จะสะท้อนโดยตรงไปยัง original_list ที่อยู่นอกฟังก์ชัน ดังนั้นอ็อบเจ็กต์ที่เปลี่ยนแปลงได้จึงถูกแชร์ระหว่างภายในและภายนอกฟังก์ชัน การแก้ไขภายในฟังก์ชันจึงส่งผลต่อผู้เรียกด้วย.

4. คำเตือนและวิธีป้องกันเมื่อใช้การส่งค่าโดยอ้างอิง

การส่งอ็อบเจ็กต์ที่เปลี่ยนแปลงได้ให้กับฟังก์ชันใน Python อาจทำให้เกิดพฤติกรรมที่ไม่คาดคิด นี่คือมาตรการป้องกันเพื่อหลีกเลี่ยงปัญหาเหล่านั้น

4.1 ใช้สำเนาของอ็อบเจ็กต์

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

ตัวอย่าง: ใช้ deepcopy เพื่อหลีกเลี่ยงปัญหาอ็อบเจ็กต์ที่เปลี่ยนแปลงได้

import copy

def add_item(my_list):
    my_list_copy = copy.deepcopy(my_list)
    my_list_copy.append("Added element")
    print("List inside the function (copy):", my_list_copy)

original_list = ["Element 1", "Element 2"]
add_item(original_list)
print("List outside the function:", original_list)

ผลลัพธ์:

List inside the function (copy): ['Element 1', 'Element 2', 'Added element']
List outside the function: ['Element 1', 'Element 2']

คำอธิบาย
โดยการใช้ deepcopy จะสร้างอ็อบเจ็กต์ใหม่ภายในฟังก์ชัน ทำให้รายการต้นฉบับไม่ถูกกระทบ นี่ทำให้สามารถเปลี่ยนแปลงได้อย่างอิสระภายในและภายนอกฟังก์ชัน

4.2 ใช้ None เป็นค่าเริ่มต้นของอาร์กิวเมนต์

หลีกเลี่ยงการตั้งค่าอ็อบเจ็กต์ที่เปลี่ยนแปลงได้เป็นค่าเริ่มต้นของฟังก์ชัน; แนะนำให้ใช้ None เป็นค่าเริ่มต้นและสร้างอ็อบเจ็กต์ใหม่ภายในฟังก์ชัน

ตัวอย่าง: วิธีปลอดภัยในการตั้งค่าอาร์กิวเมนต์เริ่มต้นเป็น None

def add_item(my_list=None):
    if my_list is None:
        my_list = []
    my_list.append("New element")
    print("List inside the function:", my_list)
    return my_list

# When no list is passed
result = add_item()
print("Returned list:", result)

# When a list is passed
existing_list = ["Element 1", "Element 2"]
result = add_item(existing_list)
print("Returned list:", result)
print("Original list:", existing_list)

ผลลัพธ์:

List inside the function: ['New element']
Returned list: ['New element']
List inside the function: ['Element 1', 'Element 2', 'New element']
Returned list: ['Element 1', 'Element 2', 'New element']
Original list: ['Element 1', 'Element 2', 'New element']

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

侍エンジニア塾

5. ตัวอย่างเชิงปฏิบัติ: โค้ดเพื่อทำความเข้าใจการส่งโดยอ้างอิงให้ลึกซึ้งขึ้น

มาทบทวนสิ่งที่เราได้เรียนรู้ด้วยตัวอย่างเชิงปฏิบัติ

ตัวอย่าง: การแก้ไขคีย์และค่าใน Dictionary

Dictionary เป็นอ็อบเจ็กต์ที่เปลี่ยนแปลงได้ ดังนั้นการดำเนินการที่ทำภายในฟังก์ชันจะส่งผลต่อ Dictionary ภายนอกฟังก์ชัน ตัวอย่างด้านล่างแสดงพฤติกรรมเมื่อเปลี่ยนคีย์และค่าของ Dictionary

def modify_dict(my_dict):
    my_dict["new key"] = "new value"
    print("Dictionary inside the function:", my_dict)

original_dict = {"key1": "value1", "key2": "value2"}
modify_dict(original_dict)
print("Dictionary outside the function:", original_dict)

ผลลัพธ์:

Dictionary inside the function: {'key1': 'value1', 'key2': 'value2', 'new key': 'new value'}
Dictionary outside the function: {'key1': 'value1', 'key2': 'value2', 'new key': 'new value'}

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

6. สรุป

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

ประเด็นสรุป

  • ความแตกต่างระหว่างการส่งค่าแบบผ่านค่า (pass-by-value) และการส่งค่าแบบผ่านอ้างอิง (pass-by-reference) : ใน Python การที่การแก้ไขภายในฟังก์ชันส่งผลต่อค่าตัวแปรต้นฉบับหรือไม่ขึ้นอยู่กับลักษณะของอ็อบเจกต์
  • Immutable vs. mutable : อ็อบเจกต์ที่ไม่เปลี่ยนแปลงได้ (immutable) ใน Python จะไม่สะท้อนการเปลี่ยนแปลงที่ทำภายในฟังก์ชัน ในขณะที่อ็อบเจกต์ที่เปลี่ยนแปลงได้ (mutable) จะถูกแชร์ระหว่างภายในและภายนอกฟังก์ชันผ่านการส่งอ้างอิง
  • กลยุทธ์การบรรเทา : สำหรับอ็อบเจกต์ที่เปลี่ยนแปลงได้ วิธีการเช่นการใช้ deepcopy จากโมดูล copy หรือการตั้งค่าอาร์กิวเมนต์เริ่มต้นเป็น None มีประสิทธิภาพในการหลีกเลี่ยงพฤติกรรมที่ไม่คาดคิด

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