Daily Rant: Python Comparisons

เห็นโพสต์นึงในอินเทอร์เน็ตพูดถึงการใช้ตัวดำเนินการ == และ 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

ก็จะได้ผลลัพธ์ตามที่ต้องการ

อ้างอิง และอ่านเพิ่มเติม

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *