Theppitak's blog

My personal blog.

27 เมษายน 2559

From C++ to Python (1)

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

ในการเรียนครั้งนี้ ผมได้อาศัยบทเรียนจาก python-course.eu (และไปอ่านทบทวนกับ บทเรียนที่ debianclub ที่คุณวิทยาเคยลงไว้)

บทเรียนที่ python-course.eu นับว่าเหมาะกับคนที่มีพื้นฐาน C/C++ มาแล้วมาก เพราะเขาอธิบายบนพื้นฐานของคนเคยเขียนโปรแกรมมาแล้ว ไม่ใช่เริ่มต้นจากศูนย์ ยกตัวอย่างเปรียบเทียบกับ C/C++ และเน้นประเด็นที่เป็นหลุมพรางของผู้ที่ย้ายมาจาก C/C++

เท่าที่ได้ตั้งโจทย์ฝึกหัดให้กับตัวเอง ด้วยการทดลองพอร์ตโค้ดภาษา C/C++ ของตัวเองที่เคยเขียนไว้ให้เป็นไพธอน พร้อมกับทำ unit test ไปด้วย ทำให้ได้พบเจอประเด็นต่าง ๆ ที่โปรแกรมเมอร์ภาษา C/C++ จะสะดุด ผมเองก็พบว่าต้องก้าวข้ามสิ่งเหล่านี้ถึงจะเริ่มจับทางไพธอนได้ จึงเขียนบันทึกไว้สักหน่อย

Indentation

ข้อนี้ดูเหมือนเป็นเรื่องเล็ก แต่กลับเป็นสาเหตุหลักข้อหนึ่งที่ทำให้ผมยี้ไพธอนก่อนหน้านี้ เพราะโปรแกรมเมอร์ภาษา C/C++ จะคุ้นกับ free-form syntax และพบเจอการ indent แบบตามใจฉันมาแล้วมากมาย บางคนใช้ tab บางคนใช้ space 2, 4 หรือ 8 ช่อง บางคนใช้ปนกันทั้ง tab ทั้ง space ซึ่งซอร์สจะเละทันทีเมื่อมีการเปลี่ยนขนาดของ tab stop นี่ยังไม่นับคนที่ไม่สนใจ style ใด ๆ ทั้งสิ้นอีกนะ แต่ไม่ว่าอย่างไรโปรแกรมก็ยังคอมไพล์ผ่าน แล้วถ้ามันกลายเป็นการกำหนด syntax ของภาษา มันจะเละเทะขนาดไหน

เมื่อพยายามนึกถึงตัวอย่างอื่นที่กำหนดอะไรทำนองนี้ ผมก็นึกถึง Makefile ที่ใช้ tab ในการกำหนดขอบเขตของ rule แต่มันก็แค่ระดับเดียว ไม่ได้ซ้อนกันหลายชั้น จึงดูไม่มีอะไรมาก และอีกตัวอย่างหนึ่งคือ Fortran 77 ที่เคยเขียนในสมัยเรียน ซึ่งอาศัยตำแหน่งคอลัมน์เป็นส่วนหนึ่งของ syntax อันเป็นมรดกตกทอดมาจากสมัยใช้บัตรเจาะรู (ตอนที่เรียน Fortran 77 รู้สึกอึดอัดตรงนี้มาก เพราะตอนนั้นผ่านภาษา Pascal มาแล้ว แม้แต่ภาษา BASIC [AppleSoft, GW] ที่ว่าไม่ค่อยมีโครงสร้างก็ยังไม่ทำอะไรโลว์เทคเยี่ยงนี้) แต่ละอย่างที่นึกถึงก็ไม่ได้ชวนพิสมัยเอาเสียเลย

