Что отсутствует в следующем коде
Перейти к содержимому

Что отсутствует в следующем коде

  • автор:

Найдите ошибки в следующем коде

Обложка поста Найдите ошибки в следующем коде

Первая заключается в том, что используется тип unsigned int , который работает только со значениями, большими или равными нулю. Поэтому условие цикла for всегда будет истинно, и цикл будет выполняться бесконечно.

Корректный код, выводящий значения всех чисел от 100 до 1, должен использовать условие i > 0 . Если нам на самом деле нужно вывести нулевое значение, то следует добавить дополнительный оператор printf после цикла for .

unsigned int i; for (i = 100; i > 0; --i) printf("%d\n", i); printf("%d\n", i); 

Вторая ошибка — вместо %d следует использовать %u , поскольку мы выводим целые значения без знака.

unsigned int i; for (i = 100; i > 0; --i) printf("%u\n", i); 

Теперь этот код правильно выведет список чисел от 100 до 1, в убывающем порядке.

Разбор взят из книги Гейл Л. Макдауэлл «Cracking the Coding Interview» (есть в переводе).

Основы программирования на языке Python. Часть 2

На минутку представим себя богами. В первой части этой статьи мы с вами разобрали самые базовые элементы программирования. Или на нашем, на «божественном» — мы научились создавать самый простой мир, в котором живут разные человечки со своими особенностями, но без целей в жизни. Пора заставить их что-то делать и использовать свой потенциал по максимуму. Для этого переходим на новый уровень.

Условная конструкция. Табуляция

В этой статье мы будем переводить с алгоритмического языка на язык Python то, что выучили в одной из предыдущих статей — «Основы алгоритмов». Также мы успели изучить создание и применение переменных в Python, и то, какие типы данных могут быть им присвоены, в статье «Основы программирования на языке Python. Часть 1». Но далеко на этом не уедешь — нет никакой гибкости, никаких дополнительных возможностей, ничего интересного.

Условная конструкция if-elif-else — первый шаг на пути к разнообразию. Ее цель — совершать конкретные действия при конкретных обстоятельствах.

Эта логическая конструкция расшифровывается так — «если-иначе». Даже без нашего четкого осознания она появляется в повседневной жизни везде. Самая простая идея алгоритма с ветвлением (с условием): «Если в холодильнике нет продуктов, их надо купить, прежде чем начать готовить ужин, иначе можно сразу начинать готовку».

Как в программе Python выглядят ветвления?
Подробно разберем строение этой конструкции:

  • if — главная команда создания условия, ее одной уже достаточно. С ней конструкция будет выглядеть так:

Если выполнится, то будет запущено , иначе не произойдет ничего.

x = 25
if x > 0:
print(“x больше 0”)
Вывод программы:
х больше 0
x = -6
if x > 0:
print(“x больше 0”)
Вывод программы:
ничего не будет выведено
  • else — команда, задающая альтернативу.

if :

else:

Если выполнится, то будет запущен блок , а если оно не выполнится — запустится .

  • elif — команда для создания сложного условия, когда у нас есть не два исхода, а больше.

if :

elif :

elif :


else:

В зависимости от выполнения какого-то из будет запущено соответствующее . Команда else в данном случае не обязательна: если она есть, при невыполнении всех условий выполнится ее тело, а если ее нет — просто не произойдет ничего.

Табуляцию можно проставить либо нажатием клавиши Tab, либо используя несколько идущих подряд пробелов. Традиционно табуляции соответствует 4 пробела. Но нельзя одновременно использовать и пробелы, и Tab — в одном блоке должно быть только что-то одно.

Про табуляцию вообще нельзя забывать и нужно быть с ней аккуратнее:

  • если ее проставить неправильно — программа будет выполняться не так, как мы задумали;
  • если не проставить совсем — программа не сможет даже запуститься.

А чем вообще может быть условие команды?
Это должна быть такая конструкция, про которую определенно можно сказать, истинна она или ложна. Например, 5 > 1 — истина (True), а 5 > 10 — ложь (False).

Для записи условий в Python предусмотрены как математические операторы сравнения, так и логические операторы:

Математические операторы сравнения работают так же, как и в школьном курсе алгебры. Только обратите внимание, что большинство из них записывается иначе, и могут применяться не только для чисел, а и для других типов данных.

Когда мы попробуем вывести на экран результат выполнения условия, мы увидим, что он может вернуть:

  • либо значение True, которое соответствует выполнению записанного условия;
  • либо False, соответствующее невыполнению условия.

x = 15
y = 16
print(x == y)
print((y > x) and (x > 5))

Это как раз и есть тот самый тип данных bool из предыдущей части статьи.

А ниже приведен пример кода, в котором разная расстановка отступов дает нам совершенно разное поведение программ.

Циклы. Тип range

Как в программе Python выглядят циклы?

Циклы используются для многократного повторения определенной команды или набора команд. Для выделения тела цикла также используется табуляция.

В Python есть два цикла — while и for.

Цикл while будет повторять команды своего тела до тех пор, пока выполняется условие, записанное для него.

Структура записи этого цикла:

