Как стать автором
Обновить

Debugging в Julia — два способа

Время на прочтение9 мин
Количество просмотров2.5K
Автор оригинала: Ole Kröger


скришнот из metal slug 3


2020 год — это определенно год странностей. Мой код тоже часто включает в себя некоторые странные ошибки. И в данном посте я хочу показать вам несколько методов отладки кода на языке julia.


Я ни с какой стороны не профессионал в этом деле, да и это справедливо для всего, о чем я пишу в блоге, так что просто имейте это в виду… Ну, на самом деле некоторые из вас платят за мою работу, так что технически я могу назвать себя профессиональным блогером, не так ли?


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


Я предполагаю, что у вас есть некоторые базовые знания о Джулии. Следующие посты могут дать вам основы, если вы заинтересованы:



Кроме того, нужно знание базового синтаксиса.


Пример кода


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


В качестве примера возьмем задачку 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.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+6
Комментарии0

Публикации

Изменить настройки темы

Истории

Ближайшие события