__lt__ bukan __cmp__

100

Python 2.x memiliki dua cara untuk membebani operator perbandingan, __cmp__atau "operator perbandingan kaya" seperti __lt__. Perbandingan yang kaya kelebihan dikatakan lebih disukai, tetapi mengapa demikian?

Operator perbandingan kaya lebih sederhana untuk menerapkan masing-masing, tetapi Anda harus mengimplementasikan beberapa di antaranya dengan logika yang hampir identik. Namun, jika Anda dapat menggunakan cmppengurutan builtin dan tuple, maka __cmp__cukup sederhana dan memenuhi semua perbandingan:

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))

Kesederhanaan ini tampaknya memenuhi kebutuhan saya jauh lebih baik daripada membebani semua 6 (!) Dari perbandingan yang kaya. (Namun, Anda dapat menurunkannya menjadi "hanya" 4 jika Anda mengandalkan "argumen yang ditukar" / perilaku yang tercermin, tetapi itu menghasilkan peningkatan bersih komplikasi, menurut pendapat saya.)

Apakah ada jebakan tak terduga yang perlu saya waspadai jika saya hanya kelebihan beban __cmp__ ?

Saya memahami <, <=,== , dll operator dapat kelebihan beban untuk tujuan lain, dan dapat kembali setiap objek yang mereka suka. Saya tidak bertanya tentang manfaat pendekatan itu, tetapi hanya tentang perbedaan ketika menggunakan operator ini untuk perbandingan dalam arti yang sama yang mereka maksudkan untuk angka.

Pembaruan: Seperti yang ditunjukkan Christopher , cmpmenghilang di 3.x. Apakah ada alternatif lain yang membuat perbandingan penerapan semudah di atas __cmp__?

Komunitas
sumber
5
Lihat jawaban saya untuk pertanyaan terakhir Anda, tetapi sebenarnya ada desain yang akan membuat segalanya lebih mudah untuk banyak kelas termasuk kelas Anda (saat ini Anda memerlukan mixin, metaclass, atau dekorator kelas untuk menerapkannya): jika ada metode khusus utama , itu harus mengembalikan tupel nilai, dan semua pembanding DAN hash didefinisikan dalam tupel itu. Guido menyukai ide saya ketika saya menjelaskannya kepadanya, tetapi kemudian saya sibuk dengan hal-hal lain dan tidak sempat menulis PEP ... mungkin untuk 3,2 ;-). Sementara itu saya tetap menggunakan mixin saya untuk itu! -)
Alex Martelli

Jawaban:

90

Ya, mudah untuk menerapkan semuanya dalam hal misalnya __lt__dengan kelas mixin (atau metaclass, atau dekorator kelas jika selera Anda seperti itu).

Sebagai contoh:

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

Sekarang kelas Anda dapat mendefinisikan just __lt__dan multiply inherit dari ComparableMixin (setelah basis lain apa pun yang dibutuhkan, jika ada). Dekorator kelas akan sangat mirip, hanya dengan memasukkan fungsi yang serupa sebagai atribut kelas baru yang didekorasi (hasilnya mungkin lebih cepat secara mikroskopis saat runtime, dengan biaya yang sama menitnya dalam hal memori).

Tentu saja, jika kelas Anda memiliki cara yang sangat cepat untuk diimplementasikan (misalnya) __eq__dan __ne__, itu harus mendefinisikannya secara langsung sehingga versi mixin tidak digunakan (misalnya, untuk kasus dict) - sebenarnya __ne__mungkin juga didefinisikan untuk memfasilitasi itu sebagai:

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

tetapi pada kode di atas saya ingin menjaga simetri yang menyenangkan dengan hanya menggunakan <;-). Mengenai mengapa __cmp__harus pergi, karena kita memang punya __lt__dan teman, mengapa menyimpan cara lain yang berbeda untuk melakukan hal yang persis sama? Itu terlalu banyak bobot mati di setiap runtime Python (Klasik, Jython, IronPython, PyPy, ...). Kode yang pasti tidak memiliki bug adalah kode yang tidak ada - dari mana prinsip Python bahwa seharusnya ada satu cara yang ideal untuk melakukan tugas (C memiliki prinsip yang sama di bagian "Spirit of C" dari standar ISO, btw).

Ini tidak berarti kita pergi keluar dari cara kami untuk melarang hal-hal (misalnya, dekat-kesetaraan antara mixin dan dekorator kelas untuk beberapa penggunaan), tapi pasti tidak berarti bahwa kita tidak suka untuk membawa sekitar kode dalam compiler dan / atau runtime yang ada secara mubazir hanya untuk mendukung beberapa pendekatan yang setara untuk melakukan tugas yang persis sama.

Pengeditan lebih lanjut: sebenarnya ada cara yang lebih baik untuk memberikan perbandingan DAN hashing untuk banyak kelas, termasuk dalam pertanyaan - __key__metode, seperti yang saya sebutkan di komentar saya untuk pertanyaan tersebut. Karena saya tidak pernah sempat menulis PEP untuk itu, Anda saat ini harus mengimplementasikannya dengan Mixin (& c) jika Anda suka:

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__())

Ini adalah kasus yang sangat umum untuk perbandingan sebuah instance dengan instance lain yang bermuara pada perbandingan tupel untuk masing-masing dengan beberapa bidang - dan kemudian, hashing harus diterapkan pada dasar yang sama persis. The __key__alamat metode khusus yang perlu langsung.

Alex Martelli
sumber
Maaf atas keterlambatan @R. Pate, saya memutuskan bahwa karena saya harus mengedit, saya harus memberikan jawaban yang paling menyeluruh yang saya bisa daripada terburu-buru (dan saya baru saja mengedit lagi untuk menyarankan ide kunci lama saya yang tidak pernah saya dapatkan ke PEPping, serta bagaimana caranya untuk mengimplementasikannya dengan mixin).
Alex Martelli
Saya sangat menyukai ide kunci itu, akan menggunakannya dan melihat bagaimana rasanya. (Meskipun bernama cmp_key atau _cmp_key alih-alih nama yang dicadangkan.)
TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixinketika saya mencoba ini dengan Python 3. Lihat kode lengkapnya di gist.github.com/2696496
Adam Parkin
2
Di Python 2.7 + / 3.2 + Anda dapat menggunakan functools.total_orderingalih-alih membangunnya sendiri ComparableMixim. Seperti yang disarankan dalam jawaban jmagnusson
Hari
4
Menggunakan <untuk mengimplementasikan __eq__dalam Python 3 adalah ide yang sangat buruk, karena TypeError: unorderable types.
Antti Haapala
49

Untuk menyederhanakan kasus ini, ada dekorator kelas dengan Python 2.7 + / 3.2 +, functools.total_ordering , yang dapat digunakan untuk mengimplementasikan apa yang disarankan Alex. Contoh dari dokumen:

@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()))
jmagnusson.dll
sumber
9
total_orderingtidak diimplementasikan __ne__, jadi hati-hati!
Flimm
3
@Flimm, tidak, tapi __ne__. tapi itu karena __ne__memiliki implementasi default yang didelegasikan __eq__. Jadi tidak ada yang perlu diperhatikan di sini.
Jan Hudec
harus menentukan setidaknya satu operasi pemesanan: <> <=> = .... eq tidak diperlukan sebagai pesanan total jika! a <b dan b <a lalu a = b
Xanlantos
9

Ini tercakup dalam PEP 207 - Rich Comparisons

Juga, __cmp__hilang di python 3.0. (Perhatikan bahwa ini tidak ada di http://docs.python.org/3.0/reference/datamodel.html tetapi IS di http://docs.python.org/2.7/reference/datamodel.html )

Christopher
sumber
PEP hanya mementingkan mengapa perbandingan yang kaya diperlukan, dengan cara pengguna NumPy ingin A <B mengembalikan urutan.
Saya tidak menyadari itu pasti akan pergi, ini membuat saya sedih. (Tapi terima kasih telah menunjukkannya.)
PEP juga membahas "mengapa" mereka lebih disukai. Pada dasarnya ini bermuara pada efisiensi: 1. Tidak perlu mengimplementasikan operasi yang tidak masuk akal untuk objek Anda (seperti koleksi yang tidak berurutan.) 2. Beberapa koleksi memiliki operasi yang sangat efisien pada beberapa jenis perbandingan. Perbandingan yang kaya memungkinkan penerjemah memanfaatkannya jika Anda mendefinisikannya.
Christopher
1
Re 1, Jika tidak masuk akal, jangan terapkan cmp . Re 2, memiliki kedua opsi dapat memungkinkan Anda mengoptimalkan sesuai kebutuhan, sambil tetap membuat prototipe dan pengujian dengan cepat. Tidak ada yang memberi tahu saya mengapa itu dihapus. (Pada dasarnya ini bermuara pada efisiensi pengembang bagi saya.) Mungkinkah perbandingan yang kaya kurang efisien dengan penggantian cmp yang diterapkan? Itu tidak masuk akal bagiku.
1
@R. Pate, ketika saya mencoba menjelaskan dalam jawaban saya, tidak ada kerugian nyata dalam hal umum (karena mixin, dekorator, atau metaclass, memungkinkan Anda dengan mudah mendefinisikan semuanya dalam istilah hanya <jika Anda mau) dan untuk semua implementasi Python untuk dibawa-bawa kode redundan yang jatuh kembali ke cmp selamanya - hanya untuk membiarkan pengguna Python mengekspresikan hal-hal dengan dua cara yang setara - akan berjalan 100% melawan butir Python.
Alex Martelli
2

(Diedit 17/6/17 untuk mempertimbangkan komentar.)

Saya mencoba jawaban mixin yang sebanding di atas. Saya mengalami masalah dengan "Tidak ada". Berikut adalah versi modifikasi yang menangani perbandingan kesetaraan dengan "Tidak Ada". (Saya tidak melihat alasan untuk mempermasalahkan perbandingan ketidaksetaraan dengan Tidak Ada yang kurang semantik):


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    
Gabriel Ferrer
sumber
Bagaimana Anda berpikir bahwa selfbisa menjadi tunggal Nonedari NoneTypedan pada saat yang sama menerapkan Anda ComparableMixin? Dan memang resep ini buruk untuk Python 3.
Antti Haapala
3
selfakan tidak pernah menjadi None, sehingga cabang dapat pergi sepenuhnya. Jangan gunakan type(other) == type(None); cukup gunakan other is None. Daripada khusus casing None, tes jika jenis lainnya adalah turunan dari jenis self, dan mengembalikan NotImplementedtunggal jika tidak: if not isinstance(other, type(self)): return NotImplemented. Lakukan ini untuk semua metode. Python kemudian akan memberi operan lain kesempatan untuk memberikan jawaban.
Martijn Pieters
1

Terinspirasi oleh Alex Martelli ComparableMixin& KeyedMixinAnswers, saya datang dengan mixin berikut. Ini memungkinkan Anda untuk menerapkan _compare_to()metode tunggal , yang menggunakan perbandingan berbasis kunci yang mirip dengan KeyedMixin, tetapi memungkinkan kelas Anda untuk memilih kunci perbandingan yang paling efisien berdasarkan jenisnya other. (Perhatikan bahwa mixin ini tidak banyak membantu untuk objek yang dapat diuji kesetaraannya tetapi tidak untuk urutannya).

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
Eli Collins
sumber