Например, нам нужно уменьшать значение переменной a в 2 раза нацело до тех пор, пока она не станет меньше или равной переменной b. Для этого в условии цикла прописываем сравнение переменных, а в теле цикла — уменьшение значения переменной a. Пока условие выполняется (то есть принимает значение True), будет выполняться тело цикла, а когда оно перестанет выполняться — свою работу продолжит основной блок кода.

a = 1000000
b = 16
while a > b:
a //= 2
print(a)

После первой итерации (запуска содержимого цикла) переменная а будет равна 500 000, после второй — 250 000 и т.п. Итерации будут повторяться, пока будет продолжать быть истинным выражение a > b. Когда итерации цикла завершаются, мы видим итоговое значение переменной а после всех делений — 15.

Бесконечный цикл — ситуация, когда цикл while сам по себе никогда не сможет завершить свою работу, то есть значение его условия никогда не перестанет быть равным True.

Но как вообще можно в него попасть?

  1. Случайно, когда в записи условия работы цикла была допущена ошибка.

В приведенном примере цикл будет работать до тех пор, пока значение переменной b больше 0. Но никакого изменения этой переменной не происходит, она не меняет своего значения, и, соответственно, никогда не станет меньше или равной 0. А значит, цикл никогда не завершит свою работу.

  1. Специально. Редко, но это имеет смысл. Например, когда условие работы цикла зависит от большого количества параметров, проще будет прямо в цикле указать, что ему стоит прекратить свою работу.

Для создания бесконечного цикла в условии работы нужно прописать выражение, результат которого всегда будет равен True. Например, это может быть само значение True или условие сравнения 0 == 0, также возвращающее всегда True (0 действительно всегда равен 0).

Для остановки цикла используется команда break. Когда цикл натыкается на нее, он моментально прекращает свою работу. Не сработает и последующий блок кода, не будет и следующего шага цикла — программа сразу перескакивает в основной блок кода после цикла.

В примере мы создали бесконечный цикл, но, как только значение a станет меньше или равным b, выполнится условие if и сработает команда break, которая завершит цикл.

Цикл for необходим для выполнения команды или набора команд определенное количество раз или перебора набора данных.

Этот цикл похож на человека, выкладывающего продукты на кассе в магазине. Сколько бы ни было продуктов в корзине — он каждый достанет из корзины и положит на ленту, перебирая по очереди все продукты.

Структура записи цикла for:

Здесь мы сталкиваемся с понятием коллекции — той самой нашей «корзины с покупками», то есть какой-то структуры данных, содержащей много различных значений в себе. Пока что мы из таких типов структур данных знакомы со строками и списками из первой части статьи, а ниже в этой статье разберем еще одну — range.

Как работает цикл for? На каждом шаге цикла будет принимать новое значение из , после чего будет выполняться .

По традиции, если перебираемая переменная не несет в себе особой смысловой нагрузки, ее называют i, j или k.

может принимать любое значение, которое можно перебрать.

a = «abcd»
for i in a:
print(i)
Вывод:
a
b
c
d
  • перебор списка:
a = [12, 34, 567, 8910]
for i in a:
print(i)
Вывод:
12
34
567
8910
  • перебор диапазона range, который создает набор целых чисел.

Команда range позволяет создавать диапазоны — последовательности целых чисел, отличающихся друг от друга на одно и то же число — разницу или шаг диапазона.

Есть несколько способов создания диапазона:

  • range(a) — диапазон целых чисел от 0 до (a — 1) с шагом 1.
    Например: range(5) — 0, 1, 2, 3, 4.
  • range(a, b) — диапазон чисел от a до (b — 1) с шагом 1.
    Например: range(2, 7) — 2, 3, 4, 5, 6.
  • range(a, b, step) — диапазон чисел от a до (b — 1) с разницей между соседними числами, равной step.
    Например: range(1, 17, 3) — 1, 4, 7, 10, 13, 16.
for i in range(2, 6):
print(i)
Вывод:
2
3
4
5

Взаимозаменяемость циклов

Сейчас мы будем заниматься практикой, а заодно посмотрим, как с помощью разных циклов можно решить одну и ту же задачу.

Задача.
У нас есть список чисел a. Нужно вывести на экран его содержимое, по одному числу в строке.

Решение №1.

Чтобы выполнить задачу, нам необходимо перебрать каким-то циклом все его элементы и каждый вывести на экран. Тогда напрашивается самое очевидное решение:

for i in a:
print(i)

Мы берем каждый элемент списка и выводим его, все просто. Но это не единственный способ.

Решение №2.

Мы можем вместо элементов списка перебирать числа-позиции элементов в списке а и выводить на экран элементы на соответствующих позициях. Это можно сделать, например, так:

for i in range(len(a)):
print(a[i])

Здесь len(a) — это длина списка а. Мы используем ее, чтобы понять, до какого числа надо перебирать i, чтобы на i-тых позициях списка все еще были элементы. Это же можно тоже записать другим способом:

Разные подходы к применению циклов позволяют всегда выбирать наиболее удобный под конкретные задачи. Например, здесь нам не важны позиции элементов списка и проще всего было использовать первое решение. Но в разных контекстах задач могут быть разные преимущества и недостатки каждого из циклов.

Вложенные структуры

