Spec-Zone.ru › Julia 1.5

Поток управления

Julia предоставляет различные конструкции потока управления:

  • Составные выражения: begin и ;.
  • Условное выполнение: if-elseif-else и ?: (тернарный оператор).
  • Короткое замыкание: &&, || и цепочечные сравнения.
  • Повторное выполнение: циклы: while и for.
  • Обработка исключений: try-catch, error и throw.
  • Задачи (иначе корутины): yieldto.

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

Составные выражения

Иногда удобно иметь одно выражение, которое оценивает несколько подвыражений в определённом порядке, возвращая значение последнего подвыражения как своё значение. В Julia существуют две конструкции для этого: begin блоки и ; цепочки. Значение обеих конструкций составных выражений равно значению последнего подвыражения. Вот пример begin блока:

julia> z = begin
           x = 1
           y = 2
           x + y
       end
3

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

julia> z = (x = 1; y = 2; x + y)
3

Этот синтаксис особенно полезен с краткой формой определения функций на одной строке, введённой в Функциях. Хотя это типично, нет требования, чтобы begin блоки были многострочными, или чтобы ; цепочки были однострочными:

julia> begin x = 1; y = 2; x + y end
3

julia> (x = 1;
        y = 2;
        x + y)
3

Условное выполнение

Условное выполнение позволяет оценивать части кода или нет в зависимости от значения булевого выражения. Вот анатомия синтаксиса условного оператора if-elseif-else:

if x < y
    println("x is less than y")
elseif x > y
    println("x is greater than y")
else
    println("x is equal to y")
end

Если выражение условия x < y имеет значение true, то соответствующий блок оценивается; в противном случае выражение условия x > y оценивается, и если оно имеет значение true, то соответствующий блок оценивается; если ни одно выражение не истинно, то else блок оценивается. Вот пример в действии:

julia> function test(x, y)
           if x < y
               println("x is less than y")
           elseif x > y
               println("x is greater than y")
           else
               println("x is equal to y")
           end
       end
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

elseif и else блоки являются необязательными, и может быть использовано любое количество elseif блоков. Выражения условия в конструкции if-elseif-else оцениваются до тех пор, пока первое из них не примет значение true, после чего оценивается связанный блок, и дальнейшие выражения условия или блоки не оцениваются.

if блоки «просачиваются», т. е. они не вводят локальную область видимости. Это означает, что новые переменные, определённые внутри if блоков, могут быть использованы после if блока, даже если они не были определены ранее. Таким образом, мы могли бы определить функцию test выше как

julia> function test(x,y)
           if x < y
               relation = "less than"
           elseif x == y
               relation = "equal to"
           else
               relation = "greater than"
           end
           println("x is ", relation, " y.")
       end
test (generic function with 1 method)

julia> test(2, 1)
x is greater than y.

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

julia> function test(x,y)
           if x < y
               relation = "less than"
           elseif x == y
               relation = "equal to"
           end
           println("x is ", relation, " y.")
       end
test (generic function with 1 method)

julia> test(1,2)
x is less than y.

julia> test(2,1)
ERROR: UndefVarError: relation not defined
Stacktrace:
 [1] test(::Int64, ::Int64) at ./none:7

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

julia> x = 3
3

julia> if x > 0
           "positive!"
       else
           "negative..."
       end
"positive!"

Обратите внимание, что очень короткие условные операторы (однострочные) часто выражаются с помощью короткого замыкания в Julia, как описано в следующем разделе.

В отличие от C, MATLAB, Perl, Python и Ruby, но как Java и некоторых других языков со строгим типом, ошибка возникает, если значение выражения условия отличается от true или false:

julia> if 1
           println("true")
       end
ERROR: TypeError: non-boolean (Int64) used in boolean context

Эта ошибка указывает, что условное выражение имеет неверный тип: Int64 вместо требуемого Bool.

Так называемый "тернарный оператор", ?:, тесно связан с синтаксисом if-elseif-else, но используется там, где требуется условный выбор между значениями одного выражения, а не условное выполнение более длинных блоков кода. Он получил своё название от того, что в большинстве языков это единственный оператор, принимающий три операнда:

a ? b : c

Выражение a перед ? является выражением условия, и тернарная операция оценивает выражение b перед :, если условие a имеет значение true, или выражение c после :, если оно имеет значение false. Обратите внимание, что пробелы вокруг ? и : являются обязательными: выражение, например, a?b:c, не является допустимым тернарным выражением (но новая строка допустима после ? и :).

