Шаг 153.
Python: сборник рецептов.
Классы и объекты. Заставляем классы поддерживать операции сравнения

    На этом шаге мы рассмотрим возможности декоратора functools.total_ordering.

Задача

    Вы бы хотели сравнивать экземпляры вашего класса, используя обычные операторы сравнения (т. е. >=, !=, <= и т. д.), но без написания большого количества специальных методов.

Решение

    Классы Python могут поддерживать сравнение путем реализации специального метода для каждого из операторов сравнения. Например, чтобы обеспечить поддержку оператора >=, вам нужно определить в классах метод __ge__(). Хотя определение одного метода обычно не является проблемой, реализация методов для всех возможных операторов сравнения может быстро надоесть.

    Декоратор functools.total_ordering может помочь упростить этот процесс. Вы декорируете им класс и определяете метод __eq_(), а также один из методов сравнения (__lt_(), __le__(), __gt__() или __ge__()). Затем декоратор заполнит другие методы за вас.

    В качестве примера давайте построим несколько домов и добавим в них несколько комнат, а затем выполним сравнения:

>>> from functools import total_ordering
>>> class Room:
    def __init__(self, name, length, width):
        self.name = name
        self.length = length
        self.width = width
        self.square_feet = self.length * self.width

>>> @total_ordering
class House:
    def __init__(self, name, style):
        self.name = name
        self.style = style
        self.rooms = list()

    @property
    def living_space_footage(self):
        return sum(r.square_feet for r in self.rooms)

    def add_room(self, room):
        self.rooms.append(room)

    def __str__(self):
        return '{}: {} square foot {}'.format(self.name, 
                                              self.living_space_footage, 
                                              self.style)

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

    def __lt__(self, other):
        return self.living_space_footage < other.living_space_footage 

>>> 

    Здесь класс House был декорирован с помощью @total_ordering. Определения __eq__() и __lt_() предоставлены для сравнения домов на основе общего метража их комнат. Это минимальное определение - все, что требуется, чтобы заставить работать другие операции сравнения. Например:

>>> # Построим несколько домов и добавим в них комнаты
>>> h1 = House('h1', 'Cape')
>>> h1.add_room(Room('Master Bedroom', 14, 21))
>>> h1.add_room(Room('Living Room', 18, 20))
>>> h1.add_room(Room('Kitchen', 12, 16))
>>> h1.add_room(Room('Office', 12, 12))
>>> 
>>> h2 = House('h2', 'Ranch')
>>> h2.add_room(Room('Master Bedroom', 14, 21))
>>> h2.add_room(Room('Living Room', 18, 20))
>>> h2.add_room(Room('Kitchen', 12, 16))
>>> 
>>> h3 = House('h3', 'Split')
>>> h3.add_room(Room('Master Bedroom', 14, 21))
>>> h3.add_room(Room('Living Room', 18, 20))
>>> h3.add_room(Room('Office', 12, 16))
>>> h3.add_room(Room('Kitchen', 15, 17))
>>> houses = [h1, h2, h3]
>>> print('Is h1 bigger than h2?', h1 > h2)
Is h1 bigger than h2? True
>>> print("Is h2 smaller than h3?", h2 < h3)
Is h2 smaller than h3? True
>>> print("Is h2 greater than or equal to h1?", h2 >= h1)
Is h2 greater than  or equal to h1? False
>>> print("Which one is biggest?", max(houses))
Which one is biggest? h3: 1101 square foot Split
>>> print("Which is smallest?", min(houses))
Which is smallest? h2: 846 square foot Ranch
>>>


Обсуждение

    Если вы написали код, который заставляет класс поддерживать все основные операторы сравнения, тогда total_ordering, вероятно, уже не кажется вам такой уж магией: он буквально определяет отображение каждого метода поддержки сравнений на все другие, которые потребуются. Так что если вы определили __lt__() в вашем классе, как показано в решении, то этот метод используется для построения всех других операторов сравнения. Он просто заполняет класс методами:

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

    def __lt__(self, other):
        . . .

# Методы, созданные @total_ordering
__le__ = lambda self, other: self < other or self  ==  other
__gt__ = lambda self, other: not (self < other or self == other)
__ge__ = lambda self, other: not (self < other)
__ne__ = lambda self, other: not self == other

    Несложно, конечно, написать такие методы вручную, но @total_ordering делает все наверняка, ни о чем не забывая.

    На следующем шаге мы рассмотрим создание закешированных экземпляров.




Предыдущий шаг Содержание Следующий шаг