Python известен своей простотой и предсказуемостью, но за этой доступностью скрываются интересные и неочевидные особенности, способные удивить программистов с базовым опытом(а если повезет, то и опытных). В этой статье мы рассмотрим несколько таких «фокусов» и тонкостей, чтобы глубже понять внутреннюю логику и философию языка.
Оператор побитовой инверсии: почему bool(~-True) — это False?
В Python оператор побитовой инверсии (bitwise NOT) обозначается символом ~
и применяется к целым числам, инвертируя каждый бит. Результат операции для положительных чисел соответствует формуле ~x = -(x + 1)
. Например:
x = 5 # Бинарное представление: 00000101 result = ~x # Результат: -(x + 1), т.е. -6 (в двоичном представлении: 11111010) print(result) # Вывод: -6
Рассмотрим, как из bool(~-True)
получается False
:
result = bool(~-True) print(result) # Вывод: False
Визуально этот процесс можно отобразить так:
Помимо этого, оператор ~
можно использовать, но лучше так конечно не делать, в преобразовании индекса последовательностей, например таких как строки, кортежи и списки. Рассмотрим вот такой код:
# Для списка lst = [10, 20, 30] print(lst[~0]) # Вывод: 30 | list[~0] эквивалентно list[-1] # Для строки word = "HABR" print(word[~1]) # Вывод: B | list[~1] эквивалентно list[-2] # Для кортежа tup = (1, 2, 3) print(tup[~2]) # Вывод: 1 | list[~2] эквивалентно list[-3]
В каком-то смысле, если образно представить, то оператор ~
при работе с индексами можно рассматривать как способ инвертировать направление отсчёта индексов:
Это действительно похоже на то, как если бы отсчёт индексов «поменялся» с начала на конец.
Неочевидное поведение встроенных функций: почему all([]) — True, а any([]) — False?
all()
для пустого списка []
возвращает True
:
print(all([])) # Вывод: True
Это поведение можно объяснить следующим образом: функция all()
для пустого списка []
возвращает True
, поскольку в пустом списке нет элементов, которые могли бы быть ложными и опровергнуть утверждение, что все элементы истинны. Если с этим примером всё ясно, то следующий уже можно назвать «парадоксом вложенных списков»:
Поговорим о any()
. any()
для пустого списка []
возвращает False
:
print(any([])) # Вывод: False
Это поведение можно объяснить следующим образом: функция any()
возвращает True
, если хотя бы один элемент последовательности является истинным (не False
). Поскольку в пустой последовательности нет элементов, удовлетворяющих этому условию, функция any()
возвращает False
. Добавим в копилку «парадоксов»:
Разбавим статью неочевидностями, которые могут пригодиться в повседневной работе с кодом, с функциями isinstance()
и range()
. Говоря о isinstance()
, нередко встречается такой код:
num = 10 if isinstance(num, int) or isinstance(num, float): print("Habr") # Вывод: Habr
Здесь isinstance()
используется дважды. Но, эта функция может принимать более одного параметра для проверки типов:
num = 10 if isinstance(num, (int, float)): print("Habr") # Вывод: Habr
(int, float)
— это кортеж, состоящий из двух параметров: типов int
и float
. Но это ещё не всё. Вместо кортежа можно использовать union operator:
num = 10 if isinstance(num, int | float): print("Habr") # Вывод: Habr
Рассмотрим на примере небольшой задачки: дан список с разными типами чисел. Нужно найти сумму всех положительных целых и дробных чисел. Решение может быть следующим:
nums = [42, 3.14, (2+3j), -5, 10, '23'] # Суммируем только положительные целые и дробные числа result = sum(i for i in nums if isinstance(i, int | float) and i > 0) print(result) # Вывод: 55.14
Кроме этого, isinstance()
можно использовать в дебаггинге:
num = 10 print(f'{isinstance(num, int) = }') # Вывод: isinstance(num, int) = True print(f'{isinstance(num, float) = }') # Вывод: isinstance(num, float) = False
Поговорим о range()
. В Python с функцией range()
можно использовать срезы для получения элементов из последовательности:
result = list(range(20)[::2]) print(result) # Вывод: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] # Предыдущий код тоже самое, что и: result = list(range(0, 20, 2)) print(result) # Вывод: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] # А вот это уже для строк: text = "Hello Habra World" for i in range(len(text))[::6]: print(text[i], end=' ') # Собрали первую букву каждого слова, Вывод: H H W
Небольшая шпаргалка по срезам в range()
:
result_step = list(range(15)[::5]) print(result_step) # Вывод: [0, 5, 10] result_slice = list(range(15)[:5:]) print(result_slice) # Вывод: [0, 1, 2, 3, 4] result_start = list(range(15)[5::]) print(result_start) # Вывод: [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
И теперь, благодаря срезам, решение первой задачи Проекта Эйлера может претендовать на звание самого элегантного:
print(sum(set(range(1000)[::3]) | set(range(1000)[::5]))) # 233168
Фокусы списков в Python
Рассмотрим вот такой код:
lst = [[]] * 5 print(lst) # Вывод: [[], [], [], [], []]
Каждый элемент в lst
выглядит как отдельный пустой список, но это не так. Все элементы в lst
указывают на один и тот же список в памяти:
lst[0].append("Habr") print(lst) # Вывод: [['Habr'], ['Habr'], ['Habr'], ['Habr'], ['Habr']]
Чтобы окончательно убедиться в этом, можно посмотреть на id каждого элемента внутри списка:
У всех пяти элементов внутри lst
один и тот же id
. Обойти это можно следующим образом:
lst = [[] for _ in range(5)] print(lst) # Вывод: [[], [], [], [], []] | id каждого элемента внутри будет разным lst[0].append("Habr") print(lst) # Вывод: [['Habr'], [], [], [], []]
А теперь посмотрим, что значит список, содержащий ссылку на самого себя:
lst = [] lst.append(lst) print(lst) # Вывод: [[...]]
В пустой список lst
добавляется сам lst
. Теперь список содержит ссылку на самого себя. И при попытке вывести список, Python распознает, что список содержит рекурсивную ссылку, и вместо бесконечного вывода выводит: [[...]]
. Мы можем заглянуть внутрь, и удостовериться так ли это или нет:
print(lst[0] is lst) # Вывод: True
Разумеется, продолжать «заглядывать внутрь» можно бесконечно долго:
Двуликие True и False: «скрытая природа» булевых значений
В Python тип bool
является подтипом типа int
. Это означает, что логические значения True
и False
представляют собой целые числа 1 и 0 соответственно. Таким образом, объекты типа bool
могут использоваться в арифметических операциях так же, как и целые числа. Например:
print(True + False + True) # Вывод: 2 print("Habr"[False]) # Вывод: H print("Habr"[True+True]) # Вывод: b print("Habr"[-True]) # Вывод: r print(False == False in [False]) # Вывод: True
Это свойство булевых значений полезно, например, в подсчете количества элементов, удовлетворяющих условию, с использованием функций вроде sum
:
lst = [True, False, True] print(sum(lst)) # Вывод: 2
А вот так, например, выглядит вычисление первых десяти чисел последовательности Фибоначчи, используя True
и False
в качестве первых двух чисел:
def get_fibonacci(): """Получение первых десяти элементов последовательности Фибоначчи без прямого использования числовых значений.""" nums = [] a, b = False, True for _ in range(len("tennumbers")): nums.append(a) a, b = b, a + b return [int(i) if isinstance(i, bool) else i for i in nums] ''' Здесь хоть мы и конвертируем первые два элемента, но делаем это не для вычисления, а для однотипного вывода результатов. Иначе: [False, True, 1, 2, 3, 5, 8, 13, 21, 34] ''' print(get_fibonacci()) # Вывод: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
И даже решение этой задачи в одну строку (вопреки PEP-8 и здравому смыслу, но во имя эксперимента) выдаст желаемый результат:
print((lambda f: [f.append(f[-True] + f[--~True]) or f[-True] for _ in range(len("abcdefjh"))] and f)([int(False), int(True)])) # Вывод: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Всё это поведение булевых значений можно описать двумя строками кода:
isinstance(True, int) # Вернет True isinstance(False, int) # Вернет True
Или даже одной строкой кода:
# Родительский класс bool — это int: bool.__bases__ # Вернет (<class 'int'>,)
В завершение, стоит отметить важное выражение, отражающее один из ключевых аспектов философии Python:
isinstance(type, object) == isinstance(object, type) # Вывод: True
Это утверждение подчеркивает взаимосвязь между типами и объектами, демонстрируя гибкость и мощь динамической типизации языка.
ссылка на оригинал статьи https://habr.com/ru/articles/869200/
Добавить комментарий