Булевы значения, 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)
>>> 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:
...
>>> 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 - останавливаются как только результат определён.
Мини-задание
- Проверь 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)}")
- Реализуй функцию которая различает «пусто» от «не задано»:
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")) # значение
- Перепиши 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 не копирует список.