นั่นคือ mindset ของผมก่อนเรียน แต่ คำอธิบาย ในบทเรียนก็ทำให้ผมเข้าใจและยอมรับ ทำให้เรียนไพธอนต่อไปได้อย่างผ่อนคลาย มันคือการกำหนดขอบเขตของบล็อคคำสั่งโดยเปลี่ยนจาก style ให้เป็น syntax เสีย คนที่เขียนโปรแกรมภาษา C/C++ โดยเคร่งครัดกับ style อยู่แล้วจึงยอมรับตรงนี้ได้ไม่ยาก ส่วนประเด็นเรื่องการใช้ tab กับ space ปนกัน ไพธอนก็มีข้อกำหนดที่ชัดเจนที่ทำให้แยกแยะออกจากกันได้ คือถือเป็น indentation คนละระดับไปเลย ไม่นับปนกัน

มันคือการกำจัดวิจิกิจฉานิวรณ์ครับ ผ่านตรงนี้ไปได้ก็ลื่นไหลขึ้นเยอะ

Public, Protected, Private

ภาษา C++ จะมีการกำหนด access specifier สำหรับ member ต่าง ๆ ของคลาสเป็น public, protected, private จากนั้น โปรแกรมเมอร์แต่ละคนก็จะมีวิธีการต่าง ๆ ในการตั้งชื่อ member ให้สามารถแยกแยะ access ได้ บางคนใช้ตัวพิมพ์เล็ก-พิมพ์ใหญ่ต้นชื่อ บางคนใช้ prefix บางคนใช้ suffix บางคนไม่แยกเลย (อันนี้ยุ่ง)

แต่ไพธอนไม่มี access specifier แต่จะใช้การตั้งชื่อแยกแยะทันที โดยชื่อปกติจะถือเป็น public ชื่อที่ขึ้นต้นด้วย _ ถือเป็น protected และชื่อที่ขึ้นต้นด้วย __ ถือเป็น private นับว่าเป็นการเปลี่ยน style ให้เป็น syntax อีกหนึ่งเรื่อง

Dynamic Type & Scope

การย้ายจากภาษาที่เป็น static typing มาเป็น dynamic typing จะต้องปรับโหมดความคิดสักหน่อย ซึ่งพื้นฐานภาษา BASIC สมัยเก่า บวกกับความรู้จากวิชา Compilers & Programming Languages พอช่วยได้ ไม่เป็นปัญหาสำหรับผมนัก สิ่งที่ต้องปรับเปลี่ยนแนวคิดก็เช่น:

  • ชนิดของตัวแปรเป็น dynamic ไม่ใช่ตายตัวเปลี่ยนชนิดไม่ได้เหมือนในภาษา static type ทั้งหลาย ถึงตัวแปรหนึ่งจะเก็บค่า integer อยู่ คุณจะ assign สตริงหรือลิสต์ให้มันใหม่ก็ยังได้
    >>> x = 1
    >>> print(x)
    1
    >>> x = "Hello!"
    >>> print(x)
    Hello!
    
  • scope ของตัวแปรเป็นแบบ dynamic ฟังก์ชันหนึ่ง ๆ ทำงานแต่ละครั้งอาจเข้าถึงตัวแปรได้ไม่เหมือนกัน ขึ้นอยู่กับ state ของทั้งโปรแกรมในขณะนั้น
    >>> def f():
    ...   print(s)
    ... 
    >>> s = "Hello"
    >>> f()
    Hello
    >>> s = 2
    >>> f()
    2
    
    แต่ dynamic scope นี้ก็ถูกขี่ทับด้วย block scope ได้ ถ้ามีการกำหนดตัวแปรโลคอลในชื่อเดียวกัน
    >>> def g():
    ...   s = "Bye"
    ...   print(s)
    ... 
    >>> s = "Hello"
    >>> g()
    Bye
    >>> print(s)
    Hello
    
    อ่านเพิ่มเติม

Call by Value หรือ Call by Reference?

ภาษาไพธอนไม่มีพอยน์เตอร์เหมือน C/C++ แต่กลไกภายในกลับใช้พอยน์เตอร์เต็มไปหมด แม้แต่ตัวแปรกับค่าของมันก็เชื่อมกันด้วยพอยน์เตอร์ การ assign ตัวแปรก็เป็นการ assign พอยน์เตอร์ไปยังออบเจกต์ที่เป็นค่า ดังสามารถตรวจสอบได้ดังนี้:

>>> x = 1
>>> y = x
>>> id(x)
10861696
>>> id(y)
10861696

จะเห็นว่า หลัง assignment แล้ว x กับ y ชี้ไปยังออบเจกต์เดียวกัน ไม่ใช่แค่มีค่าเท่ากัน! อย่างไรก็ดี ถ้าเรา assign ค่าใหม่ให้กับ y จะไม่ได้ทำให้ค่าของ x เปลี่ยน แต่เป็นการกำหนดให้ y ชี้ไปยังออบเจกต์ใหม่:

>>> y = 2
>>> id(y)
10861728
>>> x
1

ก็น่าจะปลอดภัยดี แถมยังเป็นการประหยัดหน่วยความจำด้วยถ้าค่าเป็นออบเจกต์ใหญ่ ๆ ที่ไม่ใช่ integer เรื่องประหยัดน่ะใช่ แต่แน่ใจหรือเรื่องความปลอดภัย?

>>> p = [1, 2, 3]
>>> q = p
>>> q[1] = 'x'
>>> p
[1, 'x', 3]

จะเห็นว่าลิสต์ p เปลี่ยนตาม q หลัง assignment ทั้งนี้เพราะทั้ง p และ q ชี้ไปยังออบเจกต์เดียวกันตลอดเวลา และการ assign q[1] ก็เป็นการเปลี่ยนพอยน์เตอร์ q[1] จากที่ชี้ไปยัง integer 2 ให้ชี้ไปยัง string 'x' แทน แต่ในเมื่อ q ชี้ไปยังออบเจกต์เดียวกันกับ p การเปลี่ยนพอยน์เตอร์ q[1] จึงเป็นการเปลี่ยนพอยน์เตอร์ p[1] ด้วย!

พฤติกรรมแบบนี้ เกิดกับออบเจกต์ที่เป็น complex data structure ทั้งหมด ไม่ว่าจะเป็น list, dictionary หรือ class instance ซึ่งหากอธิบายในภาษาของ C/C++ ด้วยคำว่า พอยน์เตอร์ (จากชื่อตัวแปรไปยังค่า) ก็จะสามารถทำความเข้าใจได้ รวมถึงประโยชน์ของ deepcopy ด้วย

ความสนุกเกิดขึ้นเมื่อเราส่งอาร์กิวเมนต์ให้กับฟังก์ชัน

แบบนี้พฤติกรรมจะเหมือน call by value:

>>> def f(x):
...   x = x + [1]
... 
>>> p = [1, 2, 3]
>>> f(p)
>>> p
[1, 2, 3]

แต่แบบนี้กลับเหมือน call by reference:

>>> def g(x):
...   x += [1]
... 
>>> p = [1, 2, 3]
>>> g(p)
>>> p
[1, 2, 3, 1]

คำอธิบายก็คือ x ซึ่งเป็นพารามิเตอร์ของ f() และ g() นั้น จะก็อปปี้พอยน์เตอร์ไปยังค่าของ p มาทั้งคู่ จากนั้น x ใน f() ถูก assign ให้ชี้ไปยังออบเจกต์ใหม่ที่เป็นผลลัพธ์ของการบวก x กับลิสต์ [1] ในขณะที่ x ใน g() ถูกเพิ่มอิลิเมนต์ใหม่ต่อท้ายโดยตรงในออบเจกต์เดิม ซึ่งเป็นออบเจกต์เดียวกับที่ p ของผู้เรียกชี้อยู่ ผลจึงเป็นการเปลี่ยนค่าของ p ของผู้เรียกไปด้วย

คำถามที่ว่า การเรียกฟังก์ชันของไพธอนเป็น call by value หรือ call by reference จึงตอบได้แค่ว่า ไม่ใช่ทั้งสองอย่าง ผู้ที่ย้ายมากจากภาษา C/C++ ต้องระวังให้ดี ภาษาไพธอนไม่มีพอยน์เตอร์ก็จริง แต่เวลาเขียนไพธอนให้นึกถึงพอยน์เตอร์เข้าไว้