Самый простой способ понять это поведение — посмотреть пример. В предыдущем примере вызов println используется во всех трёх ответвлениях: единственный реальный выбор — какая строковая литерал печатать. Это можно было бы записать более лаконично, используя тернарный оператор. Для большей ясности давайте сначала попробуем двухоператорную версию:

julia> x = 1; y = 2;

julia> println(x < y ? "less than" : "not less than")
less than

julia> x = 1; y = 0;

julia> println(x < y ? "less than" : "not less than")
not less than

Если выражение x < y истинно, то всё выражение тернарного оператора принимает значение строки "less than", а в противном случае — строки "not less than". Исходный трёхсторонний пример требует объединения нескольких использований тернарного оператора.

julia> test(x, y) = println(x < y ? "x is less than y"    :
                            x > y ? "x is greater than y" : "x is equal to y")
test (generic function with 1 method)

julia> test(1, 2)
x is less than y

julia> test(2, 1)
x is greater than y

julia> test(1, 1)
x is equal to y

Для облегчения объединения оператор ассоциируется справа налево.

Важно, что, как и в if-elseif-else, выражения перед и после : оцениваются только если выражение условия имеет значение true или false соответственно:

julia> v(x) = (println(x); x)
v (generic function with 1 method)

julia> 1 < 2 ? v("yes") : v("no")
yes
"yes"

julia> 1 > 2 ? v("yes") : v("no")
no
"no"

Короткое замыкание

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

  • В выражении a && b подвыражение b оценивается только в том случае, если a имеет значение true.
  • В выражении a || b подвыражение b оценивается только в том случае, если a имеет значение false.

Пояснение состоит в том, что a && b должно быть равно false, если a равно false, независимо от значения b, и аналогично, значение a || b должно быть истинным, если a имеет значение true, независимо от значения b. Оба && и || ассоциируются справа налево, но && имеет более высокий приоритет, чем ||. Экспериментировать с этим поведением легко:

julia> t(x) = (println(x); true)
t (generic function with 1 method)

julia> f(x) = (println(x); false)
f (generic function with 1 method)

julia> t(1) && t(2)
1
2
true

julia> t(1) && f(2)
1
2
false

julia> f(1) && t(2)
1
false

julia> f(1) && f(2)
1
false

julia> t(1) || t(2)
1
true

julia> t(1) || f(2)
1
true

julia> f(1) || t(2)
1
2
true

julia> f(1) || f(2)
1
2
false

Вы легко можете провести аналогичные эксперименты с ассоциативностью и приоритетом различных комбинаций операторов && и ||.

Это поведение часто используется в Julia для создания альтернативы очень коротким if операторам. Вместо if <cond> <statement> end можно написать <cond> && <statement> (что можно прочитать как: <условие> и затем <операция>). Аналогично, вместо if ! <cond> <statement> end можно написать <cond> || <statement> (что можно прочитать как: <условие> или же <операция>).

Например, рекурсивную функцию факториала можно определить следующим образом:

julia> function fact(n::Int)
           n >= 0 || error("n must be non-negative")
           n == 0 && return 1
           n * fact(n-1)
       end
fact (generic function with 1 method)

julia> fact(5)
120

julia> fact(0)
1

julia> fact(-1)
ERROR: n must be non-negative
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fact(::Int64) at ./none:2
 [3] top-level scope

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

julia> f(1) & t(2)
1
2
false

julia> t(1) | t(2)
1
2
true

Точно так же, как выражения условия, используемые в if, elseif или тернарном операторе, операнды && или || должны быть булевыми значениями (true или false). Использование значения, отличного от булева, где угодно, кроме последнего элемента в цепочке условных выражений, является ошибкой:

julia> 1 && true
ERROR: TypeError: non-boolean (Int64) used in boolean context

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

julia> true && (x = (1, 2, 3))
(1, 2, 3)

julia> false && (x = (1, 2, 3))
false

Повторное выполнение: циклы

Существуют две конструкции для повторного выполнения выражений: цикл while и цикл for. Вот пример цикла while:

julia> i = 1;

julia> while i <= 5
           println(i)
           global i += 1
       end
1
2
3
4
5

Цикл while оценивает выражение условия (i <= 5 в данном случае), и до тех пор, пока оно остается true, продолжает также оценивать тело цикла while. Если выражение условия является false при первом достижении цикла while, тело никогда не оценивается.

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

julia> for i = 1:5
           println(i)
       end
1
2
3
4
5

Здесь 1:5 — это объект диапазона, представляющий последовательность чисел 1, 2, 3, 4, 5. Цикл for перебирает эти значения, присваивая каждое из них по очереди переменной i. Существенное различие между предыдущей формой цикла while и формой цикла for — это область видимости переменной. Если переменная i не была введена в другой области видимости, в форме цикла for она видна только внутри цикла for, а не вне/потом. Для проверки этого вам потребуется либо новый экземпляр интерактивной сессии, либо другое имя переменной:

julia> for j = 1:5
           println(j)
       end
1
2
3
4
5

julia> j
ERROR: UndefVarError: j not defined

Подробное объяснение области видимости переменных и ее работы в Julia см. в разделе Область видимости переменных.

В общем, конструкция цикла for может перебирать любой контейнер. В этих случаях обычно используется альтернативный (но полностью эквивалентный) ключевой запрос in или ∈ вместо =, так как это делает код более понятным:

julia> for i in [1,4,0]
           println(i)
       end
1
4
0

julia> for s ∈ ["foo","bar","baz"]
           println(s)
       end
foo
bar
baz

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

Иногда удобно прервать повторение цикла while до того, как условие проверки станет ложным, или остановить итерацию в цикле for до достижения конца итерируемого объекта. Это можно сделать с помощью ключевого слова break:

julia> i = 1;

julia> while true
           println(i)
           if i >= 5
               break
           end
           global i += 1
       end
1
2
3
4
5

julia> for j = 1:1000
           println(j)
           if j >= 5
               break
           end
       end
1
2
3
4
5

Без ключевого слова break, цикл while выше никогда не завершится самостоятельно, а цикл for будет выполняться до 1000 раз. Оба цикла завершаются преждевременно с помощью использования break.

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

julia> for i = 1:10
           if i % 3 != 0
               continue
           end
           println(i)
       end
3
6
9

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

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

julia> for i = 1:2, j = 3:4
           println((i, j))
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

При этом синтаксисе итерируемые объекты могут по-прежнему ссылаться на переменные внешнего цикла; например, for i = 1:n, j = 1:i допустимо. Однако оператор break внутри такого цикла прерывает весь вложенный цикл, а не только внутренний. Обе переменные (i и j) устанавливаются в свои текущие значения итерации каждый раз, когда выполняется внутренний цикл. Поэтому присваивания i не будут видны последующим итерациям:

julia> for i = 1:2, j = 3:4
           println((i, j))
           i = 0
       end
(1, 3)
(1, 4)
(2, 3)
(2, 4)

Если бы этот пример был переписан с использованием ключевого слова for для каждой переменной, то вывод был бы другим: второе и четвертое значения содержали бы 0.

Обработка исключений

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

Встроенные Exception

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

Exception
ArgumentError
BoundsError
CompositeException
DimensionMismatch
DivideError
DomainError
EOFError
ErrorException
InexactError
InitError
InterruptException
InvalidStateException
KeyError
LoadError
OutOfMemoryError
ReadOnlyMemoryError
RemoteException
MethodError
OverflowError
Meta.ParseError
SystemError
TypeError
UndefRefError
UndefVarError
StringIndexError

Например, функция sqrt выбрасывает DomainError, если она применяется к отрицательному вещественному значению:

julia> sqrt(-1)
ERROR: DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

Вы можете определить собственные исключения следующим образом:

julia> struct MyCustomException <: Exception end

Функция throw

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

julia> f(x) = x>=0 ? exp(-x) : throw(DomainError(x, "argument must be nonnegative"))
f (generic function with 1 method)

julia> f(1)
0.36787944117144233

julia> f(-1)
ERROR: DomainError with -1:
argument must be nonnegative
Stacktrace:
 [1] f(::Int64) at ./none:1

Обратите внимание, что DomainError без скобок не является исключением, а является типом исключения. Для получения объекта Exception необходимо вызвать его:

julia> typeof(DomainError(nothing)) <: Exception
true

julia> typeof(DomainError) <: Exception
false

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

julia> throw(UndefVarError(:x))
ERROR: UndefVarError: x not defined

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

julia> struct MyUndefVarError <: Exception
           var::Symbol
       end

julia> Base.showerror(io::IO, e::MyUndefVarError) = print(io, e.var, " not defined")

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

size(A) == size(B) || throw(DimensionMismatch("size of A not equal to size of B"))

предпочтительнее

size(A) == size(B) || throw(DimensionMismatch("Size of A not equal to size of B")).

Однако иногда имеет смысл сохранить первую букву в верхнем регистре, например, если аргумент функции — это заглавная буква:

size(A,1) == size(B,2) || throw(DimensionMismatch("A has first dimension...")).

Ошибки

Функция error используется для создания ErrorException, который прерывает нормальный поток управления.

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

julia> fussy_sqrt(x) = x >= 0 ? sqrt(x) : error("negative x not allowed")
fussy_sqrt (generic function with 1 method)

julia> fussy_sqrt(2)
1.4142135623730951

julia> fussy_sqrt(-1)
ERROR: negative x not allowed
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fussy_sqrt(::Int64) at ./none:1
 [3] top-level scope

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

julia> function verbose_fussy_sqrt(x)
           println("before fussy_sqrt")
           r = fussy_sqrt(x)
           println("after fussy_sqrt")
           return r
       end
verbose_fussy_sqrt (generic function with 1 method)

julia> verbose_fussy_sqrt(2)
before fussy_sqrt
after fussy_sqrt
1.4142135623730951

julia> verbose_fussy_sqrt(-1)
before fussy_sqrt
ERROR: negative x not allowed
Stacktrace:
 [1] error at ./error.jl:33 [inlined]
 [2] fussy_sqrt at ./none:1 [inlined]
 [3] verbose_fussy_sqrt(::Int64) at ./none:3
 [4] top-level scope

Оператор try/catch

Утверждение try/catch позволяет тестировать Exception и обрабатывать ситуации, которые могут обычно привести к сбою вашей программы. Например, в коде ниже функция для квадратного корня обычно вызывает исключение. Разместив блок try/catch вокруг неё, мы можем смягчить это здесь. Вы можете выбрать, как вы хотите обработать это исключение, будь то регистрация его, возвращение значения-заполнителя или, как в случае ниже, где мы просто вывели сообщение. Важно учитывать, что использование блока try/catch намного медленнее, чем использование условного ветвления для обработки таких ситуаций. Ниже приведены дополнительные примеры обработки исключений с помощью блока try/catch:

julia> try
           sqrt("ten")
       catch e
           println("You should have entered a numeric value")
       end
You should have entered a numeric value

Утверждения try/catch также позволяют сохранить Exception в переменной. В следующем искусственном примере вычисляется квадратный корень из второго элемента x, если x индексируется, в противном случае предполагается, что x — действительное число, и возвращается его квадратный корень:

julia> sqrt_second(x) = try
           sqrt(x[2])
       catch y
           if isa(y, DomainError)
               sqrt(complex(x[2], 0))
           elseif isa(y, BoundsError)
               sqrt(x)
           end
       end
sqrt_second (generic function with 1 method)

julia> sqrt_second([1 4])
2.0

julia> sqrt_second([1 -4])
0.0 + 2.0im

julia> sqrt_second(9)
3.0

julia> sqrt_second(-9)
ERROR: DomainError with -9.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

Обратите внимание, что символ, следующий за catch, всегда интерпретируется как имя исключения, поэтому необходимо быть внимательным при написании выражений try/catch в одной строке. Следующий код не будет работать для возврата значения x в случае ошибки:

try bad() catch x end

Вместо этого используйте точку с запятой или вставьте перевод строки после catch:

try bad() catch; x end

try bad()
catch
    x
end

Мощь конструкции try/catch заключается в возможности немедленного разматывания глубоко вложенного вычисления до более высокого уровня в стеке вызывающих функций. Существуют ситуации, когда ошибка не произошла, но желательно размотать стек и передать значение на более высокий уровень. Julia предоставляет функции rethrow, backtrace, catch_backtrace и Base.catch_stack для более продвинутой обработки ошибок.

Блоки finally

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

Например, вот как мы можем гарантировать, что открытый файл закрыт:

f = open("file")
try
    # operate on file f
finally
    close(f)
end

Когда управление покидает блок try (например, из-за return или просто нормального завершения), выполняется close(f). Если блок try завершается из-за исключения, исключение продолжит распространяться. Блок catch может быть объединен с блоками try и finally также. В этом случае блок finally будет выполнен после того, как блок catch обработает ошибку.

Задачи (также известные как сопрограммы)

Задачи — это функция управления потоком, которая позволяет приостанавливать и возобновлять вычисления гибким способом. Мы упоминаем их здесь только для полноты; для более подробного обсуждения см. Асинхронное программирование.

© 2009–2020 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.5.3/manual/control-flow/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API