ฉันควรใช้ __ne__ เป็นการปฏิเสธ __eq__ ใน Python หรือไม่


102

ฉันมีคลาสที่ฉันต้องการจะลบล้าง__eq__เมธอด ดูเหมือนว่าฉันควรจะลบล้าง__ne__เมธอดนี้ด้วย ฉันควรใช้__ne__เป็นการปฏิเสธ__eq__เช่นนี้หรือเป็นความคิดที่ไม่ดี?

class A:

    def __init__(self, state):
        self.state = state

    def __eq__(self, other):
        return self.state == other.state

    def __ne__(self, other):
        return not self.__eq__(other)

คำตอบ:


61

ใช่มันสมบูรณ์ดี ในความเป็นจริงเอกสารขอให้คุณกำหนด__ne__เมื่อคุณกำหนด__eq__:

ไม่มีความสัมพันธ์โดยนัยระหว่างตัวดำเนินการเปรียบเทียบ ความจริงของx==yไม่ได้หมายความว่าx!=y เป็นเท็จ ดังนั้นเมื่อกำหนด __eq__()ควรกำหนดด้วย__ne__()เพื่อให้ตัวดำเนินการทำงานตามที่คาดไว้

ในหลาย ๆ กรณี (เช่นกรณีนี้) มันจะง่ายพอ ๆ กับการปฏิเสธผลลัพธ์ของ__eq__แต่ไม่เสมอไป


13
นี่คือคำตอบที่ถูกต้อง (ลงที่นี่โดย @ aaron-hall) เอกสารที่คุณยกมาไม่สนับสนุนให้คุณนำไปใช้__ne__โดยใช้__eq__เพียงอย่างเดียวที่คุณนำไปใช้
Guyarad

2
@guyarad: อันที่จริงคำตอบของแอรอนยังผิดอยู่เล็กน้อยเนื่องจากไม่ได้มอบหมายอย่างถูกต้อง แทนที่จะถือว่าNotImplementedผลตอบแทนจากอีกด้านหนึ่งเป็นตัวชี้นำในการมอบหมายไป__ne__ยังอีกด้านหนึ่งnot self == otherคือ (สมมติว่าตัวถูกดำเนินการ__eq__ไม่ทราบวิธีเปรียบเทียบตัวถูกดำเนินการอื่น) โดยปริยายมอบหมาย__eq__จากอีกด้านหนึ่งจากนั้นจึงสลับกลับ ชนิดแปลก ๆ เช่นทุ่ง SQLAlchemy ออมของนี้ทำให้เกิดปัญหา
ShadowRanger

1
คำวิจารณ์ของ ShadowRanger จะใช้กับกรณีทางพยาธิวิทยาเท่านั้น (IMHO) และได้รับการกล่าวถึงอย่างเต็มที่ในคำตอบของฉันด้านล่าง
Aaron Hall

3
เอกสารที่ใหม่กว่า (อย่างน้อย 3.7 อาจเป็นรุ่นก่อนหน้า) จะ__ne__มอบหมายให้โดยอัตโนมัติ__eq__และคำพูดในคำตอบนี้ไม่มีอยู่ในเอกสารอีกต่อไป บรรทัดล่างเป็น pythonic ที่สมบูรณ์แบบที่จะใช้งาน__eq__และให้__ne__ผู้ร่วมประชุมเท่านั้น
bluesummers

138

Python ฉันควรใช้__ne__()ตัวดำเนินการตาม__eq__?

คำตอบสั้น ๆ : อย่าใช้มัน แต่ถ้าคุณต้องใช้==ไม่ใช่__eq__

ใน Python 3 !=เป็นการปฏิเสธ==โดยค่าเริ่มต้นดังนั้นคุณจึงไม่จำเป็นต้องเขียน a ด้วยซ้ำ__ne__และเอกสารประกอบจะไม่มีความเห็นเกี่ยวกับการเขียนอีกต่อไป

โดยทั่วไปสำหรับโค้ด Python 3 เท่านั้นอย่าเขียนโค้ดเว้นแต่คุณจะต้องบดบังการใช้งานพาเรนต์เช่นสำหรับอ็อบเจ็กต์ในตัว

นั่นคือโปรดทราบความคิดเห็นของ Raymond Hettinger :

__ne__วิธีการดังต่อไปนี้โดยอัตโนมัติจาก__eq__เฉพาะในกรณีที่ __ne__ไม่ได้กำหนดไว้แล้วใน superclass ดังนั้นหากคุณได้รับมรดกจาก builtin คุณควรแทนที่ทั้งสองอย่าง

หากคุณต้องการให้โค้ดของคุณทำงานใน Python 2 ให้ทำตามคำแนะนำสำหรับ Python 2 และมันจะทำงานใน Python 3 ได้ดี

ใน Python 2 Python เองไม่ได้ใช้การดำเนินการใด ๆ โดยอัตโนมัติในรูปแบบอื่นดังนั้นคุณควรกำหนด__ne__เงื่อนไข==แทนการใช้__eq__. เช่น

class A(object):
    def __eq__(self, other):
        return self.value == other.value

    def __ne__(self, other):
        return not self == other # NOT `return not self.__eq__(other)`

ดูหลักฐานว่า

  • การใช้__ne__()ตัวดำเนินการตาม__eq__และ
  • ไม่ได้ใช้งาน__ne__ใน Python 2 เลย

แสดงพฤติกรรมที่ไม่ถูกต้องในการสาธิตด้านล่าง

คำตอบยาว

เอกสารสำหรับงูหลาม 2 พูดว่า:

ไม่มีความสัมพันธ์โดยนัยระหว่างตัวดำเนินการเปรียบเทียบ ความจริงของx==yไม่ได้หมายความว่าx!=yเป็นเท็จ ดังนั้นเมื่อกำหนด__eq__()ควรกำหนดด้วย__ne__()เพื่อให้ตัวดำเนินการทำงานตามที่คาดไว้

นั่นหมายความว่าถ้าเรานิยาม__ne__ในแง่ของการผกผัน__eq__เราจะได้พฤติกรรมที่สอดคล้องกัน

ส่วนนี้ของเอกสารได้รับการอัปเดตสำหรับPython 3:

โดยค่าเริ่มต้น__ne__()ได้รับมอบหมายให้และตีความผลเว้นแต่เป็น__eq__()NotImplemented

และในส่วน"มีอะไรใหม่"เราพบว่าพฤติกรรมนี้เปลี่ยนไป:

  • !=ตอนนี้ส่งกลับตรงข้ามของ==เว้นแต่ผลตอบแทน==NotImplemented

สำหรับการนำไปใช้งาน__ne__เราต้องการใช้ตัว==ดำเนินการแทนการใช้__eq__วิธีการโดยตรงเพื่อที่ว่าหากself.__eq__(other)คลาสย่อยส่งคืนNotImplementedสำหรับประเภทที่ตรวจสอบ Python จะตรวจสอบอย่างเหมาะสมother.__eq__(self) จากเอกสารประกอบ :

NotImplementedวัตถุ

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

เมื่อได้รับการดำเนินการเปรียบเทียบที่อุดมไปด้วยถ้าพวกเขาไม่ได้ชนิดเดียวกันตรวจสอบหลามถ้าotherเป็นชนิดย่อยและหากมีผู้ประกอบการที่กำหนดไว้จะใช้other's วิธีแรก (ผกผันสำหรับ<, <=, >=และ>) ถ้าNotImplementedถูกส่งกลับก็จะใช้วิธีการของตรงกันข้าม (ไม่ได้ตรวจสอบวิธีเดิมซ้ำสองครั้ง) การใช้ตัว==ดำเนินการทำให้ตรรกะนี้เกิดขึ้น


ความคาดหวัง

ตามความหมายคุณควรใช้__ne__ในแง่ของการตรวจสอบความเท่าเทียมกันเนื่องจากผู้ใช้ในชั้นเรียนของคุณคาดหวังว่าฟังก์ชันต่อไปนี้จะเทียบเท่ากับ A:

def negation_of_equals(inst1, inst2):
    """always should return same as not_equals(inst1, inst2)"""
    return not inst1 == inst2

def not_equals(inst1, inst2):
    """always should return same as negation_of_equals(inst1, inst2)"""
    return inst1 != inst2

นั่นคือทั้งสองฟังก์ชั่นดังกล่าวข้างต้นควรเสมอกลับผลเดียวกัน แต่ทั้งนี้ขึ้นอยู่กับโปรแกรมเมอร์

