Истинность, сравнение и кеширование объектов

⏱ 30 мин · 🧩 заданий: 5

Истинность: bool и len

if obj: вызывает bool(obj), который ищет:

  1. obj.__bool__() — если определён, его результат;
  2. иначе obj.__len__() — ложно, если длина 0;
  3. иначе объект истинен.
class Account:
    def __init__(self, balance):
        self.balance = balance
    def __bool__(self):
        return self.balance > 0

class Playlist:
    def __init__(self, songs):
        self.songs = songs
    def __len__(self):
        return len(self.songs)

class Plain:
    pass

print(bool(Account(0)), bool(Account(50)))
print(bool(Playlist([])), bool(Playlist(["песня"])))
print(bool(Plain()))

Протокол сравнения

a == b вызывает a.__eq__(b). Если он вернул NotImplemented, пробуется b.__eq__(a); если и он не знает — сравнение по идентичности (a is b).

a < b вызывает a.__lt__(b); при NotImplemented — отражённый b.__gt__(a); если и он не знает — TypeError.

Оператор Метод Отражённый
== / != __eq__ / __ne__ __eq__ / __ne__
< / > __lt__ / __gt__ __gt__ / __lt__
<= / >= __le__ / __ge__ __ge__ / __le__
class Celsius:
    def __init__(self, deg):
        self.deg = deg
    def __eq__(self, other):
        if isinstance(other, Celsius):
            return self.deg == other.deg
        return NotImplemented
    def __lt__(self, other):
        if isinstance(other, Celsius):
            return self.deg < other.deg
        return NotImplemented

print(Celsius(20) == Celsius(20), Celsius(5) < Celsius(10), Celsius(30) > Celsius(10))
print(Celsius(20) == 20)                  # NotImplemented с обеих сторон → идентичность → False
try:
    Celsius(1) < 5
except TypeError as e:
    print("TypeError:", e)

__ne__ по умолчанию инвертирует __eq__. @functools.total_ordering достраивает недостающие <=, >, >= по __eq__ и одному из __lt__/__gt__. Ещё проще — @dataclass(order=True).

Сравнение встроенных типов

print([1, 2, 3] < [1, 3])          # поэлементно, лексикографически
print("Apple" < "apple")           # по кодам символов: заглавные раньше
print((2, "a") < (2, "b"))
print({1, 2} < {1, 2, 3})          # для множеств < — «строгое подмножество»
print(1 == 1.0, 1 == True, 0.1 + 0.2 == 0.3)
nan = float("nan")
print(nan == nan, nan != nan, nan is nan)
try:
    print(1 < "2")
except TypeError as e:
    print("TypeError:", e)

В Python 3 упорядочивание разных типов (кроме чисел между собой) запрещено.

Цепочки сравнений

x = 5
print(1 < x < 10)          # 1 < x and x < 10
print(1 < x > 3)           # 1 < x and x > 3 — законно, но читается плохо
print(False == False in [False])   # False == False and False in [False] → True
print((False == False) in [False]) # True in [False] → False

Неожиданные цепочки

a == b in c — это цепочка a == b and b in c, а не (a == b) in c. Отсюда классическая загадка False == False in [False] → True.

Кеширование объектов и is

CPython переиспользует некоторые объекты. Это деталь реализации — но о ней спрашивают:

a, b = 256, 256
print(a is b)               # True: числа -5..256 созданы заранее

c = int("257")
d = int("257")
print(c is d)               # False: обычные объекты

e = 257; f = 257
print(e is f)               # True! константы в одном блоке кода — один объект

s1 = "hello"
s2 = "hello"
print(s1 is s2)             # True: строки-идентификаторы интернируются

s3 = "hello world!"
s4 = "hello " + "world!"    # константное выражение свёрнуто компилятором
print(s3 is s4)             # True в одном модуле
s5 = "".join(["hello ", "world!"])
print(s3 is s5)             # False: создано во время выполнения

Механизмы:

  • кеш малых целых −5…256;
  • интернирование строк, похожих на идентификаторы, и имён (sys.intern — вручную);
  • константы блока кода: одинаковые литералы в одной функции/модуле — один объект;
  • свёртка констант компилятором: "a" + "b" → "ab" на этапе компиляции;
  • синглтоны: None, True, False, Ellipsis, NotImplemented, пустой кортеж ().

Ответ на собеседовании

«Почему a is b для 256 даёт True, а для 257 — False?» — CPython кеширует малые целые −5…256, и все ссылки указывают на один объект; большие числа создаются заново. Но результат может зависеть от контекста (константы в одном блоке кода), поэтому для сравнения значений всегда используют ==.

Итог

  • bool(obj): __bool__ → __len__ → истина.
  • Сравнения вызывают __eq__, __lt__ и т. д.; NotImplemented — «не умею», Python пробует второй операнд.
  • total_ordering или dataclass(order=True) достраивают остальные сравнения.
  • Цепочки a < b < c разворачиваются через and; [] == False — ложь, хотя [] ложен.
  • Кеш малых чисел, интернирование и константы делают is непредсказуемым — сравнивайте значения через ==.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Сделайте так, чтобы объект Inbox был ложным, если в нём нет писем, и истинным — если есть. Используйте __len__ (тогда len(inbox) тоже заработает).

Какие тесты проверяют код (2)
box = Inbox(); assert not box, "Пустой ящик должен быть ложным"
box = Inbox(); box.add("привет"); assert box and len(box) == 1
Задание 2 Программа на Python

Сделайте объекты Version сравнимыми: версия "1.10.0" больше "1.9.3" (сравнение по числам, а не строкам). Реализуйте __eq__ и __lt__ и примените functools.total_ordering, чтобы заработали <=, >, >=.

Какие тесты проверяют код (4)
assert Version("1.10.0") > Version("1.9.3")
assert Version("2.0") >= Version("2.0") and Version("1.0") <= Version("1.0.1")
assert sorted([Version("1.2"), Version("1.10"), Version("1.1")])[-1].parts == (1, 10)
assert Version("1.0") == Version("1.0") and Version("1.0") != Version("1.1")
Задание 3 Заполнить таблицу

Что выведет каждое выражение?

TrueFalseTypeError
[] == False
bool([]) == False
1 == 1.0 == True
(1, 2) < (1, 3)
[1] < "a"
None == 0
float("nan") == float("nan")
Отметьте ячейку — «+», пустая — «−».
Задание 4 Один вариант ответа

Что выведет код?

print(True + True + True)
Задание 5 Один вариант ответа

Для чего метод сравнения возвращает NotImplemented?

Войдите, чтобы отмечать пройденные уроки.