Подробный разбор типичных ошибок новичков в Python опубликовал сайт proglib.io. Представляем эту статью вашему вниманию.
Привет! Меня зовут Маша, я уже шесть лет занимаюсь коммерческой разработкой на Python, а ещё пишу задачи и объясняю теорию для студентов курса «Мидл Python-разработчик» от Яндекс.Практикума. По опыту знаю, что начинающий разработчик чаще всего хорошо знает синтаксис языка, но не до конца разбирается с тем, что у Python «под капотом».
В результате программист-джуниор допускает неочевидные ошибки: на первый взгляд, его код написан идеально, но почему-то работает некорректно. Защититься от таких недоразумений поможет только знание нюансов внутренней работы Python. Поэтому сегодня я рассмотрю типичные проблемы, с которыми сталкиваются новички, и предложу несколько вариантов их решения.
1. Полагаетесь на изменяемые типы в значениях по умолчанию
У Python есть прекрасная особенность, а именно возможность задавать значения по умолчанию. Вы можете написать так:
def pow(number, mod=2): pass
Или так:
class Cat: legs = 4
И вам не придётся каждый раз указывать степень, в которую вы хотите возвести число (пока эта степень – 2), или уточнять количество ног у вашего кота.
В чём подвох?
Значения по умолчанию работают правильно только с неизменяемыми объектами – строками, числами, frozen-объектами и boolean-типами. Если же вы укажете в качестве значения по умолчанию изменяемый объект, например, list
, set
или dict
, то Python не будет ругаться, но преподнесёт вам неприятный сюрприз. Вот один из примеров: кота заводили дома, а он поселился ещё и в офисе:
class House: cats = [] my_house = House() office = House() my_house.cats.append('Tom') print(my_house.cats) # ["Tom"] print(office.cats) # ["Tom"]
Чем объясняется проблема?
Инструкции, объявляющие класс, выполнятся один раз. У всех экземпляров класса House
будет ссылка на один и тот же массив – cats
.
Такое поведение бывает сложно поймать: если вы создали всего один экземпляр объекта, то, скорее всего, не заметите проблему. Но столкнётесь с ней позже.
Как решить проблему?
Привыкайте вместо значений по умолчанию указывать None
:
class House: cats: list = None def __init__(self): self.cats = [] my_house = House() office = House() my_house.cats.append('Tom') print(my_house.cats) # ["Tom"] print(office.cats) # []
Тогда код будет работать корректно, и все коты останутся на своих местах!
2. Вызываете функцию в значении по умолчанию
Продолжаем разбираться с магией значений по умолчанию, а точнее – с вызовом функции. Представьте себе, что вы установили дома умную камеру и настроили её так, чтобы она записывала действия всех, кто появляется в её поле зрения, в текстовый файл. Ваша функция будет выглядеть так:
from datetime import datetime def create_log_entry(user, action, time=datetime.now()): return f'{time}: {user} {action}'
После этого вы, спокойные и довольные собой, ушли на работу.
В чём подвох?
Вернувшись домой, вы решили проверить, как записалось каждое событие, посмотреть актуальные даты и описания. Ожидание:
create_log_entry('Алла', 'вышла из дома') '2020-09-14 15:20:03.333333: Алла вышла из дома' create_log_entry('Том', 'поймал мышь') '2020-09-14 15:25:12.795328: Том поймал мышь' create_log_entry('Адорианец', 'заварил кофе') '2020-09-14 15:40:33.173500: Адорианец заварил кофе' create_log_entry('Агент Кей', 'применил нейралайзер') '2020-09-14 15:41:48.922357: Агент Кей применил нейралайзер'
Реальность – все события как будто произошли в одно и то же время:
create_log_entry('Алла', 'вышла из дома') '2020-09-14 15:20:00.333333: Алла вышла из дома' create_log_entry('Том', 'поймал мышь') '2020-09-14 15:20:00.333333: Том поймал мышь' create_log_entry('Адорианец', 'заварил кофе') '2020-09-14 15:20:00.333333: Адорианец заварил кофе' create_log_entry('Агент Кей', 'применил нейралайзер') '2020-09-14 15:20:00.333333: Агент Кей применил нейралайзер'
Чем объясняется проблема?
Это произошло из-за того, что datetime.now
сработал всего один раз – в тот момент, когда интерпретатор встретил объявление функции конструкцией def create_log_entry
. Python запомнил, какая дата и время были на момент запуска программы, и постоянно использовал это значение.
Как её решить?
Чтобы время вычислялось каждый раз при вызове вашей функции, нужно перенести вычисления в тело функции:
from datetime import datetime def create_log_entry(user, action, time=None): time = datetime.now() if time is None else time return f'{time}: {user} {action}'
Так вы всё-таки узнаете, во сколько Том и Адорианец пили кофе и когда агент Кей ворвался к вам домой со своим нейралайзером.
3. Используете одновременно int и bool как ключи dict
Предположим, вы решили написать простой переводчик с компьютерного языка на человеческий для своего умного дома. Вам нужно, чтобы True
отображалось как «Правда», False
– как «Ложь», а 1
и 0
переводились как «Есть» и «Нет». Зафиксируем все переводы в словаре:
vocabulary = { True: "Правда", False: "Ложь", 1: "Есть", 0: "Нет" }
В чём подвох?
В этом словаре используется четыре разных ключа. Проверим, действительно ли всё работает корректно:
print(vocabulary[True]) # 'Есть' print(vocabulary[False]) # 'Нет' print(vocabulary[1]) # 'Есть' print(vocabulary[0]) # 'Нет'
Кажется, что-то пошло не так. Давайте заглянем в сам словарь:
print(vocabulary) # {True: 'Есть', False: 'Нет'}
Из него пропали два варианта перевода, а те, что остались – неверные.
Чем объясняется проблема?
Чтобы разобраться в произошедшем, нужно понимать две вещи: что такое класс bool
и как работает словарь.
- Класс
bool
, добавленный в Python 2.3, реализован как наследник классаint
. То есть глобальные объектыTrue
иFalse
– всего лишь два экземпляра классаbool
, представляющие собой1
и0
. В этом классе переопределены методы__repr__
и__str__
, которые отвечают за отображение экземпляра, но «под капотом» они остаются простыми цифрами. Это можно проверить, сравнивTrue
и число. Зная это, вы можете использовать boolean-переменные в математических выражениях. Но я так поступать не рекомендую: как сказано в дзене Python (вы можете прочитать его, введя в интерпретаторimport this
), «читаемость имеет значение». Подробнее о реализации boolean можно прочитать в PEP-0285. - Также внутри словаря находится hash-таблица: то есть все новые ключи, которые добавляются в словарь, проходят через hash-функцию, и именно она определяет, где расположить элемент в памяти. Таким образом, поиск и вставка данных становятся намного быстрее, чем в обычном массиве. Если хочется узнать больше подробностей о работе словарей в Python, рекомендую заглянуть на stackoverflow.
Как решить проблему?
Для корректной реализации переводчика следует привести все ключи к одному типу данных – str
.
vocabulary = { "True": "Правда", "False": "Ложь", "1": "Есть", "0": "Нет" }
Hash-функции ключей перестанут совпадать, и ответ словаря будет таким, как мы хотели, – общий язык с умным домом всё-таки будет найден:
vocabulary[str(True)] # "Правда"
4. Используете set для ускорения вычислений
Среди разработчиков бытует распространённое мнение, что поиск элемента в set
работает быстрее, чем в list
. Поэтому нередко можно встретить следующий вариант кода:
animals = ['cat', 'dog', 'bird', 'mouse', 'rat', 'elephant'] # <какой-то код, дополняющий или модифицирующий список> if 'dog' in set(animals): # <дальнейшие вычисления>
В чём подвох?
Рассмотрим конструкцию с точки зрения интерпретатора:
animals = ['cat', 'dog', 'bird', 'mouse', 'rat', 'elephant'] animals_set = set(animals) # Нужно пройтись по всем элементам list и добавить каждый из них в set (cложность: O(n)) if 'dog' in animals_set: # Нужно найти элемент во множестве O(1) # <дальнейшие вычисления>
Без оптимизации интерпретатор остановил бы поиск на втором элементе, но код заставил его сначала пройтись по всему списку, а потом выполнить дополнительное действие с set
. В итоге вместо двух шагов получилось семь – никакого ускорения, только дополнительные расходы на память!
Чем объясняется проблема?
Прежде всего – разной природой list
и set
. При объявлении типа list
резервируется участок памяти, в котором будут храниться ссылки на другие данные в памяти. Список может хранить ссылки на любые объекты: строки, числа, другие массивы и даже на самого себя. Все объекты в списке хранятся последовательно.
Чтобы найти нужный элемент, интерпретатор последовательно идёт по ссылкам, начиная с первой, и сравнивает объект с искомым: найдя нужные данные, он останавливает поиск. Чем длиннее список, тем больше времени занимает процесс. В O-нотации это записывается как O(n).
set
, так же, как и list
, хранит элементы, но работает принципиально иначе. Во-первых, он содержит в себе только уникальные элементы, во-вторых, в нём нельзя хранить изменяемые структуры, и, наконец, в-третьих, данные будут размещены не в заданном вами порядке, а в наиболее удобном для Python.
Так как расположение в множестве определяется содержимым элемента, поиск по set
и правда работает гораздо быстрее. Выполняя команду x in set_y
, интерпретатору нужно взять hash-функцию от x
и посмотреть, есть ли в set_y
данные по полученному адресу. Никакого последовательного просмотра элементов и нудного сравнения!
O-нотация называет такую сложность O(1): вне зависимости от размеров множества поиск будет происходить за одинаковое количество времени.
Как решить проблему?
Звучит банально, но правильнее было бы не мудрить и воспользоваться обычным поиском.
animals = ['cat', 'dog', 'bird', 'mouse', 'rat', 'elephant'] if 'dog' in animals: # <дальнейшие вычисления>
Как говорится в дзене Python, «простое лучше сложного».
Советы для новичков в Python
Пожалуй, самый главный совет, который стоит дать специалистам-джуниорам, только начинающим свою карьеру в Python, – это не только зубрить основы, но и заглядывать внутрь инструмента, которым вы пользуетесь.
Чтобы не оказаться тем самым новичком, у которого ничего не работает, я советую:
- Прочувствовать на себе дзен Python. Мало прочитать, что «простое лучше, чем сложное»: важно применять этот принцип на практике и не создавать себе дополнительных трудностей.
- Зрить в корень. Про типы, классы, структуры данных и операции с ними рассказывают на первых уроках по программированию. Ваша задача – выяснить не только «для чего они используются» и «что могут», но и «как они работают».
- Не соблазняться фрилансом. В начале пути вам точно стоит поработать в компаниях с высокой инженерной культурой. Так вы сможете перенимать опыт от людей, которые умеют и любят писать хороший код, а не набивать шишки самостоятельно.
От редакции Techrocks. Возможно, вам также будут интересны другие наши статьи:
- 5 ошибок начинающих веб-разработчиков
- 4 полезных приема работы с Python
- Факты и мифы об именах и значениях в Python
[customscript]techrocks_custom_after_post_html[/customscript]
[customscript]techrocks_custom_script[/customscript]