การสาธิตพฤติกรรมที่ไม่คาดคิดเมื่อกำหนด__ne__ตาม__eq__:

ขั้นแรกการตั้งค่า:

class BaseEquatable(object):
    def __init__(self, x):
        self.x = x
    def __eq__(self, other):
        return isinstance(other, BaseEquatable) and self.x == other.x

class ComparableWrong(BaseEquatable):
    def __ne__(self, other):
        return not self.__eq__(other)

class ComparableRight(BaseEquatable):
    def __ne__(self, other):
        return not self == other

class EqMixin(object):
    def __eq__(self, other):
        """override Base __eq__ & bounce to other for __eq__, e.g. 
        if issubclass(type(self), type(other)): # True in this example
        """
        return NotImplemented

class ChildComparableWrong(EqMixin, ComparableWrong):
    """__ne__ the wrong way (__eq__ directly)"""

class ChildComparableRight(EqMixin, ComparableRight):
    """__ne__ the right way (uses ==)"""

class ChildComparablePy3(EqMixin, BaseEquatable):
    """No __ne__, only right in Python 3."""

อินสแตนซ์อินสแตนซ์ที่ไม่เทียบเท่า:

right1, right2 = ComparableRight(1), ChildComparableRight(2)
wrong1, wrong2 = ComparableWrong(1), ChildComparableWrong(2)
right_py3_1, right_py3_2 = BaseEquatable(1), ChildComparablePy3(2)

พฤติกรรมที่คาดหวัง:

(หมายเหตุ: ในขณะที่การยืนยันทุกวินาทีของแต่ละรายการด้านล่างมีค่าเท่ากันและด้วยเหตุนี้จึงซ้ำซ้อนกับเหตุผลก่อนหน้านี้ฉันรวมไว้ด้วยเพื่อแสดงให้เห็นว่าคำสั่งนั้นไม่สำคัญว่าเมื่อใดที่หนึ่งเป็นคลาสย่อยของอีกคลาสหนึ่ง )

อินสแตนซ์เหล่านี้ได้__ne__นำไปใช้กับ==:

assert not right1 == right2
assert not right2 == right1
assert right1 != right2
assert right2 != right1

อินสแตนซ์เหล่านี้การทดสอบภายใต้ Python 3 ยังทำงานได้อย่างถูกต้อง:

assert not right_py3_1 == right_py3_2
assert not right_py3_2 == right_py3_1
assert right_py3_1 != right_py3_2
assert right_py3_2 != right_py3_1

และโปรดจำไว้ว่าสิ่งเหล่านี้ได้__ne__ดำเนินการด้วย__eq__- แม้ว่านี่จะเป็นพฤติกรรมที่คาดไว้ แต่การนำไปใช้ไม่ถูกต้อง:

assert not wrong1 == wrong2         # These are contradicted by the
assert not wrong2 == wrong1         # below unexpected behavior!

พฤติกรรมที่ไม่คาดคิด:

โปรดทราบว่าการเปรียบเทียบนี้ขัดแย้งกับการเปรียบเทียบด้านบน ( not wrong1 == wrong2)

>>> assert wrong1 != wrong2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

และ,

>>> assert wrong2 != wrong1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

อย่าข้าม__ne__ใน Python 2

เพื่อเป็นหลักฐานว่าคุณไม่ควรข้ามการนำไปใช้__ne__ใน Python 2 โปรดดูวัตถุที่เทียบเท่าเหล่านี้:

>>> right_py3_1, right_py3_1child = BaseEquatable(1), ChildComparablePy3(1)
>>> right_py3_1 != right_py3_1child # as evaluated in Python 2!
True

ผลลัพธ์ข้างต้นควรเป็นFalse!

ที่มา Python 3

การใช้ CPython เริ่มต้นสำหรับ__ne__อยู่typeobject.cในobject_richcompare :

case Py_NE:
    /* By default, __ne__() delegates to __eq__() and inverts the result,
       unless the latter returns NotImplemented. */
    if (Py_TYPE(self)->tp_richcompare == NULL) {
        res = Py_NotImplemented;
        Py_INCREF(res);
        break;
    }
    res = (*Py_TYPE(self)->tp_richcompare)(self, other, Py_EQ);
    if (res != NULL && res != Py_NotImplemented) {
        int ok = PyObject_IsTrue(res);
        Py_DECREF(res);
        if (ok < 0)
            res = NULL;
        else {
            if (ok)
                res = Py_False;
            else
                res = Py_True;
            Py_INCREF(res);
        }
    }
    break;

แต่ค่าเริ่มต้น__ne__ใช้__eq__?

__ne__รายละเอียดการใช้งานเริ่มต้นของ Python 3 ที่ระดับ C ใช้__eq__เนื่องจากระดับที่สูงกว่า==( PyObject_RichCompare ) จะมีประสิทธิภาพน้อยกว่า - ดังนั้นจึงต้องจัดการNotImplementedด้วย

หาก__eq__มีการใช้งานอย่างถูกต้องแล้วการปฏิเสธของ==ยังเป็นที่ถูกต้อง - __ne__และมันช่วยให้เราสามารถหลีกเลี่ยงการรายละเอียดการปฏิบัติในระดับต่ำของเรา

ใช้==ช่วยให้เราเพื่อให้ตรรกะระดับต่ำของเราในการเป็นหนึ่งในสถานที่และหลีกเลี่ยงการอยู่ในNotImplemented__ne__

หนึ่งอาจจะไม่ถูกต้องคิดว่าอาจจะกลับมา==NotImplemented

มันใช้ตรรกะเดียวกันกับการใช้งานเริ่มต้น__eq__ซึ่งจะตรวจสอบตัวตน (ดูdo_richcompareและหลักฐานของเราด้านล่าง)

class Foo:
    def __ne__(self, other):
        return NotImplemented
    __eq__ = __ne__

f = Foo()
f2 = Foo()

และการเปรียบเทียบ:

>>> f == f
True
>>> f != f
False
>>> f2 == f
False
>>> f2 != f
True

ประสิทธิภาพ

อย่าใช้คำพูดของฉันเลยเรามาดูกันว่ามีอะไรอีกบ้าง

class CLevel:
    "Use default logic programmed in C"

class HighLevelPython:
    def __ne__(self, other):
        return not self == other

class LowLevelPython:
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

def c_level():
    cl = CLevel()
    return lambda: cl != cl

def high_level_python():
    hlp = HighLevelPython()
    return lambda: hlp != hlp

def low_level_python():
    llp = LowLevelPython()
    return lambda: llp != llp

ฉันคิดว่าตัวเลขประสิทธิภาพเหล่านี้พูดเพื่อตัวเอง:

>>> import timeit
>>> min(timeit.repeat(c_level()))
0.09377292497083545
>>> min(timeit.repeat(high_level_python()))
0.2654011140111834
>>> min(timeit.repeat(low_level_python()))
0.3378178110579029

สิ่งนี้สมเหตุสมผลเมื่อคุณพิจารณาว่าlow_level_pythonกำลังทำลอจิกใน Python ซึ่งจะได้รับการจัดการในระดับ C

ตอบสนองต่อนักวิจารณ์บางคน

ผู้ตอบอีกคนเขียนว่า:

การใช้งานวิธีnot self == otherการของAaron Hall __ne__ไม่ถูกต้องเนื่องจากไม่สามารถย้อนกลับได้NotImplemented( not NotImplementedis False) ดังนั้น__ne__วิธีการที่มีลำดับความสำคัญจึงไม่สามารถถอยกลับไปใช้__ne__วิธีการที่ไม่มีลำดับความสำคัญได้

การ__ne__ไม่มีวันกลับมาNotImplementedไม่ได้ทำให้มันไม่ถูกต้อง แต่เราจัดการจัดลำดับความสำคัญด้วยNotImplementedการตรวจสอบความเท่าเทียมกับ==. สมมติว่า==มีการใช้งานอย่างถูกต้องเราทำเสร็จแล้ว