После знакомства с условными конструкциями и циклами разброс наших возможностей сильно возрастает. Но он возрастет еще сильнее, если мы научимся правильно их комбинировать и использовать одно в другом.

Для этого можем воспользоваться вложенными структурами, когда одна конструкция находится внутри другой.

Такой пример уже мелькнул — когда мы говорили о бесконечном цикле и команде break. Мы сделали так, что внутри цикла команда break выполнится только при истинности условия, для чего прямо внутри цикла while мы использовали условную конструкцию if.

Чтобы программа «не сошла с ума», пытаясь понять, где тело цикла, а где тело условия, мы продолжили использовать табуляцию.

Весь блок условия находится в цикле, поэтому табуляция цикла никуда не делась. Тело вложенного условия получает свою собственную табуляцию прямо поверх предыдущей. Эти разные табуляции отмечены зеленым и синим цветом.

Вложенные структуры — удобный способ повысить возможности программы. Здесь мы никак не ограничены: мы можем вкладывать циклы в циклы, условия в условия, циклы в условия и так далее. Количество вложений также ничем не ограничено.

Важно запомнить: если друг в друга вкладывается несколько циклов for, их переменные должны иметь разные имена.

Разберем в качестве примера следующий код:

n = 1
for i in range(3):
for j in range(2):
n += 2
n *= 3
print(n)

Какое число на экране будет в результате этой программы?

У нас есть внутренний цикл и внешний. Тело внутреннего цикла запускается дважды: при j = 0 и j =1. Каждый раз значение n увеличивается на 2. Значит, в процессе работы внутреннего цикла значение n увеличивается на 2 * 2 = 4.

Тогда тело внешнего цикла — прибавление к n 4 и умножение на 3. Цикл повторяется трижды — i принимает значения 0, 1, 2. Тогда после первой итерации внешнего цикла n=(1+4)*3=15, после второй итерации n=(15+4)*3=57, после третьей — n=(57+4)*3=183.

На экран будет выведено 183.

Импорт модулей

Для еще большего увеличения мощи нашей программы мы можем использовать модули — наборы инструментов с готовыми решениями части наших задач.

Это легко сравнить с проведением ремонта в квартире. Мы можем что-то сделать простыми инструментами, которые лежат на балконе. Но гораздо эффективнее будет съездить в магазин за специальными инструментами. Так мы не только шкаф — ракету соберем.

Какие есть модули и какие в них есть инструменты — мы будем изучать по мере необходимости. Сейчас главное — узнать, как именно это делается. И делать мы это будем на примере модуля math, в котором лежит прекрасная функция sqrt. Она умеет извлекать корни чисел.

Если вдруг случилось так, что вы совсем забыли, как извлекаются корни чисел, то советуем обратиться к нашей статье по математике «Понятие корня».

Для получения инструментов модуля необходимо произвести его импорт — добавление модуля или его частей в программу. Сделать это можно несколькими способами:

import math

При такой записи доступ к конкретной функции мы получим, прописав название этого модуля, после него через точку — название необходимой функции.

  • Импорт всех функций модуля:

from math import *

При такой записи в нашу программу попадают вообще все функции модуля, для доступа к которым теперь не нужно прописывать название самого модуля.

  • Импорт конкретных функций модуля:

from math import sqrt

Такая запись даст нам доступ только к конкретным функциям из всего модуля, которые мы импортируем.

Понимание условий и циклов необходимо для решения любых задач на программирование. Разберем пример задачи №6 ОГЭ.

Дана программа на языке программирования Python:

a = int(input())
b = int(input())
if a == 15 or b == 50:
print(“Да”)
else:
print(“Нет”)

Было проведено 9 запусков. В качестве переменных a и b пользователь вводил следующие значения. Первое значение – переменная a, второе значение – переменная b.

(15; 19); (19; 50); (10; 26); (10; 56); (8; 50); (10; 10); (50; 50); (10; 2); (10; 40).

Определите количество запусков, при которых программа выведет «Да».

Решение.
Заметим, что программа печатает «Да», если a = 15 или b = 50, то есть выполнение хотя бы одного из равенств говорит о том, что программа выведет «Да».

Проверим пары чисел на условия:
— (15; 19)выполнено a = 15;
— (19; 50)выполнено b = 50;
— (10; 26) — не выполнено;
-(10; 56) — не выполнено;
— (8; 50)выполнено b = 50;
— (10; 10) — не выполнено;
— (50; 50)выполнено b = 50;
— (10; 2) — не выполнено;
— (10; 40) — не выполнено.

Нам подойдут все пары, кроме тех, где написано «не выполнено». Их всего 4 штуки.

Ответ: 4

Существует математическое доказательство того, что с помощью циклов и условий можно реализовать совершенно любой алгоритм. Но использование циклов и условий — только самый базовый уровень программирования, и какие-то сложные вещи нам останутся недоступны.

Это все равно что собирать автомобиль, имея под рукой только листы металла. Дальше вы узнаете про много особенностей языка, упрощающих создание алгоритмов, чтобы заменить эти листы металла на заготовки деталей и прочие более удобные для сборки элементы. Но прежде чем работать с более высокоуровневыми конструкциями, надо попрактиковаться в работе с уже пройденным материалом, например, в этой статье.

