Поток управления
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"
Короткое замыкание
Операторы && и || в Julia соответствуют логическим операциям «и» и «или» соответственно и обычно используются для этой цели. Однако они обладают дополнительным свойством короткого замыкания: они не обязательно вычисляют свой второй аргумент, как объяснено ниже. (Также существуют битовые & и | операторы, которые могут использоваться в качестве логических «и» и «или» без короткого замыкания, но имейте в виду, что & и | имеют более высокий приоритет, чем && и || для порядка вычислений.)
Короткое замыкание очень похоже на условное выполнение. Это поведение существует в большинстве языков императивного программирования, имеющих булевы операторы && и ||: в последовательности булевых выражений, соединённых этими операторами, вычисляется только минимальное необходимое количество выражений для определения окончательного булевого значения всей цепочки. Некоторые языки (например, Python) называют их and (&&) и or (||). Конкретно это означает, что:
- В выражении
a && bподвыражениеbоценивается только тогда, когдаaоценивается какtrue. - В выражении
a || bподвыражениеbоценивается только тогда, когдаaоценивается какfalse.
Поскольку a && b должно быть false, если a равно false, независимо от значения b, и аналогично, значение a || b должно быть истинным, если a истинно, независимо от значения 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 <= 3
println(i)
global i += 1
end
1
2
3
Цикл while вычисляет выражение условия (i <= 5 в данном случае) и, пока оно остается true, продолжает вычислять тело цикла while. Если выражение условия является false при первом достижении цикла while, тело никогда не вычисляется.
Цикл for упрощает написание распространённых идиом повторного вычисления. Поскольку подсчёт вверх и вниз, как в цикле while выше, очень распространён, его можно выразить более лаконично с помощью цикла for:
julia> for i = 1:3
println(i)
end
1
2
3
Здесь 1:3 — это объект диапазона, представляющий последовательность чисел 1, 2, 3. Цикл for перебирает эти значения, присваивая каждое из них поочерёдно переменной i. Одно важное различие между предыдущей формой цикла while и формой цикла for — область видимости переменной. Цикл for всегда вводит новую переменную итерации в своём теле, независимо от того, существует ли переменная с таким же именем в области видимости внешнего цикла. Это подразумевает, что, с одной стороны, переменная i не обязательно должна быть объявлена до цикла. С другой стороны, она не будет видна за пределами цикла, и внешняя переменная с таким же именем не будет затронута. Для проверки этого потребуется либо новая инстанция интерактивной сессии, либо другое имя переменной:
julia> for j = 1:3
println(j)
end
1
2
3
julia> j
ERROR: UndefVarError: `j` not defined
julia> j = 0;
julia> for j = 1:3
println(j)
end
1
2
3
julia> j
0
Используйте for outer, чтобы изменить последнее поведение и повторно использовать существующую локальную переменную.
См. Область видимости переменных для подробного объяснения области видимости переменных, outer и как это работает в 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 >= 3
break
end
global i += 1
end
1
2
3
julia> for j = 1:1000
println(j)
if j >= 3
break
end
end
1
2
3
Без ключевого слова 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.
Несколько контейнеров можно перебирать одновременно в одном цикле for, используя zip:
julia> for (j, k) in zip([1 2 3], [4 5 6 7])
println((j,k))
end
(1, 4)
(2, 5)
(3, 6)
Использование zip создаст итератор, который является кортежем, содержащим под-итераторы для переданных ему контейнеров. Итератор zip будет перебирать все под-итераторы в порядке, выбирая $i$-ый элемент каждого под-итератора в $i$-ой итерации цикла for. Как только любой из под-итераторов иссякнет, цикл for остановится.
Обработка исключений
Когда возникает непредвиденное условие, функция может не иметь возможности вернуть разумное значение своему вызывающему объекту. В таких случаях лучше всего, чтобы исключительное условие либо завершило программу с сообщением об ошибке, либо, если программист предоставил код для обработки таких исключительных обстоятельств, позволил этому коду принять соответствующие действия.
Встроенные Exception
Exception выбрасываются, когда произошло непредвиденное условие. Встроенные Exception, перечисленные ниже, все прерывают нормальный поток управления.
Например, функция sqrt выбрасывает DomainError, если она применяется к отрицательному вещественному значению:
julia> sqrt(-1) ERROR: DomainError with -1.0: sqrt was called with a negative real argument but 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")
END_OF_DOCUMENT_MARKER При написании сообщения об ошибке предпочтительно использовать строчную букву для первого слова. Например,
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 was called with a negative real argument but 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 и current_exceptions для более продвинутой обработки ошибок.
Блоки else
Эта функциональность требует как минимум Julia 1.8.
В некоторых случаях необходимо не только уметь обрабатывать случаи ошибок, но и выполнять некоторый код только если блок try успешно завершился. Для этого можно указать блок else после блока catch, который выполняется всякий раз, когда ранее не было выброшено ошибок. Преимущество этого перед включением этого кода в блок try заключается в том, что любые последующие ошибки не будут заглушены блоком catch.
local x
try
x = read("file", String)
catch
# handle read errors
else
# do something with x
end
Блоки try, catch, else и finally каждый вводят свои блоки области видимости, поэтому, если переменная определена только в блоке try, она недоступна в блоке else или finally:
julia> try
foo = 1
catch
else
foo
end
ERROR: UndefVarError: `foo` not defined
Используйте ключевое слово local вне блока try, чтобы сделать переменную доступной из любой точки внешней области видимости.
Блоки 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–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.10/manual/control-flow/