__lt__ แทน __cmp__


103

Python 2.x มีสองวิธีในการโอเวอร์โหลดตัวดำเนินการเปรียบเทียบ__cmp__หรือ "ตัวดำเนินการเปรียบเทียบที่สมบูรณ์" เช่น__lt__. การเปรียบเทียบที่มากเกินไปนั้นเป็นที่ต้องการ แต่เหตุใดจึงเป็นเช่นนั้น

ตัวดำเนินการเปรียบเทียบแบบ Rich นั้นง่ายกว่าในการใช้งานแต่ละตัว แต่คุณต้องใช้ตัวดำเนินการหลายตัวด้วยตรรกะที่เหมือนกัน อย่างไรก็ตามหากคุณสามารถใช้การcmpสั่งซื้อในตัวและทูเพิล__cmp__ได้ก็จะค่อนข้างง่ายและตอบสนองการเปรียบเทียบทั้งหมด:

class A(object):
  def __init__(self, name, age, other):
    self.name = name
    self.age = age
    self.other = other
  def __cmp__(self, other):
    assert isinstance(other, A) # assumption for this example
    return cmp((self.name, self.age, self.other),
               (other.name, other.age, other.other))

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

มีข้อผิดพลาดที่ไม่คาดฝันใด ๆ ที่ฉันต้องระวังหากฉันทำงานหนักเกินไป__cmp__หรือไม่?

ฉันเข้าใจ<, <=, ==ฯลฯ ประกอบการสามารถจะมากเกินไปเพื่อวัตถุประสงค์อื่น ๆ และสามารถกลับวัตถุใด ๆ ที่พวกเขาต้องการ ฉันไม่ได้ถามถึงข้อดีของแนวทางนั้น แต่เกี่ยวกับความแตกต่างเมื่อใช้ตัวดำเนินการเหล่านี้เพื่อการเปรียบเทียบในแง่เดียวกับที่พวกเขาหมายถึงตัวเลข

อัปเดต:ตามที่คริสโตเฟอร์ชี้ให้เห็นว่าcmpหายไปใน 3.x มีทางเลือกอื่นที่ทำให้การเปรียบเทียบใช้งานง่ายเหมือนข้างต้น__cmp__หรือไม่?


5
ดูคำตอบของฉันเป็นคำถามสุดท้ายของคุณ แต่จริงๆแล้วมีการออกแบบที่จะทำให้สิ่งต่าง ๆ ง่ายยิ่งขึ้นสำหรับหลาย ๆ คลาสรวมถึงของคุณ (ตอนนี้คุณต้องมี mixin, metaclass หรือ class decorator เพื่อนำไปใช้): หากมีวิธีพิเศษที่สำคัญอยู่ ต้องส่งคืนค่าทูเพิลและตัวเปรียบเทียบและแฮชทั้งหมดถูกกำหนดในรูปของทูเพิลนั้น Guido ชอบความคิดของฉันเมื่อฉันอธิบายให้เขาฟัง แต่แล้วฉันก็ยุ่งกับเรื่องอื่น ๆ และไม่เคยเขียน PEP เลย ... อาจจะเป็น 3.2 ;-) ในขณะเดียวกันฉันก็ใช้มิกซ์อินของฉันต่อไป! -)
Alex Martelli

คำตอบ:


93

ใช่มันเป็นเรื่องง่ายที่จะใช้ทุกอย่างในแง่ของเช่น__lt__คลาสมิกซ์อิน (หรือเมตาคลาสหรือมัณฑนากรชั้นเรียนถ้ารสนิยมของคุณเป็นแบบนั้น)

ตัวอย่างเช่น:

class ComparableMixin:
  def __eq__(self, other):
    return not self<other and not other<self
  def __ne__(self, other):
    return self<other or other<self
  def __gt__(self, other):
    return other<self
  def __ge__(self, other):
    return not self<other
  def __le__(self, other):
    return not other<self

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

แน่นอนว่าหากชั้นเรียนของคุณมีวิธีที่รวดเร็วเป็นพิเศษในการนำไปใช้ (เช่น) __eq__และ__ne__ควรกำหนดโดยตรงเพื่อไม่ให้ใช้เวอร์ชันของ mixin (เช่นในกรณีนี้dict) - ในความเป็นจริง__ne__อาจถูกกำหนดไว้เพื่ออำนวยความสะดวก นั่นคือ:

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

แต่ในโค้ดด้านบนฉันต้องการคงความสมมาตรที่น่าพอใจไว้ใช้เท่านั้น<;-) ว่าทำไม__cmp__ต้องไปเนื่องจากเราไม่ได้มี__lt__และเพื่อนทำไมให้อีกวิธีที่แตกต่างกันที่จะทำสิ่งเดียวกันรอบ? มันมีน้ำหนักมากในทุกรันไทม์ของ Python (Classic, Jython, IronPython, PyPy, ... ) รหัสที่ไม่มีจุดบกพร่องอย่างแน่นอนคือรหัสที่ไม่มีอยู่ - โดยหลักการของ Python ที่ว่าควรมีวิธีหนึ่งที่ชัดเจนในการทำงาน (C มีหลักการเดียวกันในส่วน "Spirit of C" ของ มาตรฐาน ISO, btw)

นี้ไม่ได้หมายความว่าเราออกไปจากทางของเราที่จะห้ามสิ่ง (เช่นใกล้ความเท่าเทียมระหว่าง mixins และตกแต่งชั้นสำหรับการใช้งานบางส่วน) แต่แน่นอนมันไม่ได้หมายความว่าเราไม่ชอบการดำเนินการประมาณรหัสในการคอมไพเลอร์และ / หรือรันไทม์ที่ซ้ำซ้อนเพียงเพื่อสนับสนุนวิธีการที่เทียบเท่ากันหลายวิธีในการทำงานเดียวกัน

แก้ไขเพิ่มเติม: มีวิธีที่ดีกว่าในการเปรียบเทียบและแฮชสำหรับหลายชั้นเรียนรวมถึงในคำถาม - __key__วิธีการที่ฉันพูดถึงในความคิดเห็นของฉันต่อคำถาม เนื่องจากฉันไม่เคยเขียน PEP มาก่อนตอนนี้คุณต้องใช้มันด้วย Mixin (& c) ถ้าคุณชอบ:

class KeyedMixin:
  def __lt__(self, other):
    return self.__key__() < other.__key__()
  # and so on for other comparators, as above, plus:
  def __hash__(self):
    return hash(self.__key__())

เป็นกรณีที่พบบ่อยมากสำหรับการเปรียบเทียบของอินสแตนซ์กับอินสแตนซ์อื่น ๆ เพื่อเปรียบเทียบกับทูเพิลสำหรับแต่ละฟิลด์ที่มีฟิลด์ไม่กี่ฟิลด์จากนั้นการแฮชควรจะดำเนินการบนพื้นฐานเดียวกัน __key__อยู่วิธีการพิเศษที่ต้องการโดยตรง


ขออภัยในความล่าช้า @R. Pate ฉันตัดสินใจว่าเนื่องจากฉันต้องแก้ไขต่อไปฉันควรให้คำตอบที่ละเอียดถี่ถ้วนที่สุดเท่าที่จะทำได้แทนที่จะรีบร้อน (และฉันเพิ่งแก้ไขอีกครั้งเพื่อแนะนำแนวคิดสำคัญเก่าของฉันซึ่งฉันไม่เคยได้เจอกับ PEPping รวมทั้งวิธีการ เพื่อนำไปใช้กับ mixin)
Alex Martelli

ฉันชอบแนวคิดหลักนั้นมากจะนำไปใช้และดูว่ารู้สึกอย่างไร (แม้ว่าจะตั้งชื่อว่า cmp_key หรือ _cmp_key แทนชื่อสงวน)

TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixinเมื่อฉันลองสิ่งนี้ใน Python 3 ดูโค้ดเต็มได้ที่gist.github.com/2696496
Adam Parkin

2
ใน Python 2.7 + / 3.2 + คุณสามารถใช้functools.total_orderingแทนการสร้างของคุณเองComparableMiximได้ ตามที่แนะนำในคำตอบของ jmagnusson
วันที่

4
การใช้<เพื่อใช้งาน__eq__ใน Python 3 เป็นความคิดที่ไม่ดีเลยเพราะTypeError: unorderable types.
Antti Haapala

50

เพื่อให้กรณีนี้ง่ายขึ้นมีมัณฑนากรคลาสใน Python 2.7 + / 3.2 + ซึ่งเป็นfunctools.total_orderingซึ่งสามารถใช้เพื่อนำสิ่งที่ Alex แนะนำไปใช้ ตัวอย่างจากเอกสาร:

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

9
total_orderingไม่ได้ใช้งาน__ne__ดังนั้นระวัง!
Flimm

3
@Flimm ไม่ได้ แต่__ne__. แต่นั่นเป็นเพราะมีการเริ่มต้นใช้งานที่ได้รับมอบหมายให้__ne__ __eq__ดังนั้นจึงไม่มีอะไรต้องระวังที่นี่
ม.ค. Hudec

ต้องกำหนดการดำเนินการสั่งซื้ออย่างน้อยหนึ่งรายการ: <> <=> = .... eq ไม่จำเป็นต้องใช้เป็นคำสั่งรวมถ้า a <b และ b <a แล้ว a = b
Xanlantos

9

สิ่งนี้ครอบคลุมโดยPEP 207 - Rich Comparisons

นอกจากนี้ยัง__cmp__หายไปใน python 3.0 (โปรดทราบว่าไม่มีอยู่ในhttp://docs.python.org/3.0/reference/datamodel.htmlแต่อยู่ในhttp://docs.python.org/2.7/reference/datamodel.html )


PEP เกี่ยวข้องเฉพาะกับเหตุใดจึงจำเป็นต้องมีการเปรียบเทียบที่หลากหลายในแบบที่ผู้ใช้ NumPy ต้องการให้ A <B ส่งคืนลำดับ

ฉันไม่รู้ว่ามันจะจากไปอย่างแน่นอนสิ่งนี้ทำให้ฉันเสียใจ (แต่ขอบคุณที่ชี้ให้เห็น)

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

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

1
@ ร. Pate ในขณะที่ฉันพยายามอธิบายในคำตอบของฉันไม่มีการสูญเสียที่แท้จริงโดยทั่วไป (เนื่องจากเครื่องผสม, มัณฑนากรหรือ metaclass ช่วยให้คุณสามารถกำหนดทุกอย่างได้อย่างง่ายดายเพียงแค่ <ถ้าคุณต้องการ) และเพื่อให้การใช้งาน Python ทั้งหมดดำเนินไปได้ โค้ดที่ซ้ำซ้อนตกกลับไปที่cmpตลอดไป - เพียงเพื่อให้ผู้ใช้ Python แสดงสิ่งต่าง ๆ ในสองวิธีที่เทียบเท่ากัน - จะทำงาน 100% เมื่อเทียบกับเกรนของ Python
Alex Martelli

3

(แก้ไขเมื่อวันที่ 17/17 เพื่อนำความคิดเห็นมาพิจารณา)

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


class ComparableMixin(object):

    def __eq__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other and not other<self

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

    def __gt__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return other<self

    def __ge__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other

    def __le__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not other<self    

คุณคิดว่านั่นselfอาจเป็นซิงเกิลตันNoneของคุณNoneTypeและในเวลาเดียวกันได้ComparableMixinอย่างไร? และแน่นอนว่าสูตรนี้ไม่ดีสำหรับ Python 3
Antti Haapala

3
selfจะไม่มีทางเป็นไปได้Noneดังนั้นสาขานั้นจึงไปได้ทั้งหมด อย่าใช้type(other) == type(None); other is Noneเพียงแค่ใช้ แทนที่จะพิเศษปลอกNoneทดสอบถ้าชนิดอื่นเป็นตัวอย่างของประเภทของselfและกลับเดี่ยวหากไม่ได้:NotImplemented if not isinstance(other, type(self)): return NotImplementedทำเช่นนี้กับทุกวิธี จากนั้น Python จะให้โอกาสตัวถูกดำเนินการอื่นเพื่อให้คำตอบแทน
Martijn Pieters

1

แรงบันดาลใจจากคำตอบComparableMixinและKeyedMixinคำตอบของ Alex Martelli ฉันจึงได้มิกซ์อินดังต่อไปนี้ จะช่วยให้คุณใช้เพียงครั้งเดียว_compare_to()วิธีการที่ใช้เปรียบเทียบที่สำคัญตามที่คล้ายกับแต่ช่วยให้การเรียนของคุณที่จะเลือกที่สำคัญการเปรียบเทียบประสิทธิภาพมากที่สุดขึ้นอยู่กับชนิดของKeyedMixin other(โปรดทราบว่ามิกซ์อินนี้ไม่ได้ช่วยอะไรมากสำหรับอ็อบเจกต์ที่สามารถทดสอบความเท่าเทียมกันได้ แต่ไม่ใช่ลำดับ)

class ComparableMixin(object):
    """mixin which implements rich comparison operators in terms of a single _compare_to() helper"""

    def _compare_to(self, other):
        """return keys to compare self to other.

        if self and other are comparable, this function 
        should return ``(self key, other key)``.
        if they aren't, it should return ``None`` instead.
        """
        raise NotImplementedError("_compare_to() must be implemented by subclass")

    def __eq__(self, other):
        keys = self._compare_to(other)
        return keys[0] == keys[1] if keys else NotImplemented

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

    def __lt__(self, other):
        keys = self._compare_to(other)
        return keys[0] < keys[1] if keys else NotImplemented

    def __le__(self, other):
        keys = self._compare_to(other)
        return keys[0] <= keys[1] if keys else NotImplemented

    def __gt__(self, other):
        keys = self._compare_to(other)
        return keys[0] > keys[1] if keys else NotImplemented

    def __ge__(self, other):
        keys = self._compare_to(other)
        return keys[0] >= keys[1] if keys else NotImplemented
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.