Function Overloading?

ฟังก์ชันในภาษา C++ สามารถโอเวอร์โหลดเพื่อรับพารามิเตอร์หลายแบบได้ ถึงแม้มันจะเพิ่มความยุ่งยากในการลิงก์จากโค้ดภายนอกอันเนื่องมาจาก symbol mangling แต่มันก็ไม่ได้สร้างขึ้นมาเท่ ๆ มี use case ที่ได้ประโยชน์จากการโอเวอร์โหลดฟังก์ชัน เช่น:

  • ช่วยให้สร้าง constructor หลายแบบได้
  • ช่วยในการ overload operator เพื่อรับ operand หลายชนิดได้

แต่ไพธอนไม่สามารถโอเวอร์โหลดฟังก์ชันได้ มีให้อย่างมากก็แค่การใช้ default argument แต่ถ้ากำหนดฟังก์ชันชื่อเดียวกันหลายครั้ง มันก็จะเอาอันหลังสุดเท่านั้น ถือว่าทับอันแรก ๆ ไป

ด้วยความเป็น dynamic typing ของภาษาไพธอน ทำให้พอเข้าใจได้ว่าการโอเวอร์โหลดฟังก์ชันสามารถทำให้เกิดความยุ่งยากได้ กรณีทั่วไปเราอาจเลี่ยงได้ด้วยการตั้งชื่อฟังก์ชันหลบกันเสีย แต่สำหรับกรณีของ magic methods ทั้งหลายของคลาส เราไม่สามารถตั้งชื่อหลบได้ ซึ่งกรณีของ constructor และ operator overloading ก็เข้าข่าย magic methods ทั้งสิ้น

แล้วจะจัดการกับ use case ข้างต้นได้อย่างไร?

การโอเวอร์โหลด constructor

กรณีที่สามารถใช้ default argument ได้ ก็ใช้ default argument เช่น:

class Time:
  def __init__ (self, h=0, m=0, s=0):
    self.h, self.m, self.s = h, m, s

t1 = Time()
t2 = Time(8)
t3 = Time(8, 20)
t4 = Time(8, 20, 45)

หากโอเวอร์โหลดโดยใช้อาร์กิวเมนต์ต่างชนิดกัน ก็ไม่สามารถใช้ default argument ได้ ก็อาจใช้วิธีพิเศษ เช่น การใช้ argument tuple หรือ argument dict:

class Time:
  def __init__ (self, *args):
    if len (args) == 1 and type (args[0]) == str:
      h, m, s = args[0].split(":")
      self.h, self.m, self.s = int(h), int(m), int(s)
    else:
      self.h, self.m, self.s = 0, 0, 0
      if len (args) >= 1:
        self.h = int(args[0])
      if len (args) >= 2:
        self.m = int(args[1])
      if len (args) >= 3:
        self.s = int(args[2])

t1 = Time()
t2 = Time(8)
t3 = Time(8, 20)
t4 = Time(8, 20, 45)
t5 = Time("8:20:50")

หรือใช้ factory method ซะเลย:

class Time:
  @classmethod
  def from_str (cls, s):
    h, m, s = s.split(":")
    return cls (int(h), int(m), int(s))

  @classmethod
  def from_hms (cls, h=0, m=0, s=0):
    return cls (h, m, s)

  def __init__ (self, h, m, s):
    self.h, self.m, self.s = h, m, s

t1 = Time.from_hms()
t2 = Time.from_hms(8)
t3 = Time.from_hms(8, 20)
t4 = Time.from_hms(8, 20, 45)
t5 = Time.from_str("8:20:50")

(ดัดแปลงจาก กระทู้ Stack Overflow)

การโอเวอร์โหลด operator

สมมุติว่าเรามีคลาส Date (วันที่) ซึ่งต้องการโอเวอร์โหลดเครื่องหมายลบดังนี้:

d1 = Date("2016-04-26")
d2 = Date("2016-05-03")
assert d2 - d1 == 7
assert d2 - 7 == d1

