Порядок выполнения
В Julia имеются различные конструкции для управления порядком выполнения:
-
Составные выражения:
begin
и;
. -
Условные вычисления:
if
-elseif
-else
и?:
(тернарный оператор). -
Вычисления по сокращенной схеме: логические операторы
&&
(И) и||
(ИЛИ), а также цепочки сравнения. -
Повторяющиеся вычисления: циклы:
while
иfor
. -
Обработка исключений:
try
-catch
,error
иthrow
.
Первые пять механизмов управления порядком выполнения типичны для высокоуровневых языков программирования, в отличие от задач (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
, вычисляется соответствующий блок. Если ни одно из выражений не равно 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
перед ?
— это выражение условия. Если условие a
равно true
, вычисляется выражение b
перед :
, а если оно равно false
— выражение c
после :
. Обратите внимание, что пробелы вокруг ?
и :
обязательны: выражение 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
равно true, результатом всего тернарного выражения является строка "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
будет иметь значение true, если 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 !
можно написать
(что можно истолковать как
Например, рекурсивную подпрограмму для вычисления факториала можно определить так:
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
. Одним достаточно важным отличием цикла for
от while
является область видимости этой переменной. Внутри тела цикла 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
до того, как условие примет значение false, или прервать выполнение цикла 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
по порядку перебирает вложенные итераторы, выбирая -й элемент каждого из них на -й итерации цикла for
. Когда элементы в каком-либо вложенном итераторе заканчиваются, выполнение цикла for
останавливается.
Обработка исключений
Когда возникает непредвиденное состояние, возврат осмысленного значения вызывающей стороне может быть невозможен. Исключение может приводить либо к завершению работы программы с выводом диагностического сообщения об ошибке, либо к выполнению каких-либо действий в коде, если программист предусмотрел обработку таких исключительных условий.
Встроенные объекты Exception
Исключения (Exception
) вызываются при возникновении непредвиденного состояния. Все перечисленные ниже встроенные объекты Exception
прерывают нормальный порядок выполнения.
Exception |
---|
|
Например, при применении функции 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")
Желательно, чтобы сообщение об ошибке начиналось со строчной буквы. Например,
лучше, чем
Однако иногда есть смысл оставить первую букву прописной, например, если аргумент функции начинается с прописной буквы:
|
Ошибки
Функция 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
и current_exceptions
.
Выражения else
Совместимость: Julia 1.8
Для этой функции требуется версия не ниже Julia 1.8. |
Иногда нужно не только правильно обработать ошибку, но и обеспечить выполнение некоторого кода только в случае успешного выполнения блока try
. Для этого после блока catch
можно указать предложение else
, которое выполняется, только если ранее не возникло ошибок. Преимуществом по сравнению с включением этого кода в блок try
является то, что дальнейшие ошибки не перехватываются предложением catch
без какой-либо реакции.
local x
try
x = read("file", String)
catch
# обработка ошибок чтения
else
# действия с x
end
Каждое из предложений
Чтобы сделать переменную доступной в любом месте внешней области, используйте [ключевое слово |
Выражения finally
По завершении выполнения кода, который изменяет состояние или использует ресурсы, например файлы, обычно требуются определенные действия по очистке (например, закрытие файлов). Исключения могут усложнять эту задачу, так как из-за них выполнение блока кода может быть прервано преждевременно. Ключевое слово finally
обеспечивает выполнение определенного кода при выходе из заданного блока кода независимо от способа выхода.
Например, так можно гарантировать закрытие открытого файла:
f = open("file")
try
# действия с файлом f
finally
close(f)
end
Когда управление передается из блока try
(например, вследствие выполнения оператора return
или из-за обычного завершения), выполняется функция close(f)
. Если выход из блока try
произойдет вследствие исключения, это исключение будет передано далее. Блок catch
также можно использовать в сочетании с try
и finally
. В этом случае блок finally
будет выполнен после того, как блок catch
обработает ошибку.
Задачи (сопрограммы)
Задачи — это функция порядка выполнения, которая обеспечивает гибкую приостановку и возобновление вычислений. Здесь они упоминаются только для полноты картины; полное описание см. в главе Асинхронное программирование.