Истинность, сравнение и кеширование объектов
Истинность: bool и len
if obj: вызывает bool(obj), который ищет:
obj.__bool__()— если определён, его результат;- иначе
obj.__len__()— ложно, если длина 0; - иначе объект истинен.
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
Сделайте так, чтобы объект Inbox был ложным, если в нём нет писем, и истинным — если есть.
Используйте __len__ (тогда len(inbox) тоже заработает).
Какие тесты проверяют код (2)
box = Inbox(); assert not box, "Пустой ящик должен быть ложным"
box = Inbox(); box.add("привет"); assert box and len(box) == 1
class Inbox:
def __init__(self):
self.messages = []
def add(self, msg):
self.messages.append(msg)
def __len__(self):
return len(self.messages)
Сделайте объекты 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")
import functools
@functools.total_ordering
class Version:
def __init__(self, text):
self.parts = tuple(int(p) for p in text.split("."))
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self.parts == other.parts
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self.parts < other.parts
Что выведет каждое выражение?
| True | False | TypeError | |
|---|---|---|---|
[] == False |
|||
bool([]) == False |
|||
1 == 1.0 == True |
|||
(1, 2) < (1, 3) |
|||
[1] < "a" |
|||
None == 0 |
|||
float("nan") == float("nan") |
Что выведет код?
print(True + True + True)
Для чего метод сравнения возвращает NotImplemented?
Войдите, чтобы отмечать пройденные уроки.