Фактчек

  • Табуляция — способ структуризации кода, где отдельные блоки кода выделяются отступами.
  • Условная конструкция if-elif-else необходима для выполнения определенных блоков кода при определенных исходах условия.
  • Циклы нужны для многократного повторения блока кода. Цикл while выполняет блок кода, пока выполняется переданное ему условие; цикл for перебирает переданные ему значения.
  • Импортируя модули, мы можем получить доступ к функциям, которые изначально в программе доступны не были.

Проверь себя

Задание 1.

Без запуска кода определите, что будет выведено на экран в результате выполнения программы:

Задание 2.

Без запуска кода определите, что будет выведено на экран в результате выполнения программы:

x = 250
if x < 100:
print(x + 25)
elif x print(x — 100)

Задание 3.

Без запуска кода определите, что будет выведено на экран в результате выполнения программы:

b = 0
for i in range(5):
b = b + i
print(b)

Задание 4.

Без запуска кода определите, что будет выведено на экран в результате выполнения программы:

x = 50
y = 20
while x < y:
x -= 20
print(x)

Ответы: 1. — 4; 2. — 3; 3. — 3; 4. — 3.

Обработка ошибок с помощью Python

Ошибки случаются. Написание скриптов, которое предполагает наличие и обработку ошибок, сохраняет массу времени и ваших сил. Когда инструмент выводит сообщение об ошибке, ArcPy генерирует системную ошибку или исключение. В Python вы можете обеспечить различные структуры и методы для обработки исключений. Конечно, скрипт может не выполниться по причинам, не связанным с инструментом геообработки. Их также следует обнаружить и решить. В следующих разделах предлагается несколько способов, которые ознакомят вас с основными принципами обработки исключений в Python .

Когда инструмент записывает сообщение об ошибке, ArcPy создает исключение arcpy.ExecuteError . Python позволяет написать модуль, который будет выполняться автоматически при возникновении системной ошибки. С помощью этого модуля для обработки ошибок вы сможете получать сообщения об ошибках от ArcPy и реагировать на них. Если скрипт не имеет модуля для обработки ошибок, он завершает выполнение немедленно, что уменьшает его надежность. Модуль обработки ошибок можно использовать для управления ошибками и повышения надежности скриптов.

Сообщения об ошибках в инструментах геообработки обычно имеют шестизначный код. Эти коды-идентификаторы занесены в документы, содержащие дополнительные сведения об их причинах и необходимых действиях.

Выражение try-except

Чтобы охватить всю программу целиком или только отдельные фрагменты кода для обнаружения и определения ошибок могут использоваться выражения try-except . Если в выражении try возникнет ошибка, будет создано исключение, и код будет выполнен в соответствии с выражением except. Использование выражения except является наиболее простой формой обработки ошибок.

В следующем коде Буфер не работает, потому что не был указан обязательный аргумент buffer_distance_or_field . Чтобы найти ошибку, используется выражение except , которое извлекает и выводит сообщение об ошибке, сгенерированное Буфером . Обратите внимание, что блок except выполняется только в случае, если Буфер возвращает ошибку.

import arcpy import sys try: # Execute the Buffer tool arcpy.Buffer_analysis("c:/transport/roads.shp", "c:/transport/roads_buffer.shp") except Exception: e = sys.exc_info()[1] print(e.args[0]) # If using this code within a script tool, AddError can be used to return messages # back to a script tool. If not, AddError will have no effect. arcpy.AddError(e.args[0])

Выражение try содержит необязательное условие finally , которое можно использовать для задач, которые должны выполняться всегда, независимо от того, возникает исключение или нет. В следующем примере ArcGIS 3D Analyst extension регистрируется в соответствии с условием finally , которое гарантирует, что этот дополнительный модуль всегда будет регистрироваться.

class LicenseError(Exception): pass import arcpy try: if arcpy.CheckExtension("3D") == "Available": arcpy.CheckOutExtension("3D") else: # Raise a custom exception raise LicenseError arcpy.env.workspace = "D:/GrosMorne" arcpy.HillShade_3d("WesternBrook", "westbrook_hill", 300) arcpy.Aspect_3d("WesternBrook", "westbrook_aspect") except LicenseError: print "3D Analyst license is unavailable" except arcpy.ExecuteError: print(arcpy.GetMessages(2)) finally: # Check in the 3D Analyst extension arcpy.CheckInExtension("3D")

Выражение raise

Предыдущий пример иллюстрирует ошибку в исключении, которое возникло в коде В некоторых случаях может потребоваться создать пользовательские исключения. Для этой цели может использоваться выражение raise . В следующем коде выражение raise используется, если входной класс объектов определился как не содержащий объектов. Это не обязательно ошибка, но условие, при котором код может быть использован.

class NoFeatures(Exception): pass import arcpy import os import sys arcpy.env.overwriteOutput = True fc = arcpy.GetParameterAsText(0) try: # Check that the input has features result = arcpy.GetCount_management(fc) if int(result[0]) > 0: arcpy.FeatureToPolygon_management( fc, os.path.join(os.path.dirname(fc), 'out_poly.shp')) else: # Raise custom exception raise NoFeatures(result) except NoFeatures: # The input has no features print('<> has no features'.format(fc)) except: # By default any other errors will be caught here e = sys.exc_info()[1] print(e.args[0])