not self == otherเคยเป็นการใช้งาน Python 3 เริ่มต้นของ__ne__วิธีการนี้ แต่เป็นข้อผิดพลาดและได้รับการแก้ไขใน Python 3.4 ในเดือนมกราคม 2015 ตามที่ ShadowRanger สังเกตเห็น (ดูปัญหา # 21408)

เรามาอธิบายกันดีกว่า

ตามที่ระบุไว้ก่อนหน้านี้โดยค่าเริ่มต้น Python 3 จะจัดการ__ne__โดยการตรวจสอบก่อนว่าself.__eq__(other)ส่งคืนNotImplemented(singleton) - ซึ่งควรตรวจสอบด้วยisและส่งคืนหากเป็นเช่นนั้นมิฉะนั้นควรส่งคืนค่าผกผัน นี่คือตรรกะที่เขียนเป็นคลาส mixin:

class CStyle__ne__:
    """Mixin that provides __ne__ functionality equivalent to 
    the builtin functionality
    """
    def __ne__(self, other):
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

สิ่งนี้จำเป็นสำหรับความถูกต้องสำหรับ Python API ระดับ C และได้รับการแนะนำใน Python 3 ทำให้

ซ้ำซ้อน __ne__วิธีการที่เกี่ยวข้องทั้งหมดถูกลบออกรวมถึงวิธีการใช้เช็คของตนเองและวิธีการที่มอบหมายให้__eq__โดยตรงหรือผ่านทาง==- และ==เป็นวิธีที่ใช้บ่อยที่สุดในการดำเนินการดังกล่าว

สมมาตรมีความสำคัญหรือไม่?

นักวิจารณ์ถาวรของเราให้เป็นตัวอย่างที่ทางพยาธิวิทยาในการทำกรณีสำหรับการจัดการNotImplementedใน__ne__มูลค่าสมมาตรเหนือสิ่งอื่นใด ลองยกตัวอย่างที่ชัดเจน:

class B:
    """
    this class has no __eq__ implementation, but asserts 
    any instance is not equal to any other object
    """
    def __ne__(self, other):
        return True

class A:
    "This class asserts instances are equivalent to all other objects"
    def __eq__(self, other):
        return True

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, False, True)

ดังนั้นด้วยตรรกะนี้เพื่อรักษาความสมมาตรเราจำเป็นต้องเขียนสิ่งที่ซับซ้อน__ne__โดยไม่คำนึงถึงเวอร์ชัน Python

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return True
    def __ne__(self, other):
        result = other.__eq__(self)
        if result is NotImplemented:
            return NotImplemented
        return not result

>>> A() == B(), B() == A(), A() != B(), B() != A()
(True, True, True, True)

เห็นได้ชัดว่าเราไม่ควรคำนึงว่าอินสแตนซ์เหล่านี้ทั้งเท่ากันและไม่เท่ากัน

ฉันเสนอว่าความสมมาตรมีความสำคัญน้อยกว่าข้อสันนิษฐานของรหัสที่สมเหตุสมผลและปฏิบัติตามคำแนะนำของเอกสารประกอบ

อย่างไรก็ตามหาก A มีการใช้งานที่สมเหตุสมผล__eq__เราก็ยังสามารถทำตามคำแนะนำของฉันได้ที่นี่และเราจะยังคงมีความสมมาตร:

class B:
    def __ne__(self, other):
        return True

class A:
    def __eq__(self, other):
        return False         # <- this boolean changed... 

>>> A() == B(), B() == A(), A() != B(), B() != A()
(False, False, True, True)

สรุป

สำหรับงูหลาม 2 รหัสที่รองรับการใช้งานในการดำเนินการ== __ne__มันเป็นมากกว่า:

  • แก้ไข
  • เรียบง่าย
  • นักแสดง

ใน Python 3 เท่านั้นให้ใช้การปฏิเสธระดับต่ำในระดับ C ซึ่งง่ายกว่าและมีประสิทธิภาพมากขึ้น (แม้ว่าโปรแกรมเมอร์จะรับผิดชอบในการพิจารณาว่าถูกต้องก็ตาม)

อีกครั้งทำไม่ตรรกะระดับต่ำเขียนในระดับสูงหลาม


3
ตัวอย่างยอดเยี่ยม! สิ่งที่น่าประหลาดใจส่วนหนึ่งคือลำดับของตัวถูกดำเนินการไม่สำคัญเลยซึ่งแตกต่างจากวิธีการใช้เวทมนตร์บางอย่างที่มีการสะท้อน "ด้านขวา" ในการทำซ้ำส่วนที่ฉันพลาดไป (ซึ่งทำให้ฉันเสียเวลามาก): วิธีการเปรียบเทียบที่สมบูรณ์ของคลาสย่อยจะถูกลองก่อนโดยไม่คำนึงว่าโค้ดนั้นมีซูเปอร์คลาสหรือคลาสย่อยทางด้านซ้ายของตัวดำเนินการหรือไม่ นี่คือสาเหตุที่การa1 != c2ส่งคืนของคุณFalse- มันไม่ทำงานa1.__ne__แต่กลับc2.__ne__ทำให้วิธีการของมิกซ์ __eq__อิน ตั้งแต่NotImplementedเป็น truthy, คือnot NotImplemented False
Kevin J. Chase

2
การอัปเดตล่าสุดของคุณแสดงให้เห็นถึงข้อได้เปรียบด้านประสิทธิภาพที่ประสบความสำเร็จnot (self == other)แต่ไม่มีใครเถียงว่ามันไม่เร็ว (ดีเร็วกว่าตัวเลือกอื่น ๆ ใน Py2 อยู่ดี) ปัญหาคือมันผิดในบางกรณี งูหลามตัวเองใช้ในการทำnot (self == other)แต่เปลี่ยนเพราะมันก็ไม่ถูกต้องในการปรากฏตัวของ subclasses ที่เร็วที่สุดเพื่อคำตอบที่ผิดก็ยังคงไม่ถูกต้อง
ShadowRanger

1
ตัวอย่างที่เฉพาะเจาะจงนั้นไม่สำคัญจริงๆ ปัญหาคือในการนำไปใช้งานพฤติกรรมของ__ne__ผู้รับมอบสิทธิ์ของคุณ__eq__(ของทั้งสองฝ่ายหากจำเป็น) แต่จะไม่ถอยกลับไปสู่__ne__อีกด้านหนึ่งแม้ว่าทั้งสองจะ__eq__"ยอมแพ้" ก็ตาม __ne__ผู้รับมอบสิทธิ์ที่ถูกต้องเป็นของตัวเอง __eq__แต่ถ้าส่งกลับNotImplementedก็จะกลับไปที่อีกด้านหนึ่ง__ne__แทนที่จะกลับด้านอีกด้านหนึ่ง__eq__(เนื่องจากอีกด้านหนึ่งอาจไม่ได้เลือกอย่างชัดเจนในการมอบหมายให้__eq__และคุณไม่ควร กำลังตัดสินใจนั้น)
ShadowRanger

1
@AaronHall: ในการตรวจสอบอีกครั้งในวันนี้ฉันไม่คิดว่าการใช้งานของคุณจะเป็นปัญหาสำหรับคลาสย่อยตามปกติ (มันจะซับซ้อนมากที่จะทำให้มันแตกและคลาสย่อยที่ถือว่ามีความรู้อย่างเต็มที่เกี่ยวกับผู้ปกครองควรจะหลีกเลี่ยงได้ ). แต่ฉันแค่ให้ตัวอย่างที่ไม่ซับซ้อนในคำตอบของฉัน กรณีที่ไม่ใช่พยาธิวิทยาคือ ORM ของ SQLAlchemy โดยที่ทั้งสอง__eq__หรือไม่__ne__ส่งกลับTrueหรือFalseแต่เป็นวัตถุพร็อกซี (ซึ่งเป็น "ความจริง") การใช้งานไม่ถูกต้อง__ne__หมายถึงการสั่งซื้อมีความสำคัญต่อการเปรียบเทียบ (คุณจะได้รับพร็อกซีในการสั่งซื้อเดียวเท่านั้น)
ShadowRanger

1
เพื่อความชัดเจนใน 99% (หรือ 99.999%) ของกรณีการแก้ปัญหาของคุณนั้นใช้ได้ดีและ (ชัดเจน) เร็วขึ้น แต่เนื่องจากคุณไม่สามารถควบคุมกรณีที่ไม่ดีได้ในฐานะผู้เขียนไลบรารีที่ผู้อื่นอาจใช้รหัส (อ่าน: อะไรก็ได้ยกเว้นสคริปต์และโมดูลแบบใช้ครั้งเดียวที่เรียบง่ายสำหรับการใช้งานส่วนตัวเท่านั้น) คุณต้อง ใช้การใช้งานที่ถูกต้องเพื่อปฏิบัติตามสัญญาทั่วไปสำหรับผู้ให้บริการทำงานหนักเกินไปและทำงานกับรหัสอื่น ๆ ที่คุณอาจพบ โชคดีที่ใน Py3 ไม่มีเรื่องนี้เนื่องจากคุณสามารถละเว้นได้__ne__ทั้งหมด หนึ่งปีนับจากนี้ Py2 จะตายและเราไม่สนใจสิ่งนี้ :-)
ShadowRanger

11

สำหรับการบันทึกนั้น Py2 / Py3 แบบพกพาที่ถูกต้องตามบัญญัติ__ne__จะมีลักษณะดังนี้:

import sys

class ...:
    ...
    def __eq__(self, other):
        ...

    if sys.version_info[0] == 2:
        def __ne__(self, other):
            equal = self.__eq__(other)
            return equal if equal is NotImplemented else not equal

สิ่งนี้ใช้ได้กับสิ่งที่__eq__คุณอาจกำหนด:

  • แตกต่างจากnot (self == other)ไม่รบกวนในบางกรณีที่น่ารำคาญ / ซับซ้อนที่เกี่ยวข้องกับการเปรียบเทียบโดยที่หนึ่งในคลาสที่เกี่ยวข้องไม่ได้หมายความว่าผลลัพธ์ของ__ne__จะเหมือนกับผลลัพธ์ของnoton __eq__(เช่น SQLAlchemy's ORM ที่ทั้งสอง__eq__และ__ne__ส่งคืนอ็อบเจ็กต์พร็อกซีพิเศษ ไม่ใช่TrueหรือFalseและพยายามที่จะได้notผลลัพธ์ของ__eq__would return Falseแทนที่จะเป็นวัตถุพร็อกซีที่ถูกต้อง)
  • ไม่เหมือนกับnot self.__eq__(other)สิ่งนี้มอบหมายให้กับ__ne__อินสแตนซ์อื่นอย่างถูกต้องเมื่อself.__eq__ส่งคืนNotImplemented( not self.__eq__(other)จะผิดเป็นพิเศษเนื่องจากNotImplementedเป็นความจริงดังนั้นเมื่อ__eq__ไม่ทราบวิธีการเปรียบเทียบ__ne__จะส่งคืนFalseซึ่งหมายความว่าวัตถุทั้งสองมีค่าเท่ากันเมื่อในความเป็นจริงเท่านั้น วัตถุที่ถามไม่มีความคิดซึ่งหมายความว่าค่าเริ่มต้นไม่เท่ากัน)

หากคุณ__eq__ไม่ได้ใช้NotImplementedผลตอบแทนสิ่งนี้จะใช้งานได้ (โดยมีค่าใช้จ่ายที่ไร้ความหมาย) หากใช้ในNotImplementedบางครั้งสิ่งนี้จะจัดการอย่างถูกต้อง และงูหลามตรวจสอบรุ่นหมายความว่าถ้าชั้นจะimport-ed ในหลาม 3 __ne__ทิ้ง undefined ช่วยให้พื้นเมืองทางเลือกที่มีประสิทธิภาพ ธ__ne__การดำเนินงาน (รุ่น C ข้างต้น)จะใช้เวลามากกว่า


ทำไมจึงจำเป็น

กฎการโอเวอร์โหลด Python

คำอธิบายว่าเหตุใดคุณจึงทำสิ่งนี้แทนวิธีแก้ปัญหาอื่น ๆ นั้นค่อนข้างลึกลับ Python มีกฎทั่วไปสองสามข้อเกี่ยวกับตัวดำเนินการโอเวอร์โหลดและตัวดำเนินการเปรียบเทียบโดยเฉพาะ:

  1. (นำไปใช้กับผู้ประกอบการทั้งหมด) เมื่อใช้LHS OP RHSลองLHS.__op__(RHS)และถ้าผลตอบแทนที่ลองNotImplemented RHS.__rop__(LHS)ข้อยกเว้น: ถ้าRHSเป็น subclass ของLHSคลาส 's แล้วทดสอบครั้งแรกRHS.__rop__(LHS) ในกรณีของผู้ประกอบการเปรียบเทียบ__eq__และ__ne__เป็นของตัวเอง "เชือก" s (ดังนั้นเพื่อการทดสอบสำหรับ__ne__เป็นLHS.__ne__(RHS)แล้วRHS.__ne__(LHS)กลับถ้าRHSเป็น subclass ของLHS's class)
  2. นอกเหนือจากแนวคิดของตัวดำเนินการ "swapped" แล้วไม่มีความสัมพันธ์โดยนัยระหว่างตัวดำเนินการ แม้จะเป็นตัวอย่างของคลาสเดียวกันการLHS.__eq__(RHS)ส่งคืนTrueก็ไม่ได้หมายความว่าLHS.__ne__(RHS)จะส่งกลับFalse(อันที่จริงตัวดำเนินการไม่จำเป็นต้องส่งคืนค่าบูลีนด้วยซ้ำ ORM เช่น SQLAlchemy จะไม่ตั้งใจทำให้สามารถใช้ไวยากรณ์แบบสอบถามที่แสดงออกได้ชัดเจนขึ้น) สำหรับ Python 3 การ__ne__ใช้งานเริ่มต้นจะทำงานในลักษณะนี้ แต่ไม่ใช่แบบสัญญา คุณสามารถแทนที่ในรูปแบบที่ไม่ได้ตรงข้ามที่เข้มงวดของ__ne____eq__

วิธีนี้ใช้กับเครื่องเปรียบเทียบการโอเวอร์โหลด

ดังนั้นเมื่อคุณทำงานหนักเกินไปคุณจะมีงานสองอย่าง:

  1. ถ้าคุณรู้วิธีที่จะใช้ดำเนินการด้วยตัวเองทำเช่นนั้นโดยใช้เพียงความรู้ของตัวเองของวิธีการทำเปรียบเทียบ (ไม่เคยมอบหมายโดยปริยายหรืออย่างชัดเจนในด้านอื่น ๆ ของการดำเนินงานที่ทำเช่นนั้นความเสี่ยงที่ไม่ถูกต้องและ / หรือการเรียกซ้ำอนันต์ ขึ้นอยู่กับว่าคุณทำมันอย่างไร)
  2. หากคุณไม่ทราบวิธีใช้การดำเนินการด้วยตนเองให้ส่งคืนเสมอNotImplementedเพื่อให้ Python สามารถมอบหมายการใช้งานตัวถูกดำเนินการอื่นได้

ปัญหาเกี่ยวกับ not self.__eq__(other)

def __ne__(self, other):
    return not self.__eq__(other)

อย่ามอบหมายให้อีกฝ่ายหนึ่ง (และไม่ถูกต้องหาก__eq__ส่งคืนอย่างถูกต้องNotImplemented) เมื่อself.__eq__(other)ส่งคืนNotImplemented(ซึ่งก็คือ "ความจริง") คุณจะกลับมาอย่างเงียบ ๆFalseดังนั้นควรA() != something_A_knows_nothing_aboutส่งคืนFalseเมื่อควรตรวจสอบว่าsomething_A_knows_nothing_aboutทราบวิธีเปรียบเทียบกับอินสแตนซ์Aหรือไม่และหากไม่เป็นเช่นนั้นควรส่งคืนTrue(เนื่องจากทั้งสองฝ่ายไม่รู้วิธี เปรียบเทียบกับอีกคนหนึ่งถือว่าไม่เท่าเทียมกัน) หากA.__eq__มีการนำไปใช้อย่างไม่ถูกต้อง (กลับมาFalseแทนที่จะเป็นNotImplementedเมื่อมันไม่รู้จักอีกด้านหนึ่ง) แสดงว่า "ถูกต้อง" จากAมุมมองของการส่งคืนTrue(เนื่องจากAไม่คิดว่ามันเท่ากันจึงไม่เท่ากัน) แต่อาจเป็น ผิดจากsomething_A_knows_nothing_aboutมุมมองของเพราะมันไม่เคยถามsomething_A_knows_nothing_about; A() != something_A_knows_nothing_aboutลงเอยTrueแต่something_A_knows_nothing_about != A()ได้Falseหรือค่าตอบแทนอื่น ๆ

ปัญหาเกี่ยวกับ not self == other

def __ne__(self, other):
    return not self == other

มีความละเอียดอ่อนมากขึ้น มันจะถูกต้องสำหรับ 99% ของคลาสรวมถึงคลาสทั้งหมดที่__ne__ผกผันตรรกะของ__eq__. แต่not self == otherทำลายกฎทั้งสองที่กล่าวถึงข้างต้นซึ่งหมายความว่าสำหรับคลาสที่__ne__ ไม่ผกผันเชิงตรรกะ__eq__ผลลัพธ์จะไม่สมมาตรอีกครั้งเนื่องจากไม่เคยถามตัวถูกดำเนินการอย่างใดอย่างหนึ่งว่าสามารถใช้งาน__ne__ได้หรือไม่แม้ว่าอีกอันหนึ่ง ตัวถูกดำเนินการไม่ได้ ตัวอย่างที่ง่ายเป็นชั้นกะเทยซึ่งผลตอบแทนFalseสำหรับทุกการเปรียบเทียบเพื่อให้A() == Incomparable()และผลตอบแทนทั้งA() != Incomparable() Falseด้วยการใช้งานที่ถูกต้องของA.__ne__(ซึ่งจะส่งคืนNotImplementedเมื่อไม่รู้ว่าจะทำการเปรียบเทียบอย่างไร) ความสัมพันธ์จะสมมาตร A() != Incomparable()และIncomparable() != A()เห็นด้วยกับผลลัพธ์ (เนื่องจากในกรณีก่อนหน้านี้A.__ne__จะส่งคืนNotImplementedแล้วIncomparable.__ne__กลับFalseมาในขณะที่Incomparable.__ne__ผลตอบแทนกลับมาFalseโดยตรง) แต่เมื่อA.__ne__จะดำเนินการเป็นreturn not self == other, A() != Incomparable()ผลตอบแทนTrue(เพราะA.__eq__ผลตอบแทนที่ไม่NotImplementedแล้วIncomparable.__eq__ผลตอบแทนFalseและA.__ne__ตีความว่าTrue) ในขณะที่Incomparable() != A()ผลตอบแทนFalse.

คุณสามารถดูตัวอย่างนี้ในการดำเนินการที่นี่

เห็นได้ชัดว่าคลาสที่ให้ผลตอบแทนFalseทั้งคู่เสมอ__eq__และ__ne__ค่อนข้างแปลก แต่ตามที่กล่าวไว้ก่อนหน้านี้__eq__และ__ne__ไม่จำเป็นต้องกลับTrue/ False; SQLAlchemy ORM มีคลาสที่มีตัวเปรียบเทียบที่ส่งคืนอ็อบเจ็กต์พร็อกซีพิเศษสำหรับการสร้างคิวรีไม่ใช่True/ Falseเลย (เป็น "จริง" หากประเมินในบริบทบูลีน แต่ไม่ควรได้รับการประเมินในบริบทดังกล่าว)

เมื่อล้มเหลวในการโอเวอร์โหลด__ne__อย่างถูกต้องคุณจะแบ่งคลาสของการจัดเรียงนั้นเป็นรหัส:

 results = session.query(MyTable).filter(MyTable.fieldname != MyClassWithBadNE())

จะใช้งานได้ (สมมติว่า SQLAlchemy รู้วิธีแทรกMyClassWithBadNEลงในสตริง SQL เลยซึ่งสามารถทำได้ด้วยอะแดปเตอร์ชนิดโดยไม่MyClassWithBadNEต้องร่วมมือเลย) ส่งผ่านวัตถุพร็อกซีที่คาดไว้ไปfilterยังในขณะที่:

 results = session.query(MyTable).filter(MyClassWithBadNE() != MyTable.fieldname)

จะจบลงด้วยการส่งผ่านfilterธรรมดาFalseเนื่องจากself == otherส่งคืนอ็อบเจ็กต์พร็อกซีและnot self == otherเพียงแค่แปลงอ็อบเจ็กต์พร็อกซีที่Falseแท้จริงเป็น หวังว่าจะfilterมีข้อยกเว้นในการจัดการข้อโต้แย้งที่ไม่ถูกต้องเช่นFalse. ในขณะที่ผมมั่นใจว่าหลายคนจะยืนยันว่าMyTable.fieldname ควรจะมีอย่างต่อเนื่องในทางด้านซ้ายมือของการเปรียบเทียบยังคงความจริงที่ว่าไม่มีเหตุผลที่การเขียนโปรแกรมในการบังคับใช้ในกรณีทั่วไปและถูกต้องทั่วไป__ne__การทำงานจะทางใดทางหนึ่งในขณะที่return not self == otherผลงานเท่านั้น ในการจัดเรียงครั้งเดียว


1
คำตอบที่ถูกต้องสมบูรณ์และซื่อสัตย์เท่านั้น (ขออภัย @AaronHall) นี่ควรเป็นคำตอบที่ได้รับการยอมรับ
Maggyero

คุณอาจจะสนใจคำตอบของฉันมีการปรับปรุงซึ่งใช้ผมคิดว่าข้อโต้แย้งของคุณแข็งแรงกว่าIncomparableระดับตั้งแต่นี้แบ่งชั้นเติมเต็มความสัมพันธ์ระหว่าง!=และ==ผู้ประกอบการและดังนั้นจึงอาจได้รับการพิจารณาหรือไม่ถูกต้องเช่น "พยาธิวิทยา" ชอบ @AaronHall ใส่มัน และฉันยอมรับว่า @AaronHall มีประเด็นเมื่อเขาชี้ให้เห็นว่าอาร์กิวเมนต์ SQLAlchemy ของคุณอาจถูกพิจารณาว่าไม่เกี่ยวข้องเนื่องจากอยู่ในบริบทที่ไม่ใช่บูลีน (ข้อโต้แย้งของคุณยังคงน่าสนใจและน่าคิด)
Maggyero

1
+1. สำหรับโครงการที่มีอยู่ซึ่งลืมใช้__ne__สำหรับ Python 2 เมื่อนานมาแล้วฉันแค่มองหา__ne__shim ที่เลียนแบบพฤติกรรมของ Python 3-without- ได้ดีที่สุด__ne__เพื่อป้องกันการถดถอยสำหรับผู้ใช้ Python 3 ที่มีอยู่แม้ในสถานการณ์ทางพยาธิวิทยา ผมทดสอบ @ แก้ปัญหา AaronHall กับชั้นเรียนอื่น ๆ อีกหลายบางที่ซับซ้อนที่ยอมรับ แต่มันก็บางครั้งก็ไม่กลับมาเช่นเดียวกับงูหลาม __ne__3 ในทางตรงกันข้ามโซลูชัน @ ShadowRanger / @ Maggyero นี้จะทำงานเหมือนกับ Python 3-without- เสมอ__ne__ไม่ว่าฉันจะโยนของบ้าอะไรก็ตาม
Peter Nowee

5

__ne__การใช้งานที่ถูกต้อง

การใช้วิธีพิเศษของ @ ShadowRanger เป็นวิธี__ne__ที่ถูกต้อง:

def __ne__(self, other):
    result = self.__eq__(other)
    if result is not NotImplemented:
        return not result
    return NotImplemented

นอกจากนี้ยังเป็นการดำเนินการตามค่าเริ่มต้นของวิธีการพิเศษ__ne__ ตั้งแต่ Python 3.4ตามที่ระบุไว้ในเอกสาร Python :

โดยค่าเริ่มต้น__ne__()ได้รับมอบหมายให้และตีความผลเว้นแต่เป็น__eq__()NotImplemented

นอกจากนี้ทราบว่ากลับค่าสำหรับตัวถูกดำเนินการได้รับการสนับสนุนไม่ได้เฉพาะเจาะจงกับวิธีการพิเศษNotImplemented __ne__ในความเป็นจริงวิธีการเปรียบเทียบแบบพิเศษทั้งหมด1และวิธีตัวเลขพิเศษ2ควรส่งคืนค่าNotImplementedสำหรับตัวถูกดำเนินการที่ไม่รองรับตามที่ระบุไว้ในเอกสาร Python :

ไม่ได้ดำเนินการ

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

ตัวอย่างสำหรับวิธีการตัวเลขพิเศษมีให้ในเอกสาร Python :

class MyIntegral(Integral):

    def __add__(self, other):
        if isinstance(other, MyIntegral):
            return do_my_adding_stuff(self, other)
        elif isinstance(other, OtherTypeIKnowAbout):
            return do_my_other_adding_stuff(self, other)
        else:
            return NotImplemented

    def __radd__(self, other):
        if isinstance(other, MyIntegral):
            return do_my_adding_stuff(other, self)
        elif isinstance(other, OtherTypeIKnowAbout):
            return do_my_other_adding_stuff(other, self)
        elif isinstance(other, Integral):
            return int(other) + int(self)
        elif isinstance(other, Real):
            return float(other) + float(self)
        elif isinstance(other, Complex):
            return complex(other) + complex(self)
        else:
            return NotImplemented

1วิธีการเปรียบเทียบพิเศษ: __lt__, __le__, __eq__, __ne__, และ__gt____ge__

2วิธีการที่เป็นตัวเลขพิเศษ: __add__, __sub__, __mul__, __matmul__, __truediv__, __floordiv__, __mod__, __divmod__, __pow__, __lshift__, __rshift__, __and__, __xor__, __or__และของพวกเขา__r*__และสะท้อนให้เห็น__i*__ในสถานที่ลูกน้อง

__ne__การใช้งานไม่ถูกต้อง# 1

การใช้วิธีพิเศษของ @ Falmarri __ne__ไม่ถูกต้อง:

def __ne__(self, other):
    return not self.__eq__(other)

ปัญหาเกี่ยวกับการนำไปใช้งานนี้คือจะไม่ถอยกลับไปใช้เมธอดพิเศษ__ne__ของตัวถูกดำเนินการอื่นเนื่องจากไม่เคยส่งคืนค่าNotImplemented(นิพจน์not self.__eq__(other)ประเมินเป็นค่าTrueหรือFalseรวมถึงเมื่อนิพจน์ย่อยself.__eq__(other)ประเมินเป็นค่าNotImplementedเนื่องจากนิพจน์bool(NotImplemented)ประเมินเป็นค่าTrue). การประเมินค่าบูลีนNotImplementedจะทำลายความสัมพันธ์เสริมระหว่างตัวดำเนินการเปรียบเทียบ!=และ==:

class Correct:

    def __ne__(self, other):
        result = self.__eq__(other)
        if result is not NotImplemented:
            return not result
        return NotImplemented


class Incorrect:

    def __ne__(self, other):
        return not self.__eq__(other)


x, y = Correct(), Correct()
assert (x != y) is not (x == y)

x, y = Incorrect(), Incorrect()
assert (x != y) is not (x == y)  # AssertionError

__ne__การใช้งานไม่ถูกต้อง# 2

การใช้วิธีพิเศษของ @ AaronHall __ne__นั้นไม่ถูกต้อง:

def __ne__(self, other):
    return not self == other

ปัญหาในการใช้งานนี้คือมันกลับไปที่วิธีการพิเศษ__eq__ของตัวถูกดำเนินการอื่นโดยตรงโดยข้ามเมธอดพิเศษ__ne__ของตัวถูกดำเนินการอื่นเนื่องจากไม่ส่งคืนค่าNotImplemented(นิพจน์not self == otherจะกลับไปใช้วิธีพิเศษ__eq__ของตัวถูกดำเนินการอื่นและประเมินเป็น ค่าTrueหรือFalse) การข้ามเมธอดนั้นไม่ถูกต้องเนื่องจากวิธีนั้นอาจมีผลข้างเคียงเช่นการอัปเดตสถานะของวัตถุ:

class Correct:

    def __init__(self):
        self.counter = 0

    def __ne__(self, other):
        self.counter += 1
        result = self.__eq__(other)
        if result is not NotImplemented:
            return not result
        return NotImplemented


class Incorrect:

    def __init__(self):
        self.counter = 0

    def __ne__(self, other):
        self.counter += 1
        return not self == other


x, y = Correct(), Correct()
assert x != y
assert x.counter == y.counter

x, y = Incorrect(), Incorrect()
assert x != y
assert x.counter == y.counter  # AssertionError

การทำความเข้าใจการดำเนินการเปรียบเทียบ

ในวิชาคณิตศาสตร์เป็นฐานความสัมพันธ์ Rมากกว่าชุดXคือชุดของคู่อันดับ (กxY ) ใน  X 2 คำสั่ง ( xY ) ใน  Rอ่าน " xเป็นRที่เกี่ยวข้องกับการY " และแสดงโดยXRY

คุณสมบัติของความสัมพันธ์ไบนารีRเหนือชุดX :

  • Rคือสะท้อนเมื่อสำหรับทุกxในX , XRX
  • Rคือirreflexive (เรียกอีกอย่างเข้มงวด ) เมื่อสำหรับทุกxในXไม่XRX
  • Rคือสมมาตรเมื่อสำหรับทุกxและy ที่ในXถ้าXRYแล้วyRx
  • Rคือantisymmetricเมื่อสำหรับทุกxและy ที่ในXถ้าXRYและyRxแล้วx  =  Y
  • Rเป็นสกรรมกริยาเมื่อสำหรับทุกx , YและZในXถ้าXRYและyRzแล้วxRz
  • RคือConnex (เรียกว่าทั้งหมด ) เมื่อสำหรับทุกxและy ที่ในX , XRYหรือyRx
  • Rคือความสัมพันธ์ที่เท่าเทียมกันเมื่อRเป็นรีเฟล็กซีฟสมมาตรและสกรรมกริยา
    ตัวอย่างเช่น =. อย่างไรก็ตาม≠เป็นสมมาตรเท่านั้น
  • Rคือความสัมพันธ์ของคำสั่งเมื่อRเป็นแบบรีเฟลกซ์แอนตีซิมเมตริกและสกรรมกริยา
    ตัวอย่างเช่น≤และ≥
  • Rคือความสัมพันธ์ของคำสั่งที่เข้มงวดเมื่อRไม่สะท้อนกลับ, antisymmetric และ transitive
    ตัวอย่างเช่น <และ> อย่างไรก็ตาม≠เป็นเพียงการสะท้อนกลับเท่านั้น

การดำเนินการกับสองความสัมพันธ์ไบนารีRและSในชุดX :

  • การสนทนาของRคือความสัมพันธ์ไบนารีR T  = {( yx ) | XRY } มากกว่าX
  • สมบูรณ์ของRเป็นฐานความสัมพันธ์¬ R  = {( xY ) | ไม่XRY } มากกว่าX
  • การรวมกันของRและSคือความสัมพันธ์ไบนารีR  ∪  S  = {( xy ) | XRYหรือxSy } มากกว่าX

ความสัมพันธ์ระหว่างความสัมพันธ์เปรียบเทียบที่ถูกต้องเสมอ:

  • 2 ความสัมพันธ์เสริม: = และ≠คือส่วนเติมเต็มของกันและกัน
  • 6 สนทนาความสัมพันธ์: = คือการสนทนาของตัวมันเอง≠คือการสนทนาของตัวมันเอง <และ> คือการสนทนาของกันและกันและ≤และ≥คือการสนทนาของกันและกัน
  • 2 ความสัมพันธ์แบบยูเนี่ยน: ≤คือยูเนี่ยน <และ = และ≥คือยูเนี่ยนของ> และ =

ความสัมพันธ์ระหว่างความสัมพันธ์ที่มีการเปรียบเทียบที่ถูกต้องสำหรับConnexสั่งซื้อ:

  • ความสัมพันธ์เสริม 4 ประการ: <และ≥เป็นส่วนเติมเต็มของกันและกันและ> และ≤เป็นส่วนเสริมของกันและกัน

ดังนั้นเพื่อให้การดำเนินการอย่างถูกต้องในหลามดำเนินการเปรียบเทียบ==, !=, <, >, <=และ>=สอดคล้องกับความสัมพันธ์เปรียบเทียบ = ≠, <,>, ≤และ≥ทั้งหมดข้างต้นคุณสมบัติทางคณิตศาสตร์และความสัมพันธ์ควรถือ

การดำเนินการเปรียบเทียบx operator yเรียกใช้วิธีการเปรียบเทียบพิเศษ__operator__ของคลาสหนึ่งในตัวถูกดำเนินการ:

class X:

    def __operator__(self, other):
        # implementation

ตั้งแต่Rคือสะท้อนนัยXRXการดำเนินการเปรียบเทียบสะท้อนx operator y( x == y, x <= yและx >= y) หรือสะท้อนวิธีการเปรียบเทียบพิเศษโทรx.__operator__(y)( x.__eq__(y), x.__le__(y)และx.__ge__(y)) ควรประเมินค่าTrueถ้าxและyเหมือนกันว่าถ้าการแสดงออกประเมินx is y Trueตั้งแต่Rคือirreflexiveหมายถึงไม่XRXการดำเนินการเปรียบเทียบ irreflexive x operator y( x != y, x < yและx > y) หรือ irreflexive วิธีการเปรียบเทียบพิเศษโทรx.__operator__(y)( x.__ne__(y), x.__lt__(y)และx.__gt__(y)) ควรประเมินค่าFalseถ้าxและyเหมือนกันนั่นคือถ้าการแสดงออกประเมินx is y Trueคุณสมบัติสะท้อนคือการพิจารณาโดยงูหลามตัวดำเนินการเปรียบเทียบ==และเกี่ยวข้องวิธีการเปรียบเทียบพิเศษ__eq__แต่น่าแปลกใจที่ไม่ได้รับการพิจารณาสำหรับผู้ประกอบการเปรียบเทียบ<=และ>=เกี่ยวข้องและวิธีการเปรียบเทียบพิเศษ__le__และ__ge__และทรัพย์สิน irreflexive จะพิจารณาโดยงูหลามตัวดำเนินการเปรียบเทียบ!=และเกี่ยวข้องวิธีการเปรียบเทียบพิเศษ__ne__แต่น่าแปลกใจที่ไม่ได้รับการพิจารณาสำหรับผู้ประกอบการเปรียบเทียบ<และ>เกี่ยวข้องและวิธีการเปรียบเทียบพิเศษ__lt__และ__gt__. ตัวดำเนินการเปรียบเทียบที่ถูกละเว้นจะเพิ่มข้อยกเว้นแทนTypeError(และวิธีการเปรียบเทียบพิเศษที่เกี่ยวข้องจะส่งคืนค่าแทนNotImplemented) ตามที่อธิบายไว้ในเอกสาร Python :

พฤติกรรมเริ่มต้นสำหรับการเปรียบเทียบความเท่าเทียมกัน ( ==และ!=) ขึ้นอยู่กับเอกลักษณ์ของวัตถุ ดังนั้นการเปรียบเทียบความเท่าเทียมกันของอินสแตนซ์ที่มีอัตลักษณ์เดียวกันจึงทำให้เกิดความเท่าเทียมกันและการเปรียบเทียบความเท่าเทียมกันของอินสแตนซ์ที่มีอัตลักษณ์ต่างกันส่งผลให้เกิดความไม่เท่าเทียมกัน แรงจูงใจสำหรับพฤติกรรมเริ่มต้นนี้เป็นความปรารถนาที่วัตถุทั้งหมดควรจะสะท้อนกลับ (คือx is yหมายถึงx == y)

การเปรียบเทียบเพื่อเริ่มต้น ( <, >, <=และ>=) ไม่ได้ให้; TypeErrorยกความพยายาม แรงจูงใจสำหรับพฤติกรรมเริ่มต้นนี้คือการขาดค่าคงที่ที่คล้ายกันสำหรับความเท่าเทียมกัน [นี้ไม่ถูกต้องตั้งแต่<=และ>=มีสะท้อนเหมือน==และ<และ>มี irreflexive เช่น!=.]

คลาสobjectนี้จัดเตรียมการใช้งานดีฟอลต์ของวิธีการเปรียบเทียบแบบพิเศษซึ่งสืบทอดโดยคลาสย่อยทั้งหมดตามที่อธิบายไว้ในเอกสาร Python :

object.__lt__(self, other)
object.__le__(self, other)
object.__eq__(self, other)
object.__ne__(self, other)
object.__gt__(self, other)
object.__ge__(self, other)

วิธีเหล่านี้เรียกว่าวิธี "การเปรียบเทียบที่สมบูรณ์" จดหมายระหว่างสัญลักษณ์ประกอบการและชื่อวิธีการดังนี้x<yสาย x.__lt__(y), x<=yสายx.__le__(y), x==yสายx.__eq__(y), x!=yสายx.__ne__(y), x>yสายx.__gt__(y)และโทรออกx>=y x.__ge__(y)

วิธีการเปรียบเทียบที่สมบูรณ์อาจส่งคืนซิงเกิลตันNotImplementedหากไม่ใช้การดำเนินการสำหรับคู่ของอาร์กิวเมนต์ที่กำหนด

[…]

ไม่มีเวอร์ชันแลกเปลี่ยนอาร์กิวเมนต์ของวิธีการเหล่านี้ (ที่จะใช้เมื่ออาร์กิวเมนต์ด้านซ้ายไม่สนับสนุนการดำเนินการ แต่อาร์กิวเมนต์ด้านขวาทำ) ค่อนข้าง__lt__()และ__gt__()เป็นภาพสะท้อนของกันและกัน__le__()และ__ge__()มีการสะท้อนของกันและกันและ __eq__()และ__ne__()เป็นภาพสะท้อนของตัวเอง ถ้าตัวถูกดำเนินการเป็นประเภทต่างๆกันและประเภทของตัวถูกดำเนินการด้านขวาเป็นคลาสย่อยทางตรงหรือทางอ้อมของประเภทของตัวถูกดำเนินการด้านซ้ายวิธีการที่สะท้อนของตัวถูกดำเนินการด้านขวาจะมีลำดับความสำคัญมิฉะนั้นวิธีการของตัวถูกดำเนินการด้านซ้ายจะมีลำดับความสำคัญ ไม่พิจารณาคลาสย่อยเสมือน

เนื่องจากR = ( R T ) TการเปรียบเทียบxRyจะเทียบเท่ากับการเปรียบเทียบการสนทนาyR T x (ชื่ออย่างไม่เป็นทางการว่า "สะท้อน" ในเอกสาร Python) ดังนั้นจึงมีสองวิธีในการคำนวณผลลัพธ์ของการดำเนินการเปรียบเทียบx operator y: เรียกอย่างใดอย่างหนึ่งx.__operator__(y)หรือy.__operatorT__(x). Python ใช้กลยุทธ์การคำนวณดังต่อไปนี้:

  1. มันเรียกx.__operator__(y)เว้นแต่คลาสของตัวถูกดำเนินการด้านขวาจะเป็นลูกหลานของคลาสของตัวถูกดำเนินการด้านซ้ายซึ่งในกรณีนี้จะเรียกy.__operatorT__(x)( อนุญาตให้คลาสสามารถแทนที่วิธีการเปรียบเทียบแบบพิเศษของบรรพบุรุษของพวกเขาได้ )
  2. ถ้าตัวถูกดำเนินการxและyไม่สนับสนุน (แสดงโดยค่าส่งกลับNotImplemented) ก็เรียกสนทนาวิธีการเปรียบเทียบพิเศษเป็นทางเลือกที่ 1
  3. หากตัวถูกดำเนินการxและyไม่ได้รับการสนับสนุน (ระบุด้วยค่าส่งคืนNotImplemented) จะทำให้เกิดข้อTypeErrorยกเว้นยกเว้นตัวดำเนินการเปรียบเทียบ==และ!=ซึ่งจะทดสอบข้อมูลประจำตัวและไม่ใช่ข้อมูลประจำตัวของตัวถูกดำเนินการตามลำดับxและyเป็นทางเลือกที่ 2 (ใช้ประโยชน์จากคุณสมบัติการสะท้อนกลับของ==และ คุณสมบัติการสะท้อนกลับของ!=)
  4. มันส่งคืนผลลัพธ์

ใน CPython นี้จะดำเนินการในรหัส Cซึ่งสามารถแปลเป็นรหัสหลาม (มีชื่อeqสำหรับ==, neสำหรับ!=, ltสำหรับ<, gtสำหรับ>, leสำหรับ<=และgeสำหรับ>=):