กล่าวคือ:

  • ถ้าตัวลบเป็นชนิด Date ให้หาจำนวนวันระหว่างวันที่ทั้งสอง
  • ถ้าตัวลบเป็นชนิด int ให้หาวันที่ถอยหลังเป็นจำนวนวันที่ลบ

กรณีนี้ดูจะไม่มีทางอื่น นอกจากตรวจสอบชนิดของตัวลบเอา:

class Date:
  # ...

  def __sub__ (self, other):
    if type (other) == int:
      # ... subtract days from self ...
    elif type (other) == Date:
      # ... subtract two Dates ...
    else:
      raise TypeError ("Invalid operand type")

ซึ่งออกจะดูอัปลักษณ์ถ้าเป็นโค้ด C++ แต่นี่คือไพธอนมาตรฐาน

อย่างไรก็ดี เท่าที่ลองค้นดู ดูเหมือนจะมีแพกเกจ overload และ multimethod เพื่อการนี้ และดูจะมี PEP 3124 เพื่อเพิ่มการรองรับการโอเวอร์โหลดฟังก์ชันอย่างเป็นทางการ แต่ยังอยู่ในสถานะ Deferred อยู่

Polymorphism?

หนึ่งในเครื่องมือที่ใช้กันมากของ OOP ก้คือ polymorphism ซึ่งในภาษา C++ จะใช้ virtual function ประกอบกันกับ class hierarchy ซึ่ง implementation ภายในคือ pointer to function ใน v-table ของคลาส

แต่พอมาเขียนไพธอน คุณจะหา keyword หรือ naming convention ที่เทียบเคียงกับ virtual function ไม่ได้เลย ทั้งนี้เพราะไพธอนเป็น polymorphic โดยธรรมชาติอยู่แล้ว!

เวลาที่เขียนฟังก์ชันแบบนี้ใน C++:

inline int max (int x, int y) { return (x > y) ? x : y; }
inline int max (double x, double y) { return (x > y) ? x : y; }

หรือจะใช้ generic programming ด้วย template ซึ่ง implementation ภายในเทียบเท่ากัน แต่ใช้กับชนิดใดก็ได้ที่รองรับ operator > :

template <class T>
inline int max (T x, T y) { return (x > y) ? x : y; }

แต่ในไพธอนคุณเขียนแค่นี้ก็ทำงานได้กับทุกชนิดแล้ว:

def max (x, y):
  return x if x > y else y

เพราะ dynamic typing นั่นเอง ทำให้สามารถส่งออบเจกต์ชนิดไหนก็ได้ แล้วไปว่ากันที่ run-time ถ้าชนิดนั้น ๆ รองรับ operation ที่เรียกใช้ ก็เป็นอันใช้ได้ ตามแนวคิดที่เรียกว่า duck-typing ซึ่งกล่าวว่า If it looks like a duck and quacks like a duck, it must be a duck.

คุณจึงสามารถใช้ polymorphism ได้ ไม่ว่าจะเขียนโค้ดแบบนี้:

class Animal:
  def cry (self):
    pass

class Cat (Animal):
  def cry (self):
    print ("Meow!")

class Duck (Animal):
  def cry (self):
    print ("Quack!")

farm = []
farm.append (Cat())
farm.append (Duck())

for i in farm:
  i.cry()

หรือแบบนี้:

class Cat:
  def cry (self):
    print ("Meow!")

class Duck:
  def cry (self):
    print ("Quack!")

farm = []
farm.append (Cat())
farm.append (Duck())

for i in farm:
  i.cry()

คงพอเห็นภาพนะครับ

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

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

ป้ายกำกับ: , ,

