Python กับ None ร่างจริงที่ไม่ใช่ร่างทรง (ซะที่ไหน !)

เมื่อไม่กี่วันก่อนนั่งคิดขำ ๆ กับเพื่อน เรามาลองแกล้ง Python กันมั้ย เราสงสัยกันเรื่องของ None เป็นของที่เราใช้กันบ่อยมาก ๆ แต่เรามักจะมองว่า เออ มันก็เอาไว้แค่เป็นค่าส่งกลับเวลามันไม่มีของที่เราตามหาอะไรแบบนั้น ทำให้เราก็อาจจะเอามาใช้เป็น Flag ในการเช็คอีกทอดก็มี ทำให้มันเกิดเป็นบทความในวันนี้ เพราะเราจะบอกว่า None ที่ไม่มี จริง ๆ แล้วมันมีนะ

จริง ๆ None มีตัวตน

>>> type(None)
<class 'NoneType'>

หลาย ๆ คนอาจจะคิดว่า None มันเป็นแค่ Type พิเศษที่ไม่ได้มีตัวตนอะไร แต่เราจะบอกว่า จริง ๆ แล้ว None เนี่ย มันมีตัวตนจริง ๆ นะ เป็น Object ด้วย ถ้าไม่เชื่อลองรัน Code ด้านบนดู มันก็จะได้ออกมาบอกว่า None มันเป็น Object จาก NoneType Class ทำให้จริง ๆ แล้ว None มีตัวตนจริง ๆ เป็น Object ปกติที่เราสร้างนี่แหละ

>>> hex(id(None))
'0x104d18398'

หนักกว่านั้นอีก ถ้าเราลองดู Memory Address ของ None ที่เหมือนจะไม่มี มันมีอยู่ใน Memory จริง ๆ เพื่อการสังเกตที่ลึกเข้าไปอีก เราลองทำแบบด้านล่างดู

>>> a = type(None)()
>>> hex(id(a))
'0x104d18398'

เราลองสร้าง Object  a ให้เป็น NoneType ดู และเราลองเอา Memory Address ของ a ออกมาดู เราจะเห็นได้เลยว่า มันตรงกับ None เฉย ๆ เลย ซึ่งถ้าเราเข้าไปอ่านลึก ๆ เราจะทราบว่าจริง ๆ แล้ว None เป็น Singleton เลยนะ ทำให้เราสามารถใช้ None ในการเปรียบเทียบกับ None ได้ทั้งโปรแกรมเลย โดยที่มั่นใจได้เลยว่า None จะเท่ากับ None จริง ๆ ในทุก ๆ เคส เพราะเราเอาของชิ้นเดียวกันเป๊ะ มาเทียบกันเลย ไม่ว่า มันจะมี Alias เป็นอะไรก็ตาม ลองเข้าไปอ่านเพิ่มเติมได้ที่ Python Document

Check None ยังไงให้รอด

def checker (a : int) -> Union[None, int] :
    if a > 0 :
        return a

โดยทั่วไปแล้ว เมื่อเราสร้าง Function ขึ้นมา และ เราไม่ได้ทำการเรียก keyword return หรือ อาจจะเดินไปไม่ถึง ตัว Python มันก็จะใช้ Default เป็น None เลย ทำให้เราสามารถทำมันเป็น Flag ในการเช็คได้ว่าถ้ามันมีอะไรผิดพลาด แทนที่มันจะ return อะไรกลับมา ถ้ามันกลับมาเป็น None แปลว่าแตกก็ได้เหมือนกัน

>> checker(0)
None

จากตัวอย่าง เราเขียน Function ไว้เช็คแค่ว่าถ้า a มันมากกว่า 0 ให้มันเอา a กลับมาเลย แต่ถ้าไม่ใช่ละ เราไม่ได้ให้มันทำอะไรต่อแล้ว ถ้าเราคิดแบบนี้ มันไม่น่าจะเอาอะไรกลับมาเลย แต่ถ้าเราลองเอาตัวแปรมารับ หรือลองใน Interactive Shell เลย เราจะเห็นว่า จริง ๆ แล้วมันไม่ได้เป็นอย่างที่เราคิด มันได้กลับมาเป็น None เฉยเลย ทำให้สามารถทำอย่างที่เราบอกได้ว่า อาจจะใช้เป็น Flag ในการเช็คได้

class MyClass :
    def __eq__ (self, other) :
        return True

test_obj = MyClass()

print (test_obj == None)
print (test_obj is None)

ทีนี้ ปัญหามันจะเริ่มเกิดขึ้นเมื่อเราพยายามที่จะเปรียบเทียบ None เพราะคนทั่ว ๆ ไปคิดว่า None มันเป็นแค่ Keyword แต่จากที่เราคุยกันมา มันไม่ได้เป็นแบบนั้นเลย เราลองสร้าง Class และ Object ง่าย ๆ เลย โดยที่เรา Override Dunder Function eq ให้มันเอา True กลับไปเสมอ ดังนั้น ใน Print ด้านล่างควรจะเกิดอะไรขึ้น

True
False

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

สรุป

วันนี้เรามาเล่าเรื่องของ Object ตัวนึงที่เราเจอมันบ่อย ๆ แต่เราก็ไม่ได้คิดว่ามันเป็น Object ด้วยซ้ำ นั่นก็คือ None Object ที่ถูกสร้างมาจาก Class ที่ชื่อว่า NoneType นั่นเอง โดยมันมรสมบัติหลาย ๆ อย่างที่ช่วยทำให้เราเอามาใช้งานเป็นพวก Flag ในการเช็ค รวมไปถึงเป็น Default Value ในหลาย ๆ เวลา เช่น Return Function Value ถ้าเราไม่มี Return มันก็จะเอากลับไปเป็น None ให้เราเองเลย แต่เวลาใช้งานจริง ๆ แนะนำให้ระวังเรื่องของวิธีการเช็คให้ดี ๆ เพราะอาจจะเจอเคสแปลก ๆ ที่ทำให้หลุดได้ แนะนำให้ใช้ Identity Operator ในการเช็คโอกาสรอดจะเยอะกว่า