เห็นโพสต์นึงในอินเทอร์เน็ตพูดถึงการใช้ตัวดำเนินการ ==
และ is
ในไพทอนแล้วน่าจะมีเนื้อหาที่ไม่ค่อยถูกนัก
มิตรสหายเนยสดท่านหนึ่ง ใจดีไปแก้ไขที่ต้นทางให้ (link open in new tab) มองว่าผู้เขียนน่าจะรู้สึกเหมือน is
เป็น ===
ใน JS กล่าวคือเป็น type strict comparison ซึ่งไม่ใช่ในกรณีของไพทอนอย่างแน่นอน เพราะตัวภาษาเองเป็น strict typed อยู่แล้ว
เลยแวบมาเขียนอะไรขำๆ สักเล็กน้อย เกี่ยวกับการใช้ ==
และ is
สักหน่อยแล้วกัน
Prerequisites
ก่อนอื่นต้องเข้าใจว่า ==
มีไว้เช็กความค่าเท่ากัน และ is
มีไว้เช็กว่า object สองตัวเป็น instance เดียวกันไหม
>>> a = []
>>> b = []
>>> a == b
True
>>> a is b
False
is None
คำถามที่อาจจะน่าสนใจ คือทำไมเวลาเราเช็กว่าวัตถุใดๆ ในไพทอนเป็น None
หรือไม่ เราเลือกที่จะใช้ is None
แทน == None
?
แน่นอนว่าพอเขียนเป็นโค้ดแล้ว ให้ความรู้สึกเหมือนกำลังเขียนภาษาธรรมชาติมาก แต่การเลือกใช้ is None
เป็นเพียง syntactic sugar หรือเปล่า?, คำตอบคือไม่ใช่
เหตุที่เราเลือกใช้ is None
นั่นก็เพราะ None
ในไพทอนเป็น singleton object กล่าวคือจะมีได้เพียง instance เดียว ทำให้การเช็กว่า object ใดๆ เป็น None หรือไม่ ทำได้ด้วยการเช็กว่าเป็น instance เดียวกันไหมนั่นเอง!
True
, and False
กรณีศึกษาหนึ่งที่น่าสนใจคือ True
และ False
ในไพทอน ซึ่งเป็น object ของ bool
True และ False คล้ายกับ None ในแง่ที่ว่าในโปรแกรมไพทอนหนึ่ง จะมี instance ของ True เพียง instance เดียว และ instance ของ False เพียง instance เดียว
ถ้าแบบนั้น แปลว่า True และ False ต่างเป็น singleton หรือเปล่า? คำตอบคือไม่ใช่ เพราะทั้งสองเป็น instance ของ boolean เหมือนกัน (ซึ่งแปลว่าไม่ได้มี boolean instance เพียง instance เดียวในโปรแกรมไพทอนหนึ่งโปรแกรม) ในทางเทคนิกเราเรียก design pattern ในลักษณะนี้ว่า Flyweight pattern
อย่างไรก็ดี เราก็พอรู้แล้วว่าในทางทฤษฎี เราสามารถใช้ is
ในการเช็กว่า boolean ค่าใดค่าหนึ่งเป็น True
หรือ False
หรือไม่
>>> a = (0 == 1)
>>> id(a)
139976720486240
>>> id(False)
139976720486240
หรือง่ายกว่านั้น
>>> 0 == 1 is False
<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
False
อ้าว… โกหกกันนี่หว่า…
ไม่ใช่! pitfall หนึ่งของไพทอน คือ orders of operation ที่ทำ is
ก่อนทำ comparison ซึ่งบางครั้งการลืม orders of operations ก็อาจจะทำให้เราเห็นอะไรแปลกๆ แบบนี้
>>> True == not False
File "<stdin>", line 1
True == not False
^^^
SyntaxError: invalid syntax
มาลองกันใหม่ แบบใส่วงเล็บ
>>> (0 == 1) is False
True
ผลลัพธ์เป็นไปตามคาด
แต่แน่นอน อย่าหาทำในชีวิตจริง
Glitch in the Matrix?!?!
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
?!?!?!
คำตอบอยู่ใน C API implementation ของ Python (ซึ่งเกร็ดความรู้คือ treat int
กับ long
ด้วย PyLong
object เหมือนกัน)
Integer Objects — Python 3.12.1 documentation กล่าวไว้ว่า
The current implementation keeps an array of integer objects for all integers between -5 and 256. When you create an int in that range you actually just get back a reference to the existing object.
Mimicing NULLs
ใน SQL ค่าของ NULLs ไม่เท่ากับค่าของ NULLs อื่นๆ
เพื่อเป็นการสาธิตความแตกต่างของ ==
และ is
อย่างชัดเจนถึงแก่น เราสามารถสร้างคลาส NullType
ที่สร้าง singleton object ของ NULL ได้ แต่เมื่อเอา instance ของ NullType
“สองตัว” (เดียวกัน) มาเทียบกัน จะได้ผลลัพธ์เป็น False
การสร้าง singleton ในไพทอนทำได้ง่าย ด้วยการ override magic method __new__
หมายเหตุ: class_
ด้านล่าง เกิดจากการเติม underscore ข้างท้ายชื่อ argument ไม่ให้ชนกับคำว่า class
ที่เป็น “คำสงวน”, ในบางตัวอย่าง เช่นในอ้างอิงและอ่านเพิ่มเติม อาจจะเลือกใช้ cls
ก็ได้
# null.py
class NullType:
_instance = None
def __new__(class_):
if class_._instance is None:
class_._instance = super().new__(class_)
return class_._instance
แปลว่าตอนนี้ เราสามารถสร้าง null object ให้มีเพียง instance เดียวได้แล้ว
$ python3 -i null.py
>>> null_1 = NullType()
>>> null_2 = NullType()
>>> null_1 is null_2
True
ถ้าไม่ต้องการให้ null object ของเราเท่ากับอะไรเลย ก็แค่ override __eq__
method ที่จะถูกเรียกเวลาทำ comparison
# null.py
class NullType:
_instance = None
def __new__(class_):
if class_._instance is None:
class_._instance = super().__new__(class_)
return class_._instance
def __eq__(self, other):
return False
และลองรันใหม่
$ python3 -i null.py
>>> null_1 = NullType()
>>> null_2 = NullType()
>>> null_1 == null_2
False
>>> null_1 is null_2
True
ก็จะได้ผลลัพธ์ตามที่ต้องการ
อ้างอิง และอ่านเพิ่มเติม
- ได้รู้จักการ allocate เลข -5 ถึง 256 รอล่วงหน้า กับ
True == not False
จาก satwikkansal/wtfpython: What the f*ck Python? 😱 (github.com) - ทั้ง Singleton และ Flyweight เป็น design patterns เดิมของ GoF แต่ว่าถ้าอยากอ่านถึง implementation wise aspects บนไพทอน สามารถหาอ่านได้ที่ The Singleton Pattern (python-patterns.guide) และ The Flyweight Pattern (python-patterns.guide)
Leave a Reply