Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
скришнот из metal slug 3
2020 год — это определенно год странностей. Мой код тоже часто включает в себя некоторые странные ошибки. И в данном посте я хочу показать вам несколько методов отладки кода на языке julia.
Я ни с какой стороны не профессионал в этом деле, да и это справедливо для всего, о чем я пишу в блоге, так что просто имейте это в виду… Ну, на самом деле некоторые из вас платят за мою работу, так что технически я могу назвать себя профессиональным блогером, не так ли?
Во всяком случае, давайте не будем отвлекаться на эту мысль. Добро пожаловать в мой блог, если вы новичок, и добро пожаловать обратно в противном случае. Хорошо, что ваш компьютер запрашивает что-то с моего сервера.
Я предполагаю, что у вас есть некоторые базовые знания о Джулии. Следующие посты могут дать вам основы, если вы заинтересованы:
- Множественная диспетчеризация или что в Джулии особенного
- Работа с REPL
- Как работать с пакетами
Кроме того, нужно знание базового синтаксиса.
Пример кода
Прежде чем мы начнем отладку, я хочу продемонстрировать это на некотором коде. Он достаточно короткий, чтобы показать его здесь, и содержит по крайней мере одну ошибку.
В качестве примера возьмем задачку ProjectEuler problem #21. Можете попробовать решить сами. Тут будет начало реализации возможной наивной версии.
Задача заключается в следующем: мы ищем дружественные числа меньше 10 000. Дружественное число определяется как элемент дружественной пары…
Пара двух целых чисел (a,b)
дружна, если d(a) = b
и d(b) = a
, где d
— сумма делителей, так что d(4) = 1+2 = 3
.
Дана дружная пара — a = 220
и b = 284
.
Мы могли бы начать с функции, которая просто берет пару и решает, является ли она дружественной.
function is_amicable(a, b)
sum_divisors(a) == b && sum_divisors(b) == a
end
Джулия всегда возвращает выходные данные последнего выполненного выражения в функции. Это означает, что ключевое слово return
не обязательно.
Затем нам понадобится функция sum_divisors
function sum_divisors(a)
result = 0
for i = 1:a
if a % i == 0
result += i
end
end
return result
end
которая вызывается так
julia> is_amicable(220, 284)
false
Возможно, вы заметили ошибку, но если нет, то, вероятно, лучше не искать ее сейчас. Вместо этого следуйте по пути отладки.
Отладка с помощью Debugger.jl в REPL
Этот пост показывает вам два различных варианта отладки, и первый вариант может быть выполнен в REPL или в вашей IDE, то есть VSCode.
В этом разделе я объясню, как работать с отладчиком на REPL. (Debugger.jl)
julia> ] add Debugger
julia> using Debugger
Вы можете глянуть в пост про менеджер пакетов, если что-то не ясно.
julia> @enter is_amicable(220, 284)
In is_amicable(a, b) at REPL[7]:1
1 function is_amicable(a, b)
>2 sum_divisors(a) == b && sum_divisors(b) == a
3 end
About to run: (sum_divisors)(220)
1|debug>
Я набил @enter is_amicable(220, 284)
, чтобы получить этот вывод. Кстати, я только что скопировал две функции, которые я определил ранее, в REPL. С другой стороны, Вы можете создать для этого файл amicable.jl
и использовать Revise и include
(см. REPL and Revise.jl).
В случае файла номера строк, вероятно, более полезны.
Я вернусь через секунду...
julia> using Revise
julia> includet("amicable.jl")
julia> using Debugger
julia> @enter is_amicable(220, 284)
In is_amicable(a, b) at /home/ole/Julia/opensources/blog/2020-10-27-basics-debugging/amicable.jl:1
1 function is_amicable(a, b)
>2 sum_divisors(a) == b && sum_divisors(b) == a
3 end
About to run: (sum_divisors)(220)
1|debug>
Готово. Хорошо, теперь как уже упоминалось, в конце мы собираемся запустить sum_divisors(220)
.
Последняя строка 1|debug>
дает нам возможность исследовать дальше, прыгая по коду, в том числе и низкоуровневому, и много чего еще всякого интересного.
Можно посмотреть полный список команд: Debugger.jl commands
Вы также можете ввести ?
в режиме отладчика и нажать клавишу enter, чтобы увидеть список команд
Давайте начнем с n
— шаг к следующей строке.
1|debug> n
In is_amicable(a, b) at /home/ole/Julia/opensources/blog/2020-10-27-basics-debugging/amicable.jl:1
1 function is_amicable(a, b)
>2 sum_divisors(a) == b && sum_divisors(b) == a
3 end
About to run: return false
Значит sum_divisors(220) != 284
. Мы, вероятно, хотим перейти к вызову sum_divisors(220)
.
Мы всегда можем выпрыгнуть из сеанса отладки с помощью q
, а затем начать все сначала
Начнем снова с @enter is_amicable(220, 284)
и используем s
для шага в функцию
1|debug> s
In sum_divisors(a) at /home/ole/Julia/opensources/blog/2020-10-27-basics-debugging/amicable.jl:5
5 function sum_divisors(a)
> 6 result = 0
7 for i = 1:a
8 if a % i == 0
9 result += i
10 end
About to run: 0
1|debug>
А дальше продолжаем использовать n
, но вы, вероятно, можете себе представить, что это займет некоторое время.
Какие еще инструменты у нас есть, чтобы проверить, что происходит?
Некоторые из вас могут подумать: Хорошо, мы должны, по крайней мере, выяснить, что мы возвращаем, и мы можем просто вызвать sum_divisors(220)
. Это, вероятно, правильно, но не показывает возможности отладчика. Давайте представим, что мы имеем доступ только к режиму отладчика и не можем просто вызвать функцию.
В общем, этот способ узнавания нового, скрывая то, что мы уже знаем, довольно эффективен.
Я думаю, что пришло время, чтобы представить силу точек останова.
Вместо того, чтобы ползти по программе строка за строкой, часто разумно перейти к определенной точке, запустив код до тех пор, пока эта точка не будет достигнута.
Вы можете сделать это с помощью bp add
, а затем указать файл, номер строки и возможное условие. Вы можете увидеть все параметры с помощью ?
в режиме отладки.
В наших интересах будет поставить bp add 12
. После этого мы можем использовать команду c
, которая расшифровывается как continue (до точки останова).
1|debug> c
Hit breakpoint:
In sum_divisors(a) at /home/ole/Julia/opensources/blog/2020-10-27-basics-debugging/amicable.jl:5
8 if a % i == 0
9 result += i
10 end
11 end
>12 return result
13 end
About to run: return 504
Итак, теперь мы знаем, что оно возвращает 504
вместо 284
. Теперь мы можем использовать `
, чтобы перейти в режим Джулии. (Я знаю, что это вроде как запрещено нашими правилами, но время от времени это имеет смысл, и мы видим, что мы находимся в 1|julia>
, а не в julia>
, так что я думаю, что все в порядке...)
504-284
— это не самый сложный расчет, но мы можем использовать julia, чтобы сделать это за нас, не выходя полностью из режима отладки, используя:
1|debug> `
1|julia> 504-284
220
Похоже, мы нашли ошибку. Мы добавляем само число к результату, но оно на самом деле не считается за множитель.
А это значит, что мы можем сделать:
function sum_divisors(a)
result = 0
#for i = 1:a
for i = 1:a-1
if a % i == 0
result += i
end
end
return result
end
чтобы избежать эту проблему.
Да, я знаю, что мы можем избежать большего количества чисел, чтобы быть быстрее
Мы можем выйти из режима вычислений с помощью backspace, а затем q
, чтобы выйти из режима отладки. Запускаем
julia> is_amicable(220, 284)
true
и видим, что мы выковыряли этот баг.
Давайте запустим его в последний раз в сеансе отладки и посмотрим на переменные. Снова перейдем к точке останова c
и запустим
1|debug> w add i
1] i: 219
1|debug> w add a
1] i: 219
2] a: 220
Теперь мы видим переменные. Если мы снова нажмем c
, то снова перейдем к точке разрыва (для очередного вычисления sum_divisors(284) == 220
).
Мы можем снова использовать букву w
, чтобы увидеть список переменных в области видимости:
1|debug> w
1] i: 283
2] a: 284
Есть еще несколько способов поиграть, например, шагнуть в код, показать низкоуровневый код и многое другое. Этого должно быть достаточно для знакомства.
В следующем разделе я хочу привести вам тот же пример с помощью редактора кода visual studio с расширением julialang.
Использование VSCode
Я думаю, что большинство разработчиков Julia используют VSCode IDE и, по крайней мере, иногда, vim, emacs или еще что-то такое неудобное… Ладно, это, наверное, просто слишком неудобно для меня
Определенно пришло время переключиться на VSCode с Atom/Juno, поскольку расширение Julia теперь разработано для VSCode вместо Atom.
Поскольку это IDE, имеет смысл иметь более визуальный отладчик, чем тот, который описан в предыдущем разделе.
Он довольно прост в навигации, и по умолчанию вы получаете больше выходных данных.
Чтобы начать сеанс отладки, вы нажимаете на кнопку с ошибкой и воспроизводите знак слева, пока у вас открыт файл julia.
Я добавил последнюю строку is_amicable(220, 284)
, так как VSCode просто запускает программу.
Вы можете добавить точку останова, щелкнув слева от номера каждой строки.
Я сделал снимок экрана после того, как сделал эти шаги, и последним шагом было нажатие на кнопку отладки.
Через несколько секунд сеанс отладки приостанавливается по мере достижения точки останова. С левой стороны можно увидеть локальные переменные в этой позиции. Это этап после того, как я исправил ошибку, так что вы можете видеть, что возвращается правильный результат "284". Однако вы также получаете значение для a
и i
.
Короче, все то же, что мы делали раньше с нашими переменными, но там нам пришлось вручную добавлять их.
Теперь мы также можем вручную добавлять выражения для наблюдения. Это можно сделать в части Watch
ниже Variables
, которая находится за пределами скриншота. Довольно приятно иметь возможность добавлять точки останова одним щелчком мыши, а также иметь локальные переменные, показанные слева по умолчанию.
Вы можете спросить себя: Ну, на самом деле это не два способа отладки, не так ли? Это примерно одно и то же, только с другим графическим интерфейсом.
Это правда! Вот почему я сейчас перехожу к следующему разделу поста
Infiltrator.jl для скорости
Существует одна огромная проблема с отладчиком Julia, которая решается по-разному различными пакетами. Проблема в том, что отладчик работает в интерпретируемом режиме, что делает его очень медленным. Если вы отлаживали код C++, то знаете, что отладчик там тоже работает медленнее, чем выполнение, но для Джулии это, на мой взгляд, огромная проблема.
Можно перейти в скомпилированный режим с отладчиком, но это экспериментально, и, по крайней мере, для меня он никогда не останавливался на точке останова.
Некоторые другие пакеты пытаются исправить эту проблему, делая какую-то причудливую магию, но я лично большой поклонник Infiltrator.jl. Правда, некоторое время не было никаких обновлений, и у меня есть некоторые проблемы с ним, но мне нравится сама идея.
Это также один из тех проектов с менее чем 100 звездами. Я хочу подтолкнуть его к этой вехе, так что если вам нравится то, что вы видите в этом разделе, пожалуйста, щелкните им звездочку.
Infiltrator.jl идет совершенно другим путем. Прежде всего, вам нужно немного изменить свой код. Он предоставляет макрос @infiltrate
. О боже, как я люблю это название
Макрос примерно такой же, как и предыдущая точка останова. Всякий раз, когда достигается нужная строка, открывается новый вид режима REPL. Это немного усложняет переключение между режимом отладки и обычным режимом запуска, так как вам нужно добавить или удалить макросы @infiltrate
, но я думаю, что это нормально.
Я снова продемонстрирую это на примере разобранном выше. Подобного рода использование было в debugging ConstraintSolver.jl.
Я скопировал код сверху и просто добавил using Infiltrator
и @infiltrate
.
using Infiltrator
function is_amicable(a, b)
sum_divisors(a) == b && sum_divisors(b) == a
end
function sum_divisors(a)
result = 0
for i = 1:a-1
if a % i == 0
result += i
end
end
@infiltrate
return result
end
is_amicable(220, 284)
При запуске кода с include("amicable.jl")
получаем:
Hit `@infiltrate` in sum_divisors(::Int64) at amicable.jl:14:
debug>
Это означает, что мы знаем, какая точка останова была достигнута, и видим тип переменной, которую мы назвали sum_divisors
. Однако в отличие от Debugger.jl мы не видим кода.
Вы можете снова увидеть раздел справки с помощью ?
debug> ?
Code entered is evaluated in the current function's module. Note that you cannot change local
variables.
The following commands are special cased:
- `@trace`: Print the current stack trace.
- `@locals`: Print local variables.
- `@stop`: Stop infiltrating at this `@infiltrate` spot.
Exit this REPL mode with `Ctrl-D`, and clear the effect of `@stop` with `Infiltrator.clear_stop()`.
Существует не так уж много команд, поэтому мы можем просто попробовать их одну за другой:
debug> @trace
[1] sum_divisors(::Int64) at amicable.jl:14
[2] is_amicable(::Int64, ::Int64) at amicable.jl:4
[3] top-level scope at amicable.jl:18
[4] include(::String) at client.jl:457
Таким образом, мы пришли из is_amicable
и можем видеть типы, а также имя файла и номер строки, что полезно при использовании multiple dispatch.
debug> @locals
- result::Int64 = 284
- a::Int64 = 220
мы можем видеть локальные переменные, которые похожи на те, которые мы видели в представлении переменных VSCode.
Кроме того, мы можем просто вычислять выражения прям в этом режиме. Для Infiltrator.jl нет необходимости использовать `
, чтобы переключиться на вычисления.
debug> a == 220
true
Вы можете использовать @stop
, чтобы больше не останавливаться на этой вехе, и Infiltrator.clear_stop()
, чтобы очистить эти остановки.
Давайте не будем использовать @stop
сейчас, а вместо этого перейдем к следующей точке @infiltrate
с помощью CTRL-D
:
Hit `@infiltrate` in sum_divisors(::Int64) at amicable.jl:14:
debug>
таким образом, мы находимся в той же точке останова, но со вторым вызовом. К сожалению, невозможно использовать клавишу со стрелкой вверх, чтобы перейти через историю команд, которые мы использовали, так что нам нужно снова ввести @locals
, если мы хотим их увидеть.
Я открыл такую тему и попытался разрешить ее сам, но мне это не удалось. Было бы здорово, если бы это когда-нибудь было реализовано, потому что я думаю, что было бы очень полезно отлаживать быстрее таким образом.
Давайте рассмотрим сравнение двух различных способов в следующем разделе.
Выводы
Мы посмотрели на Debugger. jl, который дает вам всю информацию, которая может понадобиться в вашем REPL.
Поэтому он не зависит от редактора.
Следующий инструмент, который я упомянул, был дебагер в VSCode который является в основном просто графическим интерфейсом для Debugger.jl. Вероятно, его удобнее использовать людям, которые любят работать с IDE. Хотя в Debugger.jl могут быть некоторые опции, которые недоступны в графическом интерфейсе, как это часто бывает.
Оба этих инструмента дают то преимущество, что вы можете шаг за шагом переходить через свой код и исследовать все, что захотите. Вы можете взглянуть на низкоуровневый код (по крайней мере, в Debugger.jl). Это, вероятно, то, что каждый ожидает сделать с отладчиком. Проблема просто в том, что он слишком медленный во многих случаях использования, например, когда вы хотите отладить свой собственный пакет с 1000 строками кода.
В таком случае Infiltrator.jl — это путь, по крайней мере для меня, и пока скомпилированный режим Debugger.jl работает недостаточно хорошо. У него есть и другие недостатки, так как не бывает чтоб все и сразу, но я думаю, что он часто превосходит использование println
, поскольку можно распечатать все, что в данный момент интересует в данной точке останова, и увидеть все локальные переменные за один раз.
Спасибо за то, что дочитали и особая благодарность моим 10 покровителям!
Я буду держать вас в курсе Twitter OpenSourcES.