Класс ExecuteError

Когда инструмент геообработки выходит из строя, он создает класс исключений arcpy.ExecuteError , что означает, что вы можете разделить ошибки на различные группы, ошибки геообработки (те, которые создают исключение arcpy.ExecuteError ) и другие типы исключений. Затем можно обрабатывать эти ошибки по разному, как показано в следующем коде:

import arcpy import sys try: result = arcpy.GetCount_management("C:/invalid.shp") # Return geoprocessing specific errors except arcpy.ExecuteError: arcpy.AddError(arcpy.GetMessages(2)) # Return any other type of error except: # By default any other errors will be caught here e = sys.exc_info()[1] print(e.args[0])

traceback

В больших и более сложных скриптах бывает сложно точное определить место возникновения ошибки. Для определения точного местоположения и причины ошибки, а также для более точного определения причины ошибки и экономии ценного времени на отладку можно использовать модули Python , sys и traceback совместно.

# Import the required modules # import arcpy import sys import traceback arcpy.env.workspace = "C:/Data/myData.gdb" try: arcpy.CreateSpatialReference_management() #-------------------------- # Your code goes here # # See the table below for examples #-------------------------- except arcpy.ExecuteError: # Get the tool error messages msgs = arcpy.GetMessages(2) # Return tool error messages for use with a script tool arcpy.AddError(msgs) # Print tool error messages for use in Python print(msgs) except: # Get the traceback object tb = sys.exc_info()[2] tbinfo = traceback.format_tb(tb)[0] # Concatenate information together concerning the error into a message string pymsg = "PYTHON ERRORS:\nTraceback info:\n" + tbinfo + "\nError Info:\n" + str(sys.exc_info()[1]) msgs = "ArcPy ERRORS:\n" + arcpy.GetMessages(2) + "\n" # Return Python error messages for use in script tool or Python window arcpy.AddError(pymsg) arcpy.AddError(msgs) # Print Python error messages for use in Python / Python window print(pymsg) print(msgs)

Если был использован приведенный выше код, и произошла ошибка инструмента геообработки, такая как недопустимые входные данные, это вызовет arcpy.ExecuteError , и будет использоваться первое выражение except . Это выражение выведет сообщение об ошибке с помощью функции GetMessages . Если использовался тот же код, но возникла ошибка другого типа, будет использовано второе выражение except . Вместо печати сообщений о геообработке, будет получен объект traceback и выведены соответствующие сообщения о системных ошибках.

В таблице ниже показаны ожидаемые ошибки, являющиеся результатом трех различных строк кода, который мог быть использован вместо вышеуказанного кода. Первый пример – ошибка инструмента геообработки, которая генерирует информацию traceback и сообщения об ошибках геообработки. Второй и третий примеры не обрабатываются, а происходит только генерация данных traceback.

PYTHON ERRORS: Traceback info: File "c:\temp\errortest.py", line 10, in arcpy.GetCount_management("") Error Info: Failed to execute. Parameters are not valid. ERROR 000735: Input Rows: value is required Failed to execute (GetCount). ArcPy ERRORS: Failed to execute. Parameters are not valid. ERROR 000735: Input Rows: value is required Failed to execute (GetCount).
PYTHON ERRORS: Traceback info: File "c:\temp\errortest.py", line 10, in x = "a" + 1 Error Info: cannot concatenate 'str' and 'int' objects

float(«a text string»)

PYTHON ERRORS: Traceback info: File "c:\temp\errortest.py", line 10, in float("a text string") Error Info: invalid literal for float(): a text string

Получение сообщений об ошибках от объекта Result

Несколько слов об объекте Result , который показан ниже:

result = arcpy.GetCount_management("c:/data/rivers.shp")

Если запрос в GetCount_management вызывает исключение, объект Result не создается. Это означает, что вы не можете получать сообщения об ошибках от объекта Result .

import arcpy try: result = arcpy.GetCount_management("c:/data/rivers.shp") # Return Geoprocessing specific errors # (this method is incorrect!) except arcpy.ExecuteError: arcpy.AddError(result.getMessages(2))

Указанный выше код не работает, и появляется сообщение name ‘result’ is not defined . Причина в том, что объект Result невозможно было создать из-за ошибки инструмента. Поскольку объект Result не создан, возникает ошибка Python при попытке использования метода getMessages .

Примечание:

Объект Result , создаваемый путем запроса в сервис геообработки на ArcGIS Server , создается даже при ошибке инструмента. Создание объекта Result не удается только тогда, когда инструмент запускается локально и вызывает ошибку. Более подробную информацию об использовании объекта result смотрите в разделе Использование инструментов в Python .

В этом разделе
  1. Выражение try-except
  2. Выражение raise
  3. Класс ExecuteError
  4. traceback
  5. Получение сообщений об ошибках от объекта Result

Глючный код на Python: 10 самых распространенных ошибок, которые допускают разработчики

Python — это интерпретируемый, объектно-ориентированный язык программирования высокого уровня с динамической семантикой. Встроенные структуры данных высокого уровня в сочетании с динамической типизацией и динамическим связыванием делают его очень привлекательным для БРПС (быстрой разработки прикладных средств), а также для использования в качестве скриптового и связующего языка для подключения существующих компонентов или сервисов. Python поддерживает модули и пакеты, тем самым поощряя модульность программы и повторное использование кода.

О данной статье

Простота и легкость в освоении данного языка может ввести разработчиков в заблуждение (особенно тех, кто еще только начинает изучать Python), так что можно упустить из виду некоторые важные тонкости и недооценить силу разнообразия возможных решений с помощью Python.

Имея это в виду, в этой статье представлен «топ-10» тонких, трудных для обнаружения ошибок, которые могут допустить даже продвинутые разработчики Python.

Ошибка № 1: неправильное использование выражений в качестве значений по умолчанию для аргументов функций

Python позволяет указывать, что у функции могут быть необязательные аргументы, путем задания для них значения по умолчанию. Это, конечно, очень удобная особенность языка, но может привести к неприятным последствиям, если тип такого значения будет изменяемым. Например, рассмотрим следующее определение функции:

>>> def foo(bar=[]): # bar - это необязательный аргумент # и по умолчанию равен пустому списку. . bar.append("baz") # эта строка может стать проблемой. . return bar

Распространенная ошибка в данном случае — это думать, что значение необязательного аргумента будет устанавливаться в значение по умолчанию каждый раз, как функция будет вызываться без значения для этого аргумента. В приведенном выше коде, например, можно предположить, что повторно вызывая функцию foo() (то есть без указания значения для агрумента bar), она всегда будет возвращать «baz», поскольку предполагается, что каждый раз, когда вызывается foo () (без указания аргумента bar), bar устанавливается в [ ] (т. е. новый пустой список).

Но давайте посмотрим что же будет происходить на самом деле:

>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]

А? Почему функция продолжает добавлять значение по умолчанию «baz» к существующему списку каждый раз, когда вызывается foo(), вместо того, чтобы каждый раз создавать новый список?

Ответом на данный вопрос будет более глубокое понимание того, что творится у Python «под капотом». А именно: значение по умолчанию для функции инициализируется только один раз, во время определения функции. Таким образом, аргумент bar инициализируется по умолчанию (т. е. пустым списком) только тогда, когда foo() определен впервые, но последующие вызовы foo() (т. е. без указания аргумента bar) продолжат использовать тот же список, который был создан для аргумента bar в момент первого определения функции.

Для справки, распространенным «обходным путем» для этой ошибки является следующее определение:

>>> def foo(bar=None): . if bar is None: # or if not bar: . bar = [] . bar.append("baz") . return bar . >>> foo() ["baz"] >>> foo() ["baz"] >>> foo() ["baz"]

Ошибка № 2: неправильное использование переменных класса

Рассмотрим следующий пример:

>>> class A(object): . x = 1 . >>> class B(A): . pass . >>> class C(A): . pass . >>> print A.x, B.x, C.x 1 1 1

Вроде все в порядке.

>>> B.x = 2 >>> print A.x, B.x, C.x 1 2 1

Ага, все как и ожидалось.

>>> A.x = 3 >>> print A.x, B.x, C.x 3 2 3

Что за черт?! Мы же только изменили A.x. Почему же C.x тоже изменилось?

В Python переменные класса обрабатываются как словари и следуют тому, что часто называют Порядком разрешения методов (MRO). Таким образом, в приведенном выше коде, поскольку атрибут x не найден в классе C, он будет найден в его базовых классах (только A в приведенном выше примере, хотя Python поддерживает множественное наследование). Другими словами, C не имеет своего собственного свойства x, независимого от A. Таким образом, ссылки на C.x фактически являются ссылками на A.x. Это будет вызывать проблемы, если не обрабатывать такие случаи должным образом. Так что при изучении Python обратите особое внимание на аттрибуты класса и работу с ними.

Ошибка № 3: неправильное указание параметров для блока исключения

Предположим, что у вас есть следующий кусок кода:

>>> try: . l = ["a", "b"] . int(l[2]) . except ValueError, IndexError: # To catch both exceptions, right? . pass . Traceback (most recent call last): File "", line 3, in IndexError: list index out of range

Проблема здесь заключается в том, что выражение except не принимает список исключений, указанных таким образом. Скорее, в Python 2.x выражение «except Exception, e» используется для привязки исключения к необязательному второму заданному второму параметру (в данном случае e), чтобы сделать его доступным для дальнейшей проверки. В результате в приведенном выше коде исключение IndexError не перехватывается выражением except; скорее, вместо этого исключение заканчивается привязкой к параметру с именем IndexError.

Правильный способ перехвата нескольких исключений с помощью выражения except — указать первый параметр в виде кортежа, содержащего все исключения, которые нужно перехватить. Кроме того, для максимальной совместимости используйте ключевое слово as, так как этот синтаксис поддерживается как в Python 2, так и в Python 3:

>>> try: . l = ["a", "b"] . int(l[2]) . except (ValueError, IndexError) as e: . pass . >>>

Ошибка № 4: непонимание правил области видимости Python

Область видимости в Python основана на так называемом правиле LEGB, которое является аббревиатурой Local (имена, назначенные любым способом внутри функции (def или lambda), и не объявленные глобальными в этой функции), Enclosing (имя в локальной области действия любых статически включающих функций (def или lambda), от внутреннего к внешнему), Global (имена, назначенные на верхнем уровне файла модуля, или путем выполнения global инструкции в def внутри файла), Built-in (имена, предварительно назначенные в модуле встроенных имен: open, range, SyntaxError . ). Кажется достаточно просто, верно? Ну, на самом деле, есть некоторые тонкости в том, как это работает в Python, что подводит нас к общей более сложной проблеме программирования на Python ниже. Рассмотрим следующей пример:

>>> x = 10 >>> def foo(): . x += 1 . print x . >>> foo() Traceback (most recent call last): File "", line 1, in File "", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment

Вышеуказанная ошибка возникает потому, что, когда вы присваиваете переменную в области видимости, Python автоматически считает ее локальной для этой области и скрывает любую переменную с аналогичным именем в любой вышестоящей области.

Таким образом, многие удивляются, когда получают UnboundLocalError в ранее работающем коде, когда он модифицируется путем добавления оператора присваивания где-нибудь в теле функции.

Эта особенность особенно сбивает разработчиков с толку при использовании списков. Рассмотрим следующий пример:

>>> lst = [1, 2, 3] >>> def foo1(): . lst.append(5) # Это работает нормально. . >>> foo1() >>> lst [1, 2, 3, 5] >>> lst = [1, 2, 3] >>> def foo2(): . lst += [5] # . а вот это падает! . >>> foo2() Traceback (most recent call last): File "", line 1, in File "", line 2, in foo UnboundLocalError: local variable 'lst' referenced before assignment

А? Почему foo2 падает, в то время как foo1 работает нормально?

Ответ такой же, как в предыдущем примере, но, по распространенному мнению, здесь ситуация более тонкая. foo1 не применяет оператор присваивания к lst, тогда как foo2 — да. Помня, что lst + = [5] на самом деле является просто сокращением для lst = lst + [5], мы видим, что мы пытаемся присвоить значение lst (поэтому Python предполагает, что он находится в локальной области видимости). Однако значение, которое мы хотим присвоить lst, основано на самом lst (опять же, теперь предполагается, что он находится в локальной области видимости), который еще не был определен. И мы получаем ошибку.

Ошибка № 5: изменение списка во время итерации по нему

Проблема в следующем куске кода должна быть достаточно очевидной:

>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): . if odd(numbers[i]): . del numbers[i] # BAD: Deleting item from a list while iterating over it . Traceback (most recent call last): File "", line 2, in IndexError: list index out of range

Удаление элемента из списка или массива во время итерации по нему — это проблема Python, которая хорошо известна любому опытному разработчику программного обеспечения. Но, хотя приведенный выше пример может быть достаточно очевидным, даже опытные разработчики могут встать на эти грабли в гораздо более сложном коде.

К счастью, Python включает в себя ряд элегантных парадигм программирования, которые при правильном использовании могут привести к значительному упрощению и оптимизации кода. Дополнительным приятным следствием этого является то, что в более простом коде вероятность попасться на ошибку случайного удаления элемента списка во время итерации по нему значительно меньше. Одна из таких парадигм — генераторы списков. Кроме того, понимание работы генераторов списков особенно полезны для избежания этой конкретной проблемы, как показано в этой альтернативной реализацией приведенного выше кода, которая прекрасно работает:

>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)] # ahh, the beauty of it all >>> numbers [0, 2, 4, 6, 8]

Ошибка № 6: непонимание того, как Python связывает переменные в замыканиях

Рассмотрим следующий пример:

>>> def create_multipliers(): . return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): . print multiplier(2) . 

Вы можете ожидать следующий вывод:

0 2 4 6 8

Но на самом деле вы получите вот что:

8 8 8 8 8 

Это происходит из-за поздней привязки в Python, которое заключается в том, что значения переменных, используемых в замыканиях, ищутся во время вызова внутренней функции. Таким образом, в приведенном выше коде всякий раз, когда вызывается какая-либо из возвращаемых функций, значение i ищется в окружающей области видимости во время ее вызова (а к тому времени цикл уже завершился, поэтому i уже был присвоен конечный результат — значение 4).

Решение этой распространенной проблемы с Python будет таким:

>>> def create_multipliers(): . return [lambda x, i=i : i * x for i in range(5)] . >>> for multiplier in create_multipliers(): . print multiplier(2) . 0 2 4 6 8

Вуаля! Мы используем здесь аргументы по умолчанию для генерации анонимных функций для достижения желаемого поведения. Некоторые назвали бы это решение элегантным. Некоторые —
тонким. Некоторые ненавидят подобные штуки. Но если вы разработчик Python, в любом случае, это важно понимать.

Ошибка № 7: создание циклических зависимостей модуля

Допустим, у вас есть два файла, a.py и b.py, каждый из которых импортирует другой, следующим образом:

import b def f(): return b.x print f()
import a x = 1 def g(): print a.f()

Сначала попробуем импортировать a.py:

>>> import a 1

Сработало просто отлично. Возможно, это вас удивляет. В конце концов, модули циклически импортируют друг друга и это, вероятно, должено быть проблемой, не так ли?

Ответ заключается в том, что простое наличие циклического импорта модулей само по себе не является проблемой в Python. Если модуль уже был импортирован, Python достаточно умен, чтобы не пытаться повторно импортировать его. Однако, в зависимости от точки, в которой каждый модуль пытается получить доступ к функциям или переменным, определенным в другом, вы действительно можете столкнуться с проблемами.

Итак, возвращаясь к нашему примеру, когда мы импортировали a.py, у него не было проблем с импортом b.py, поскольку b.py не требует, чтобы что-либо из a.py было определено во время его импорта. Единственная ссылка в b.py на a — это вызов a.f(). Но этот вызов в g() и ничего в a.py или b.py не вызывает g(). Так что все работает прекрасно.

Но что произойдет, если мы попытаемся импортировать b.py (без предварительного импорта a.py, то есть):

>>> import b Traceback (most recent call last): File "", line 1, in File "b.py", line 1, in import a File "a.py", line 6, in print f() File "a.py", line 4, in f return b.x AttributeError: 'module' object has no attribute 'x'

Ой-ой. Это не хорошо! Проблема здесь в том, что в процессе импорта b.py он пытается импортировать a.py, который, в свою очередь, вызывает f(), который пытается получить доступ к b.x. Но b.x еще не было определено. Отсюда исключение AttributeError.

По крайней мере, одно из решений этой проблемы довольно тривиально. Просто измените b.py, чтобы импортировать a.py в g():

x = 1 def g(): import a # This will be evaluated only when g() is called print a.f()

Теперь, когда мы его импортируем, все нормально:

>>> import b >>> b.g() 1 # Printed a first time since module 'a' calls 'print f()' at the end 1 # Printed a second time, this one is our call to 'g' 

Ошибка № 8: пересечение имен с именами модулями стандартной библиотеки Python

Одна из прелестей Python — это множество модулей, которые поставляются «из коробки». Но в результате, если вы сознательно не будете за этим следить, можно столкнуться с тем, что имя вашего модуля может быть с тем же именем, что и модуль в стандартной библиотеке, поставляемой с Python (например, в вашем коде может быть модуль с именем email.py, который будет конфликтовать со модулем стандартной библиотеки с таким же именем).

Это может привести к серьезным проблемам. Например, если какой-нибудь из модулей будет пытаться импортировать версию модуля из стандартной библиотеки Python, а у вас в проекте будет модуль с таким же именем, который и будет по ошибке импортирован вместо модуля из стандартной библиотеки.

Поэтому следует проявлять осторожность, чтобы не использовать те же имена, что и в модулях стандартной библиотеки Python. Гораздо проще изменить название модуля в своем проекте, нежели подать запрос на изменение имени модуля в стандартной библиотеке и получить на него одобрение.

Ошибка № 9: неспособность учесть различия Python 2 и Python 3

Рассмотрим следующий файл foo.py:

import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()

На Python 2 он отработает нормально:

$ python foo.py 1 key error 1 $ python foo.py 2 value error 2

Но теперь давайте посмотрим как он будет работать в Python 3:

$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment

Что здесь только что произошло? «Проблема» в том, что в Python 3 объект в блоке исключения недоступен за его пределами. (Причина этого заключается в том, что в противном случае объекты в этом блоке будут сохраняться в памяти до тех пор, пока сборщик мусора не запустится и не удалит ссылки на них оттуда).

Один из способов избежать этой проблемы — сохранить ссылку на объект блока исключения за пределами этого блока, чтобы он оставался доступным. Вот версия предыдущего примера, которая использует эту технику, тем самым получая код, который подходит как для Python 2, так и для Python 3:

import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()

Запустим его в Python 3:

$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2

Ошибка № 10: неправильное использование метода __del__

Допустим, у вас есть вот такой файл mod.py:

import foo class Bar(object): . def __del__(self): foo.cleanup(self.myhandle)

И вы пытаетесь сделать вот такое из другого another_mod.py:

import mod mybar = mod.Bar()

И получите ужасный AttributeError.

Почему? Потому что, как сообщается здесь, когда интерпретатор отключается, глобальные переменные модуля все имеют значение None. В результате в приведенном выше примере, в момент вызова __del__, имя foo уже было установлено в None.

Решением этой «задачи со звездочкой» будет использование atexit.register(). Таким образом, когда ваша программа завершает выполнение (то есть при нормальном выходе из нее), ваши handle’ы удаляются до того, как интерпретатор звершает работу.

С учетом этого, исправление для приведенного выше кода mod.py может выглядеть примерно так:

import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): . atexit.register(cleanup, self.myhandle)

Подобная реализация обеспечивает простой и надежный способ вызова любой необходимой очистки после обычного завершения программы. Очевидно, что решение о том, как поступить с объектом, который связан с имненем self.myhandle, остается за foo.cleanup, но, думаю, идею вы поняли.

Заключение

Python — это мощный и гибкий язык со множеством механизмов и парадигм, которые могут значительно повысить производительность. Однако, как и в случае с любым программным инструментом или языком, при ограниченном понимании или оценке его возможностей, при разработке могут возникать непредвиденные проблемы.

Ознакомление с нюансами Python, затронутыми в этой статье, поможет оптимизировать использование языка, избегая при этом некоторых распространенных ошибок.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *