Spec-Zone.ru › Julia 1.8

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

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 <= 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.

Несколько контейнеров можно итерировать одновременно в одном цикле 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 прерывают обычный поток управления.

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 позволяет тестировать исключения и обеспечивать удобную обработку ситуаций, которые могут привести к сбою приложения. Например, в коде ниже функция для извлечения квадратного корня обычно выбрасывает исключение. Разместив блок 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 также позволяют сохранять исключение в переменной. В следующем искусственном примере вычисляется квадратный корень из второго элемента 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 для более продвинутой обработки ошибок.

Блоки 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–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.7.0/manual/control-flow/

Spec-Zone.ru

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