4 ความเห็น:

  • 27 เมษายน 2559 เวลา 21:44 , Blogger Wiennat แถลง…



    ถ้าผมเข้าใจไม่ผิด วิธีการ pass ค่าของพารามิเตอร์ใน python จะคือ pass by value เสมอครับ แค่ว่ากรณีที่เป็นตัวแปรอย่าง list, dict นี่ ผมใช้วิธีจำว่าค่าที่ตัวแปรนั่นเก็บอยู่คือ reference หรือ pointer พอส่งไปเป็นพารามิเตอร์มันก็เลยส่งค่าตัวมัน ซึ่งก็คือ pointer ไปแทน ทีนี้พอแก้ไขอะไรมันก็เลยมีผลกับของเดิมด้วย ต่างกับที่เป็น primitive type ซึ่งค่าในตัวมันเป็นค่าจริงๆ

     
  • 28 เมษายน 2559 เวลา 12:40 , Blogger Thep แถลง…

    ถ้าจะว่าในระดับลึกจริง ๆ มัน call by reference เสมอครับ ลองดูก็ได้:

    >>> def f(x):
    ... print(id(x))
    ... x = 100
    ... print(id(x))
    ...
    >>> a = 1
    >>> id(a)
    10861696
    >>> f(a)
    10861696
    10864864
    >>> id(a)
    10861696

    จะเห็นว่า id(x) == id(a) ในตอนแรกที่เข้าสู่ฟังก์ชัน แสดงว่าตอนที่เรียก มันส่ง reference กัน แต่พอเรา assign x เป็น 100 จึงเป็นการเปลี่ยน x ให้ reference ไปยังออบเจกต์อื่น

    (ใน python ทุกอย่างจะเป็นออบเจกต์ไปหมด ไม่เว้นแม้แต่ primitive type คิด ๆ ดูแล้ว ก็เป็นวิธีที่เป็นธรรมชาติที่สุดในการ implement dynamic type เหมือนกันนะครับ)

    แต่อย่างไรก็ดี พฤติกรรมของฟังก์ชันออกมาเหมือนเป็น call by value เพราะการ assign ค่า x ในฟังก์ชัน ไม่มีผลต่อ a ของผู้เรียก

    แต่ถ้าจะบอกว่ามันเป็น call by value เฉพาะ primitive type และเป็น call by reference กับ complex type ตัวอย่างใน blog ก็แสดงให้เห็นแล้วว่ามันเป็นได้ทั้งสองอย่างกับ complex type เพราะถ้าเราต้องการ side effect เป็นการเพิ่มสมาชิกในลิสต์จริง ๆ (คือเขียนโดยคาดหวัง call by reference) การใช้ x = x + [1] จะไม่ให้ผลตามต้องการ (พฤติกรรมกลายเป็นเหมือน call by value) ต้องเขียนเป็น x += [1] ถึงจะได้เหมือน call by reference

    ในแง่กลไกภายใน มัน call by reference เสมอ แต่ในแง่ concept การเขียนโปรแกรมแล้ว เราจะยึดตายตัวว่าเป็นแบบใดแบบหนึ่งไม่ได้ แต่ควรมุ่งไปที่การเชื่อมโยงระหว่าง name-value มากกว่าครับ

     
  • 28 เมษายน 2559 เวลา 13:01 , Blogger Thep แถลง…

    พูดถึง primitive มีอะไรสนุก ๆ อยู่ครับ:

    >>> a = 1
    >>> id(a)
    10861696
    >>> b = 1
    >>> id(b)
    10861696
    >>> id(1)
    10861696

    คือค่า integer 1 จะอยู่ในออบเจกต์เดียวกันเสมอ การ assign b = 1 ก็กลายเป็นการเชื่อมโยง b กับออบเจกต์ int(1) ซึ่งเป็นออบเจกต์เดียวกับที่ a เชื่อมโยงอยู่ แล้วถ้าเราเพิ่มค่า a ล่ะ?

    >>> a = 1
    >>> id(a)
    10861696
    >>> a += 1
    >>> a
    2
    >>> id(a)
    10861728
    >>> id(2)
    10861728

    การเพิ่มค่า a กลายเป็นการเปลี่ยนพอยน์เตอร์ให้ a ชี้ไปยังออบเจกต์ int(2) !!!

    เพราะฉะนั้น ถ้าคุณพยายามจะทำอะไรกับ primitive type แบบเดียวกับที่ผมทำกับลิสต์ โดยคาดหวังว่าจะเจอ call by reference บ้าง เช่น:

    >>> def f(x):
    ... print(id(x))
    ... x += 1
    ... print(id(x))
    ...
    >>> a = 1
    >>> id(a)
    10861696
    >>> f(a)
    10861696
    10861728
    >>> a
    1
    >>> id(a)
    10861696

    คุณก็คงเดาได้นะครับ ว่ามันเกิดอะไรขึ้นภายใน... (x += 1 มีผลเป็นการเปลี่ยนพอยน์เตอร์ให้ x ชี้ไปยังค่า x + 1 ทำให้ x += 1 มีผลเทียบเท่า x = x + 1 สำหรับ primitive type และทำให้ผลที่ได้เหมือน call by value ทั้ง ๆ ที่ id(x) ตอนเข้าฟังก์ชันมันฟ้องว่าเป็น call by reference)

     
  • 5 มิถุนายน 2559 เวลา 02:32 , Blogger Unknown แถลง…

    จริง ๆ ประเด็นเรื่อง Python นี่ call by value หรือ reference ถ้ามองผ่านกรอบ type ว่าเป็น mutable หรือ immutable อาจจะช่วยให้เข้าใจในแบบ Pythonic เพิ่มด้วยนิดนึงครับ
    ในกรณีที่เป็น immutable(อย่างเช่น integer) เพราะความที่ memory block นั้นถูก flag ไว้ไม่ให้ mutate การ assign หรือคำนวณเปลี่ยนแปลงใด ๆ จึงคือการ allocate บล็อคใหม่ ในขณะที่ตัวแปรนอก scope ยังชี้ไปที่บล็อคเดิม ส่วนข้อมูลที่เป็น mutable(อย่างเช่น list) การเปลี่ยนแปลงใด ๆ จึงคือการอ้างอิง/เปลี่ยนแปลง/บันทึกค่าไปที่บล็อคเดิม

    ทีนี้จากตัวอย่างที่เขียน ทำไม x = กับ x =+ ถึงให้ผลไม่เหมือนกัน? เหตุผลก็คือเวลาเราเขียน x = มันคือการ assign ค่า/บล็อคใหม่ ให้ตัวแปร ในขณะที่ x += มันคือการ invoke ตัว magic method __iadd__ คือถึงนัยยะแบบที่มนุษย์เราเข้าใจจะคล้ายกัน แต่ implement ต่างกัน ในบางเงื่อนไขเลยให้ผลต่างกัน ตรงนี้ไม่ได้เป็นแค่กับการ pass ค่า เป็นในทุกระดับ เช็คที่ top-level ก็แบบนี้ เช่นถ้า

    x = []
    id(x)
    x = x + [1]
    id(x)

    จะเห็นว่ามันกลายเป็นอ้างอิงคนละบล็อคกัน แต่ถ้า

    x = []
    id(x)
    x += [1]
    id(x)

    จะเห็นว่ายังอ้างอิงบล็อคเดียวกัน

    ส่วนประเด็นเรื่อง a = 1; b = 1 แล้วเช็ค id ตรงกัน ก็คือตัว Python interpreter เริ่มต้นมันจะจอง memory block ไว้สำหรับตัวเลขตั้งแต่ -5 ถึง 256 เอาไว้เลยแล้วเวียนใช้ ตัวแปรไหนที่เก็บค่าใน range นี้ ก็ให้ pointer ชี้ไปที่บล็อคที่จองไว้แล้ว การเปลี่ยนแปลงค่าในบล็อคไม่มี เพราะ integer เป็น immutable ตรงนี้ก็เช็คเหมือนกัน a = 1; b= 1 เช็ค id จะตรงกัน แต่ถ้า a = 257; b = 257 ตรงนี้จะคนละ id แล้ว เป็นการจองบล็อคใหม่

     

แสดงความเห็น (มีการกลั่นกรองสำหรับ blog ที่เก่ากว่า 14 วัน)

<< กลับหน้าแรก

hacker emblem