Книга «Чистый Python. Тонкости программирования для профи»
Привет, Хаброжители! Изучение всех возможностей Python — сложная задача, а с этой книгой вы сможете сосредоточиться на практических навыках, которые действительно важны. Раскопайте «скрытое золото» в стандартной библиотеке Python и начните писать чистый код уже сегодня.
Если у вас есть опыт работы со старыми версиями Python, вы сможете ускорить работу с современными шаблонами и функциями, представленными на Python 3.
Если вы работали с другими языками программирования и хотите перейти на Python, то найдете практические советы, необходимые для того, чтобы стать эффективным питонистом.
Если вы хотите научиться писать чистый код, то найдете здесь самые интересные примеры и малоизвестные трюки.
Отрывок «Самое сумасшедшее выражение-словарь на западе»
Иногда вы наталкиваетесь на крошечный пример кода, который обладает поистине неожиданной глубиной — одна-единственная строка кода, которая способна многому научить, если хорошенько над ней поразмыслить. Такой фрагмент код — это как коан в дзен-буддизме: вопрос или утверждение, используемое в практике дзен, чтобы вызвать сомнение и проверить достижения ученика.
Крошечный фрагмент кода, который мы обсудим в этом разделе, является одним из таких примеров. На первый взгляд он может выглядеть как прямолинейное выражение-словарь, но при ближайшем рассмотрении он отправляет вас в расширяющий сознание психоделический круиз по интерпретатору СPython.
От этого однострочника я получаю такой кайф, что как-то раз я даже напечатал его на своем значке участника конференции по Python в качестве повода для беседы. Это привело к нескольким конструктивным диалогам с участниками моей электронной рассылки по Python.
Итак, без дальнейших церемоний, вот этот фрагмент кода. Возьмите паузу, чтобы поразмышлять над приведенным ниже выражением-словарем и тем, к чему его вычисление должно привести:
>>> {True: 'да', 1: 'нет', 1.0: 'возможно'}
Я подожду здесь…
О«кей, готовы?
Ниже показан результат, который мы получим при вычислении приведенного выше выражения-словаря в сеансе интерпретатора Python:
>>> {True: 'да', 1: 'нет', 1.0: 'возможно'}
{True: 'возможно'}
Признаюсь, когда увидел этот результат впервые, я был весьма ошарашен. Но все встанет на свои места, когда вы проведете неспешное пошаговое изучение того, что тут происходит. Давайте поразмыслим, почему мы получаем этот, надо сказать, весьма не интуитивный результат.
Когда Python обрабатывает наше выражение-словарь, он сначала строит новый пустой объект-словарь, а затем присваивает ему ключи и значения в том порядке, в каком они переданы в выражение-словарь.
Тогда, когда мы его разложим на части, наше выражение-словарь будет эквивалентно приведенной ниже последовательности инструкций, которые исполняются по порядку:
>>> xs = dict()
>>> xs[True] = 'да'
>>> xs[1] = 'нет'
>>> xs[1.0] = 'возможно'
Как ни странно, Python считает все ключи, используемые в этом примере словаря, эквивалентными:
>>> True == 1 == 1.0
True
Ладно, но погодите минуточку. Уверен, вы сможете интуитивно признать, что 1.0 == 1, но вот почему True считается также эквивалентным и 1? В первый раз, когда я увидел это выражение-словарь, оно действительно меня озадачило.
Немного покопавшись в документации Python, я узнал, что Python рассматривает тип bool как подкласс типа int. Именно так обстоит дело в Python 2 и Python 3:
Булев тип — это подтип целочисленного типа, и булевы значения ведут себя, соответственно, как значения 0 и 1 почти во всех контекстах, при этом исключением является то, что при преобразовании в строковый тип, соответственно, возвращаются строковые значения 'False' или 'True'.
И разумеется, это означает, что в Python булевы значения технически можно использовать в качестве индексов списка или кортежа:
>>> ['нет', 'да'][True]
'да'
Но вам, пожалуй, не следует использовать подобного рода логические переменные во имя ясности (и душевного здоровья ваших коллег).
Так или иначе, вернемся к нашему выражению-словарю.
Что касается языка Python, то все эти значения — True, 1 и 1.0 — представляют одинаковый ключ словаря. Когда интерпретатор вычисляет выражение-словарь, он неоднократно переписывает значение ключа True. Это объясняет, почему в самом конце результирующий словарь содержит всего один ключ.
Прежде чем мы пойдем дальше, взглянем еще раз на исходное выражение-словарь:
>>> {True: 'да', 1: 'нет', 1.0: 'возможно'}
{True: 'возможно'}
Почему здесь в качестве ключа мы по-прежнему получаем True? Разве не должен ключ из-за повторных присваиваний в самом конце тоже поменяться на 1.0?
После небольших изысканий в исходном коде интерпретатора Python я выяснил, что, когда с объектом-ключом ассоциируется новое значение, словари Python сам этот объект-ключ не обновляют:
>>> ys = {1.0: 'нет'}
>>> ys[True] = 'да'
>>> ys
{1.0: 'да'}
Безусловно, это имеет смысл в качестве оптимизации производительности: если ключи рассматриваются идентичными, то зачем тратить время на обновление оригинала?
В последнем примере вы видели, что первоначальный объект True как ключ никогда не заменяется. По этой причине строковое представление словаря по-прежнему печатает ключ как True (вместо 1 или 1.0).
С тем, что мы знаем теперь, по всей видимости, значения в результирующем словаре переписываются только потому, что сравнение всегда будет показывать их как эквивалентные друг другу. Вместе с тем оказывается, что этот эффект не является следствием проверки на эквивалентность методом __eq__ тоже.
Словари Python опираются на структуру данных хеш-таблица. Когда я впервые увидел это удивительное выражение-словарь, моя первая мысль заключалась в том, что такое поведение было как-то связано с хеш-конфликтами.
Дело в том, что хеш-таблица во внутреннем представлении хранит имеющиеся в ней ключи в различных «корзинах» в соответствии с хеш-значением каждого ключа. Хеш-значение выводится из ключа как числовое значение фиксированной длины, которое однозначно идентифицирует ключ.
Этот факт позволяет выполнять быстрые операции поиска. Намного быстрее отыскать числовое хеш-значение ключа в поисковой таблице, чем сравнивать полный объект-ключ со всеми другими ключами и выполнять проверку на эквивалентность.
Вместе с тем способы вычисления хеш-значений, как правило, не идеальны. И в конечном счете два или более ключа, которые на самом деле различаются, будут иметь одинаковое производное хеш-значение, и они в итоге окажутся в той же самой корзине поисковой таблицы.
Когда два ключа имеют одинаковое хеш-значение, такая ситуация называется хеш-конфликтом и является особым случаем, с которым должны разбираться алгоритмы вставки и нахождения элементов в хеш-таблице.
Исходя из этой оценки, весьма вероятно, что хеширование как-то связано с неожиданным результатом, который мы получили из нашего выражения-словаря. Поэтому давайте выясним, играют ли хеш-значения ключей здесь тоже какую-то определенную роль.
Я определяю приведенный ниже класс как небольшой сыскной инструмент:
class AlwaysEquals:
def __eq__(self, other):
return True
def __hash__(self):
return id(self)
Этот класс характерен двумя аспектами.
Во-первых, поскольку дандер-метод __eq__ всегда возвращает True, все экземпляры этого класса притворяются, что они эквивалентны любому объекту:
>>> AlwaysEquals() == AlwaysEquals()
True
>>> AlwaysEquals() == 42
True
>>> AlwaysEquals() == 'штаа?'
True
И во-вторых, каждый экземпляр AlwaysEquals также будет возвращать уникальное хеш-значение, генерируемое встроенной функцией id ():
>>> objects = [AlwaysEquals(),
AlwaysEquals(),
AlwaysEquals()]
>>> [hash(obj) for obj in objects]
[4574298968, 4574287912, 4574287072]
В Python функция id () возвращает адрес объекта в оперативной памяти, который гарантированно является уникальным.
При помощи этого класса теперь можно создавать объекты, которые притворяются, что они являются эквивалентными любому другому объекту, но при этом с ними будет связано уникальное хеш-значение. Это позволит проверить, переписываются ли ключи словаря, опираясь только на результат их сравнения на эквивалентность.
И, как вы видите, ключи в следующем ниже примере не переписываются, несмотря на то что сравнение всегда будет показывать их как эквивалентные друг другу:
>>> {AlwaysEquals(): 'да', AlwaysEquals(): 'нет'}
{ : 'да',
: 'нет' }
Мы также можем взглянуть на эту идею с другой стороны и проверить, будет ли возврат одинакового хеш-значения достаточным основанием для того, чтобы заставить ключи быть переписанными:
class SameHash:
def __hash__(self):
return 1
Сравнение экземпляров класса SameHash будет показывать их как не эквивалентные друг другу, но они все будут обладать одинаковым хеш-значением, равным 1:
>>> a = SameHash()
>>> b = SameHash()
>>> a == b
False
>>> hash(a), hash(b)
(1, 1)
Давайте посмотрим, как словари Python реагируют, когда мы пытаемся использовать экземляры класса SameHash в качестве ключей словаря:
>>> {a: 'a', b: 'b'}
{ : 'a',
: 'b' }
Как показывает этот пример, эффект «ключи переписываются» вызывается не одними только конфликтами хеш-значений.
Словари выполняют проверку на эквивалентность и сравнивают хеш-значение, чтобы определить, являются ли два ключа одинаковыми. Попробуем резюмировать результаты нашего исследования.
Выражение-словарь {True: 'да', 1: 'нет', 1.0: 'возможно'} вычисляется как {True: 'возможно'}, потому что сравнение всех ключей этого примера, True, 1, и 1.0, будет показывать их как эквивалентные друг другу, и они все имеют одинаковое хеш-значение:
>>> True == 1 == 1.0
True
>>> (hash(True), hash(1), hash(1.0))
(1, 1, 1)
Пожалуй, теперь уже не так удивительно, что мы получили именно такой результат в качестве конечного состояния словаря:
>>> {True: 'да', 1: 'нет', 1.0: 'возможно'}
{True: 'возможно'}
Здесь мы затронули много тем, и этот конкретный трюк Python поначалу может не укладываться в голове — вот почему в самом начале раздела я сравнил его с коаном в дзен.
Если вы с трудом понимаете, что происходит в этом разделе, попробуйте поэкспериментировать по очереди со всеми примерами кода в сеансе интерпретатора Python. Вы будете вознаграждены расширением своих познаний о внутренних механизмах языка Python.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 20% по купону — Python