Булевы значения, None и truthy/falsy

В Python условия принимают на вход не только True/False, но и любой объект - и считают его «истинным» или «ложным» по правилам типа. Эта концепция называется truthy/falsy и сильно влияет на то, как пишут условные конструкции. Параллельно разберём None, оператор is и типичные ошибки с проверками.

bool: True, False и арифметика

bool имеет два значения: True и False. Технически это подкласс int:

>>> True + 1     # 2
>>> True == 1    # True
>>> False == 0   # True
>>> isinstance(True, int) # True

Это иногда удивляет: sum([True, False, True]) вернёт 2. Можно даже использовать как трюк для подсчёта True в списке: sum(item > 0 for item in items).

Базовые операции:

True and False   # False - логическое И
True or False    # True  - логическое ИЛИ
not True         # False - инвертор
True ^ False     # True  - XOR (только один True)
В Python `and`/`or` возвращают **один из операндов**, а не True/False:
>>> 0 or "default"
'default'
>>> "hello" or "default"
'hello'
>>> 5 and 0
0
>>> 5 and 10
10

Это short-circuit evaluation: and возвращает первый falsy или последний, or - первый truthy или последний. Удобно для значений по умолчанию.

Truthy и falsy

В условиях if/while Python приводит объект к bool через метод __bool__() (или __len__() если первого нет) - эти dunder-методы можно определить и в своём классе. Объект считается falsy, если эти методы возвращают False/0. Иначе - truthy.

Стандартно falsy:

None
False
0, 0.0, 0j           # любой числовой ноль
""                    # пустая строка
b""                   # пустые bytes
[]                    # пустой список
()                    # пустой кортеж
{}                    # пустой dict
set()                 # пустой set
range(0)              # пустой range

Всё остальное - truthy:

if "hello":     print("yes")  # yes
if [0]:         print("yes")  # yes - в списке один элемент
if " ":         print("yes")  # yes - пробел не пустой
if -1:          print("yes")  # yes - не ноль

Это позволяет писать идиоматично:

# Проверка что список не пустой
if items:
    process(items)

# Вместо
if len(items) > 0:
    process(items)

Второй вариант технически правильный, но первый - идиоматичный Python. Линтеры обычно настаивают на первом стиле.

Подводный камень: 0 и "" falsy

Иногда нужно различать «значение отсутствует» и «значение есть, но 0/пустая строка»:

# Плохо: 0 будет считаться отсутствием
def greet(name=""):
    if name:
        print(f"Привет, {name}!")
    else:
        print("Привет, аноним!")

greet("")  # «аноним», но пустая строка может быть валидным значением

# Лучше: явная проверка на None
def greet(name=None):
    if name is None:
        print("Привет, аноним!")
    else:
        print(f"Привет, {name}!")

greet("")  # «Привет, !»

В API часто различают:

  • None - значение не задано
  • "" - значение задано пустой строкой
  • 0 - значение задано нулём

Используй is None для проверки именно отсутствия, а не truthy/falsy.

None - отсутствие значения

None это singleton-объект типа NoneType. Используется как:

  • Значение, которое функция возвращает по умолчанию (если нет return или return без значения)
  • Опциональный параметр функции (def f(x=None):)
  • Признак «значения нет» в данных (типа NULL в БД)
def lookup(key, items):
    for k, v in items:
        if k == key:
            return v
    return None  # явно или неявно

result = lookup("nokey", [...])
if result is None:
    print("Не найдено")

None единственный экземпляр в программе - сравнивать через is/is not правильнее чем через ==:

# Правильно
if x is None:
    ...
if x is not None:
    ...

# Неправильно (хотя обычно работает)
if x == None:
    ...
- `==` сравнивает **значения** (вызывает `__eq__`) - `is` сравнивает **идентичность** (тот же объект в памяти)
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b    # True - значения одинаковы
>>> a is b    # False - разные объекты
>>> c = a
>>> a is c    # True - тот же объект

Для None, True, False, маленьких int (-5..256) is обычно работает как == за счёт интернирования, но не полагайся на это. Только None гарантированно один в программе.

Идиомы условий

Проверка списка/коллекции:

# Хорошо
if not items:
    return

# Можно, но менее идиоматично
if len(items) == 0:
    return

Проверка опционального параметра:

def f(arg=None):
    if arg is None:
        arg = []  # значение по умолчанию

Mutable default argument - классическая ловушка (почему так происходит - в уроке про функции и их параметры по умолчанию):

# ПЛОХО - список создаётся один раз при определении функции
def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2] - не пустой!
print(add_item(3))  # [1, 2, 3]

# ХОРОШО
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

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

if 0 <= age <= 150:
    print("Валидный возраст")

Python поддерживает математическую нотацию для цепочек. Это эквивалентно 0 <= age and age <= 150, но читается приятнее.

Операторы сравнения

Стандартные:

==    # равенство (по значению)
!=    # неравенство
<     # меньше
<=    # меньше или равно
>     # больше
>=    # больше или равно
is    # идентичность
is not # не идентичность
in    # вхождение в коллекцию
not in # не входит
>>> "abc" in "xabcy"  # True
>>> 3 in [1, 2, 3]     # True
>>> "key" in {"key": 1}  # True - проверяет ключи dict

>>> 1 < 2 < 3 < 4      # True
>>> 1 < 2 > 0.5        # True
>>> 5 != 3 != 5        # True - не «5 != 5», а цепочка

NaN - не равен сам себе

Особый случай с float NaN:

>>> import math
>>> nan = math.nan
>>> nan == nan   # False! - по стандарту IEEE 754
>>> nan != nan   # True
>>> math.isnan(nan)  # правильная проверка

Это специально - NaN означает «неопределённое значение», и сравнение с самим собой не имеет смысла. Остальные подвохи плавающей точки разбирали в уроке про числовые типы. Если работаешь с float (или pandas DataFrame), не сравнивай через ==, а используй math.isnan() или pandas.isna().

any() и all()

Полезные встроенные функции для проверки коллекций:

items = [1, 2, 3, 0]

any(items)   # True - хотя бы один truthy
all(items)   # False - 0 falsy
any(x > 5 for x in items)  # False
all(x >= 0 for x in items) # True

Работают с любыми итераторами и поддерживают short-circuit - останавливаются как только результат определён.

Мини-задание

  1. Проверь truthy/falsy для разных значений:
values = [None, 0, 0.0, "", [], {}, set(), False, 1, "a", [0], {"k": None}, " "]
for v in values:
    print(f"{v!r:<20} -> {bool(v)}")
  1. Реализуй функцию которая различает «пусто» от «не задано»:
def describe(value=None):
    if value is None:
        return "значение не передано"
    if not value:
        return "значение пустое"
    return f"значение: {value!r}"

print(describe())          # не передано
print(describe(""))        # пустое
print(describe([]))        # пустое
print(describe(0))         # пустое
print(describe("hello"))   # значение
  1. Перепиши mutable default через None:
# Найди ошибку и исправь
def append_log(message, log=[]):
    log.append(message)
    return log

# Покажи что текущая версия багает
print(append_log("first"))   # ['first']
print(append_log("second"))  # ['first', 'second'] - проблема

Что дальше

Освоили truthy/falsy и None. Чтобы «значение может отсутствовать» было видно и статическому анализатору, есть аннотация Optional - см. урок про type hints. В следующем уроке - переменные и изменчивость: как Python хранит значения в памяти, разница mutable и immutable объектов, и почему a = b не копирует список.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.