def eq(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__eq__(left)
        if result is NotImplemented:
            result = left.__eq__(right)
    else:
        result = left.__eq__(right)
        if result is NotImplemented:
            result = right.__eq__(left)
    if result is NotImplemented:
        result = left is right
    return result
def ne(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ne__(left)
        if result is NotImplemented:
            result = left.__ne__(right)
    else:
        result = left.__ne__(right)
        if result is NotImplemented:
            result = right.__ne__(left)
    if result is NotImplemented:
        result = left is not right
    return result
def lt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__gt__(left)
        if result is NotImplemented:
            result = left.__lt__(right)
    else:
        result = left.__lt__(right)
        if result is NotImplemented:
            result = right.__gt__(left)
    if result is NotImplemented:
        raise TypeError(
            f"'<' not supported between instances of '{type(left).__name__}' "
            f"and '{type(right).__name__}'"
        )
    return result
def gt(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__lt__(left)
        if result is NotImplemented:
            result = left.__gt__(right)
    else:
        result = left.__gt__(right)
        if result is NotImplemented:
            result = right.__lt__(left)
    if result is NotImplemented:
        raise TypeError(
            f"'>' not supported between instances of '{type(left).__name__}' "
            f"and '{type(right).__name__}'"
        )
    return result
def le(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__ge__(left)
        if result is NotImplemented:
            result = left.__le__(right)
    else:
        result = left.__le__(right)
        if result is NotImplemented:
            result = right.__ge__(left)
    if result is NotImplemented:
        raise TypeError(
            f"'<=' not supported between instances of '{type(left).__name__}' "
            f"and '{type(right).__name__}'"
        )
    return result
def ge(left, right):
    if type(left) != type(right) and isinstance(right, type(left)):
        result = right.__le__(left)
        if result is NotImplemented:
            result = left.__ge__(right)
    else:
        result = left.__ge__(right)
        if result is NotImplemented:
            result = right.__le__(left)
    if result is NotImplemented:
        raise TypeError(
            f"'>=' not supported between instances of '{type(left).__name__}' "
            f"and '{type(right).__name__}'"
        )
    return result

เนื่องจากR = ¬ (¬ R ) การเปรียบเทียบxRyจะเทียบเท่ากับการเปรียบเทียบส่วนประกอบ ¬ ( x ¬ Ry ) ≠เป็นส่วนเติมเต็มของ = ดังนั้นวิธีการพิเศษ__ne__จึงถูกนำไปใช้ในรูปแบบของวิธีพิเศษ__eq__สำหรับตัวถูกดำเนินการที่รองรับตามค่าเริ่มต้นในขณะที่วิธีการเปรียบเทียบพิเศษอื่น ๆ จะถูกนำไปใช้โดยอิสระตามค่าเริ่มต้น (ความจริงที่ว่า≤คือการรวมกันของ <และ = และ ≥เป็นสหภาพของ> = และเป็นที่น่าแปลกใจไม่ได้พิจารณาซึ่งหมายความว่าในปัจจุบันวิธีการพิเศษ__le__และ__ge__ควรจะใช้ดำเนินการ) ตามที่อธิบายไว้ในเอกสารหลาม :

โดยค่าเริ่มต้น__ne__()ได้รับมอบหมายให้และตีความผลเว้นแต่เป็น__eq__() NotImplementedไม่มีความสัมพันธ์โดยนัยอื่น ๆ ของผู้ประกอบการเปรียบเทียบเป็นตัวอย่างเช่นความจริงของไม่ได้หมายความถึง(x<y or x==y)x<=y

ใน CPython สิ่งนี้ถูกนำไปใช้ในรหัส Cซึ่งสามารถแปลเป็นรหัส Python:

def __eq__(self, other):
    return self is other or NotImplemented
def __ne__(self, other):
    result = self.__eq__(other)
    if result is not NotImplemented:
        return not result
    return NotImplemented
def __lt__(self, other):
    return NotImplemented
def __gt__(self, other):
    return NotImplemented
def __le__(self, other):
    return NotImplemented
def __ge__(self, other):
    return NotImplemented

โดยค่าเริ่มต้น:

  • การดำเนินการเปรียบเทียบx operator yจะทำให้เกิดข้อTypeErrorยกเว้นยกเว้นสำหรับตัวดำเนินการเปรียบเทียบ==และ!=ซึ่งจะส่งคืนข้อมูลประจำตัวและไม่ใช่ข้อมูลประจำตัวของตัวถูกดำเนินการตามลำดับxและy;
  • การเรียกเมธอดการเปรียบเทียบแบบพิเศษx.__operator__(y)จะส่งคืนค่าNotImplementedยกเว้นวิธีการเปรียบเทียบแบบพิเศษ__eq__และ__ne__ซึ่งจะส่งกลับตามลำดับTrueและFalseหากตัวถูกดำเนินการxและyเหมือนกันตามลำดับและไม่เหมือนกันและค่าเป็นNotImplementedอย่างอื่น

ในตัวอย่างสุดท้ายของคุณ: "เนื่องจากการใช้งานนี้ล้มเหลวในการจำลองลักษณะการทำงานของการใช้งานเริ่มต้นของ__ne__วิธีการเมื่อ__eq__วิธีการส่งคืน NotImplemented จึงไม่ถูกต้อง" - Aกำหนดความเท่าเทียมกันโดยไม่มีเงื่อนไข ด้วยA() == B()ประการฉะนี้. ดังนั้นA() != B() ควรจะเป็นเท็จและมันคือ ตัวอย่างที่ให้มาเป็นพยาธิสภาพ (เช่น__ne__ไม่ควรส่งคืนสตริงและ__eq__ไม่ควรขึ้นอยู่กับ__ne__- __ne__ควรขึ้นอยู่กับ__eq__ซึ่งเป็นค่าเริ่มต้นที่คาดหวังใน Python 3) ฉันยังคง -1 สำหรับคำตอบนี้จนกว่าคุณจะเปลี่ยนใจ
Aaron Hall

ตัวอย่างสุดท้ายมีสองชั้น, Bผลตอบแทนที่สตริง truthy ในการตรวจสอบทั้งหมด__ne__และAผลตอบแทนที่ในการตรวจสอบทั้งหมดTrue __eq__นี่คือความขัดแย้งทางพยาธิวิทยา ภายใต้ความขัดแย้งเช่นนี้จะเป็นการดีที่สุดที่จะยกข้อยกเว้น ไม่มีความรู้B, Aที่อยู่ภายใต้ข้อผูกพันที่จะเคารพไม่มีBของการดำเนินการ__ne__ตามวัตถุประสงค์ของสมมาตร เมื่อถึงจุดนั้นในตัวอย่างการAใช้งาน__ne__ไม่เกี่ยวข้องกับฉัน โปรดหากรณีที่เป็นประโยชน์และไม่ใช่ทางพยาธิวิทยาเพื่อให้ประเด็นของคุณ ฉันได้อัปเดตคำตอบเพื่อตอบสนองคุณแล้ว
Aaron Hall

กรณีการใช้งานของ SQLAlchemy ใช้สำหรับภาษาเฉพาะโดเมน หากมีใครออกแบบ DSL ดังกล่าวอาจมีคนโยนคำแนะนำทั้งหมดออกไปนอกหน้าต่าง ตัวอย่างของคุณคาดหวังว่าเครื่องบินจะบินถอยหลังครึ่งหนึ่งของเวลาและฉันคาดหวังว่าพวกเขาจะบินไปข้างหน้าเท่านั้นและฉันคิดว่านั่นเป็นการตัดสินใจออกแบบที่สมเหตุสมผล ฉันคิดว่าความกังวลที่คุณแจ้งนั้นไม่มีเหตุผลและถอยหลัง
Aaron Hall

-1

ถ้าทุก__eq__, __ne__, __lt__, __ge__, __le__และ__gt__ทำให้ความรู้สึกของชั้นเรียนแล้วก็ดำเนินการ__cmp__แทน มิฉะนั้นให้ทำตามที่คุณกำลังทำอยู่เพราะคำพูดที่ Daniel DiPaolo พูด (ในขณะที่ฉันกำลังทดสอบแทนที่จะค้นหามัน))


12
__cmp__()วิธีพิเศษไม่มีการสนับสนุนในหลาม 3.x ดังนั้นคุณควรที่จะได้รับใช้ในการใช้ประกอบการเปรียบเทียบที่อุดมไปด้วย
Don O'Donnell

8
หรืออีกทางหนึ่งถ้าคุณอยู่ใน Python 2.7 หรือ 3.x มัณฑนากร functools.total_ordering ก็มีประโยชน์เช่นกัน
Adam Parkin

ขอบคุณสำหรับหัวขึ้น. ฉันได้ตระหนักถึงหลายสิ่งหลายอย่างในช่วงปีครึ่งที่ผ่านมา ;)
Karl Knechtel
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.