Начало работы с Julia
Поскольку язык JuMP встроен в Julia, перед изучением JuMP важно знать некоторые основы Julia.
Это руководство представляет собой сжатый экспресс-курс по основам Julia. Ресурсы, дающие более полное представление о Julia, можно найти здесь. |
Установка Julia
Чтобы установить Julia, загрузите последнюю стабильную версию, а затем следуйте инструкциям по установке для конкретной платформы.
Если вы не уверены в обратном, скорее всего, вам нужна 64-разрядная версия. |
Далее вам понадобится среда IDE для разработки. Популярным выбором является VS Code, поэтому следуйте этим инструкциям по установке.
Julia также можно использовать с интерактивными скриптами Jupyter или интерактивными скриптами Pluto.jl в реактивном режиме.
REPL Julia
Основной способ взаимодействия с Julia — через REPL (Read Evaluate Print Loop, цикл «чтение — вычисление — вывод»). Для доступа к REPL запустите исполняемый файл Julia, чтобы перейти к командной строке julia>
, а затем начните вводить код:
julia> 1 + 1
2
Более длинные программы следует писать в виде скриптов в текстовых файлах, которые запускаются так:
julia> include("path/to/file.jl")
Из-за задержки при запуске Julia скрипты из командной строки, как в следующем случае, запускаются медленно:
Вместо этого используйте REPL или интерактивный скрипт, а также получите дополнительные сведения в разделе The "time-to-first-solve" issue. |
Блоки кода в этой документации
В этой документации часть примеров кода предназначена для выполнения в командной строке julia>
, а часть — иным образом.
Командная строка Julia в основном используется для демонстрации коротких фрагментов кода, а их выходные данные такие же, как при запуске из REPL.
Блоки без julia>
можно копировать и вставлять в REPL. Причина их использования в том, что они позволяют выводить более содержательные данные, например графики или LaTeX, в веб- и PDF-версиях документации. При их выполнении в REPL выходные данные могут быть иными.
Получение справки
-
Читайте документацию:
-
Задайте вопрос (или поищите ответ на него) на форуме сообщества Julia: https://discourse.julialang.org
-
Если вопрос связан с JuMP, задайте его в разделе Оптимизация (математическая)
-
или пометьте вопрос тегом jump.
Чтобы получить доступ к встроенной справке в REPL, введите ?
для входа в режим справки, а затем имя нужной функции:
help?> print
search: print println printstyled sprint isprint prevind parentindices precision escape_string
print([io::IO], xs...)
Write to io (or to the default output stream stdout if io is not given) a canonical
(un-decorated) text representation. The representation used by print includes minimal formatting
and tries to avoid Julia-specific details.
print falls back to calling show, so most types should just define show. Define print if your
type has a separate "plain" representation. For example, show displays strings with quotes, and
print displays strings without quotes.
string returns the output of print as a string.
Examples
≡≡≡≡≡≡≡≡≡≡
julia> print("Hello World!")
Hello World!
julia> io = IOBuffer();
julia> print(io, "Hello", ' ', :World!)
julia> String(take!(io))
"Hello World!"
Числа и арифметические операции
Так как мы будем решать задачи оптимизации, нам придется использовать много математики. К счастью, язык Julia отлично подходит для математических вычислений, и в нем есть все стандартные операторы:
julia> 1 + 1
2
julia> 1 - 2
-1
julia> 2 * 2
4
julia> 4 / 5
0.8
julia> 3^2
9
Заметили, что в Julia не выводится .0
после некоторых чисел? Julia — динамический язык, то есть объявлять тип переменной явным образом не требуется. Однако на внутреннем уровне Julia назначает тип каждой переменной. Проверить тип любого объекта можно с помощью функции typeof
:
julia> typeof(1)
Int64
julia> typeof(1.0)
Float64
Здесь 1
имеет тип Int64
, то есть это целое число с 64-разрядной точностью, а 1.0
имеет тип Float64
, то есть это число с плавающей запятой с 64-разрядной точностью.
Если вы не знакомы с числами с плавающей запятой, обязательно прочитайте раздел Floating point numbers. |
Комплексные числа создаются с помощью im
:
julia> x = 2 + 1im
2 + 1im
julia> real(x)
2
julia> imag(x)
1
julia> typeof(x)
Complex{Int64}
julia> x * (1 - 2im)
4 - 3im
Фигурные скобки заключают в себя так называемые параметры типа. |
Есть еще ряд интересных возможностей, например иррациональное представление числа π.
julia> π
π = 3.1415926535897...
Для ввода буквы π (и большинства других греческих букв) введите |
julia> typeof(π)
Irrational{:π}
Однако при выполнении математических действий с иррациональными числами они преобразуются в тип Float64
:
julia> typeof(2π / 3)
Float64
Числа с плавающей запятой
Если вы не знакомы с числами с плавающей запятой, обязательно внимательно прочитайте этот раздел. |
Float64
— это аппроксимация с плавающей запятой вещественного числа с использованием 64 битов данных.
Поскольку это всего лишь аппроксимация, то, что справедливо в математике, может не соблюдаться на компьютере. Например:
julia> 0.1 * 3 == 0.3
false
Вот более сложный пример:
julia> sin(2π / 3) == √3 / 2
false
Чтобы получить символ |
Посмотрим, в чем разница:
julia> 0.1 * 3 - 0.3
5.551115123125783e-17
julia> sin(2π / 3) - √3 / 2
1.1102230246251565e-16
Эти числа малы, но отличны от нуля.
Чтобы понять причину такого различия, подумайте, как бы мы записали 1 / 3
и 2 / 3
, используя только четыре цифры после десятичной запятой. Мы бы записали 1 / 3
как 0.3333
, а 2 / 3
как 0.6667
. Поэтому, несмотря на то, что 2 * (1 / 3) == 2 / 3
, мы получаем 2 * 0.3333 == 0.6666 != 0.6667
.
Попробуем сделать то же самое, используя оператор ≈ (\approx + [TAB]
) вместо ==
:
julia> 0.1 * 3 ≈ 0.3
true
julia> sin(2π / 3) ≈ √3 / 2
true
≈
— это элегантный способ вызова функции isapprox
:
julia> isapprox(sin(2π / 3), √3 / 2; atol = 1e-8)
true
Плавающая запятая является причиной того, что решатели используют допуски при решении моделей оптимизации. Одна из распространенных ошибок — проверка двоичной переменной на равенство нулю с помощью |
Обратите внимание, что isapprox
всегда возвращает false
, если одно из сравниваемых чисел равно 0
и при этом atol
равно нулю (значение по умолчанию).
julia> 1e-300 ≈ 0.0
false
Поэтому если один из аргументов может быть равен нулю, обязательно устанавливайте ненулевое значение atol
.
julia> isapprox(1e-9, 0.0; atol = 1e-8)
true
На сайте Gurobi есть хорошая серия статей о влиянии плавающей запятой на оптимизацию. |
Из-за невнимательности арифметические операции с плавающей запятой могут приводить к самым разным проблемам. Например:
julia> 1 + 1e-16 == 1
true
Оказывается, что числа с плавающей запятой даже не ассоциативны:
julia> (1 + 1e-16) - 1e-16 == 1 + (1e-16 - 1e-16)
false
Важно отметить, что эта проблема касается не только Julia. Она имеется в каждом языке программирования (попробуйте сделать то же самое, например, в Python).
Векторы, матрицы и массивы
Как и в MATLAB, в Julia есть встроенная поддержка векторов, матриц и тензоров; все они представлены массивами различной размерности. Векторы состоят из элементов, разделенных запятыми, в квадратных скобках:
julia> b = [5, 6]
2-element Vector{Int64}:
5
6
Для построения матриц можно разделять столбцы пробелами, а строки — точками с запятой:
julia> A = [1.0 2.0; 3.0 4.0]
2×2 Matrix{Float64}:
1.0 2.0
3.0 4.0
Доступны операции линейной алгебры:
julia> x = A \ b
2-element Vector{Float64}:
-3.9999999999999987
4.499999999999999
Здесь мы снова имеем дело с плавающей запятой: |
julia> A * x
2-element Vector{Float64}:
5.0
6.0
julia> A * x ≈ b
true
Обратите внимание, что при умножении векторов и матриц размерность имеет значение. Например, нельзя умножить вектор на вектор:
julia> b * b
MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})
Closest candidates are:
*(::Any, ::Any, !Matched::Any, !Matched::Any...)
@ Base operators.jl:587
*(!Matched::ChainRulesCore.NoTangent, ::Any)
@ ChainRulesCore ~/.julia/packages/ChainRulesCore/6Pucz/src/tangent_arithmetic.jl:64
*(::Any, !Matched::ChainRulesCore.ZeroTangent)
@ ChainRulesCore ~/.julia/packages/ChainRulesCore/6Pucz/src/tangent_arithmetic.jl:105
...
Но умножение транспонированных матриц выполняется:
julia> b' * b
61
julia> b * b'
2×2 Matrix{Int64}:
25 30
30 36
Другие распространенные типы
Комментарии
Хотя, строго говоря, комментарии к коду — это не тип, их можно добавлять после символа #
:
julia> 1 + 1 # Это комментарий
2
Многострочные комментарии начинаются с символов #=
и заканчиваются символами =#
:
#=
Here is a
multiline comment
=#
Комментарии можно даже вкладывать в выражения. Иногда это полезно при документировании входных данных функций:
julia> isapprox(
sin(π),
0.0;
#= Здесь нужно явно указать значение atol, потому что выполняется сравнение с 0 =#
atol = 0.001,
)
true
Строки
Строки заключаются в двойные кавычки:
julia> typeof("This is Julia")
String
В строках допускаются символы Юникода:
julia> typeof("π is about 3.1415")
String
Для вывода строки используйте println
:
julia> println("Hello, World!")
Hello, World!
Для интерполяции значений в строку используйте $()
:
julia> x = 123
123
julia> println("The value of x is: $(x)")
The value of x is: 123
Для многострочных строк используйте тройные кавычки:
julia> s = """
Here is
a
multiline string
"""
"Here is\na\nmultiline string\n"
julia> println(s)
Here is
a
multiline string
Символы
Symbol
в Julia — это структура данных компилятора, представляющая имена переменных Julia.
julia> println("The value of x is: $(eval(:x))")
The value of x is: 123
Здесь мы использовали |
julia> typeof(:x)
Symbol
Объект Symbol
можно представить себе как строку String
, которая занимает меньше памяти и не может быть изменена.
Преобразование между String
и Symbol
выполняется с помощью конструкторов этих типов:
julia> String(:abc)
"abc"
julia> Symbol("abc")
:abc
Типом |
Кортежи
В Julia широко применяется простая структура данных, называемая кортежем. Кортежи — это неизменяемые коллекции значений. Например:
julia> t = ("hello", 1.2, :foo)
("hello", 1.2, :foo)
julia> typeof(t)
Tuple{String, Float64, Symbol}
К элементам кортежа можно обращаться по индексам, как и в случае с массивами:
julia> t[2]
1.2
Их также можно распаковывать следующим образом:
julia> a, b, c = t
("hello", 1.2, :foo)
julia> b
1.2
Значениям также можно присваивать имена, что является удобным способом создания легких структур данных.
julia> t = (word = "hello", num = 1.2, sym = :foo)
(word = "hello", num = 1.2, sym = :foo)
К значениям можно обращаться через точечную нотацию:
julia> t.word
"hello"
Словари
Как и в Python, в Julia есть встроенная поддержка словарей. Словари предоставляют универсальный способ сопоставления ключей со значениями. Например, целочисленные значения могут сопоставляться со строками:
julia> d1 = Dict(1 => "A", 2 => "B", 4 => "D")
Dict{Int64, String} with 3 entries:
4 => "D"
2 => "B"
1 => "A"
И еще раз о типах: |
Для поиска значения используется синтаксис с квадратными скобками:
julia> d1[2]
"B"
Словари поддерживают нецелочисленные ключи и смешанные типы данных:
julia> Dict("A" => 1, "B" => 2.5, "D" => 2 - 3im)
Dict{String, Number} with 3 entries:
"B" => 2.5
"A" => 1
"D" => 2-3im
Типы Julia образуют иерархию. Здесь тип значения словаря — |
julia> Dict("A" => 1.0 + 0.0im, "B" => 2.5 + 0.0im, "D" => 2.0 - 3.0im)
Dict{String, ComplexF64} with 3 entries:
"B" => 2.5+0.0im
"A" => 1.0+0.0im
"D" => 2.0-3.0im
Словари могут быть вложенными:
julia> d2 = Dict("A" => 1, "B" => 2, "D" => Dict(:foo => 3, :bar => 4))
Dict{String, Any} with 3 entries:
"B" => 2
"A" => 1
"D" => Dict(:bar=>4, :foo=>3)
julia> d2["B"]
2
julia> d2["D"][:foo]
3
Структуры
Пользовательские структуры данных можно определять с помощью struct
:
julia> struct MyStruct
x::Int
y::String
z::Dict{Int,Int}
end
julia> a = MyStruct(1, "a", Dict(2 => 3))
Main.MyStruct(1, "a", Dict(2 => 3))
julia> a.x
1
По умолчанию они неизменяемые:
julia> a.x = 2
setfield!: immutable struct of type MyStruct cannot be changed
Однако можно объявить и изменяемую структуру mutable struct
:
julia> mutable struct MyStructMutable
x::Int
y::String
z::Dict{Int,Int}
end
julia> a = MyStructMutable(1, "a", Dict(2 => 3))
Main.MyStructMutable(1, "a", Dict(2 => 3))
julia> a.x
1
julia> a.x = 2
2
julia> a
Main.MyStructMutable(2, "a", Dict(2 => 3))
Циклы
В Julia изначально поддерживаются циклы в стиле for-each с синтаксисом for <value> in <collection> end
:
julia> for i in 1:5
println(i)
end
1
2
3
4
5
Диапазоны строятся в виде |
julia> for i in 1.2:1.1:5.6
println(i)
end
1.2
2.3
3.4
4.5
5.6
Такой цикл for-each также работает со словарями:
julia> for (key, value) in Dict("A" => 1, "B" => 2.5, "D" => 2 - 3im)
println("$(key): $(value)")
end
B: 2.5
A: 1
D: 2 - 3im
Обратите внимание, что в отличие от векторных языков, таких как MATLAB и R, циклы не приводят к значительному снижению производительности в Julia.
Порядок выполнения
Порядок выполнения в Julia аналогичен принятому в MATLAB: используются ключевые слова if-elseif-else-end
и логические операторы ||
и &&
для операций или и и соответственно:
julia> for i in 0:5:15
if i < 5
println("$(i) is less than 5")
elseif i < 10
println("$(i) is less than 10")
else
if i == 10
println("the value is 10")
else
println("$(i) is bigger than 10")
end
end
end
0 is less than 5
5 is less than 10
the value is 10
15 is bigger than 10
Включения
Подобно таким языкам, как Haskell и Python, в Julia при построении массивов и словарей поддерживаются простые циклы, называемые включениями.
Список целых чисел по возрастанию:
julia> [i for i in 1:5]
5-element Vector{Int64}:
1
2
3
4
5
Матрицы можно строить, включая несколько индексов:
julia> [i * j for i in 1:5, j in 5:10]
5×6 Matrix{Int64}:
5 6 7 8 9 10
10 12 14 16 18 20
15 18 21 24 27 30
20 24 28 32 36 40
25 30 35 40 45 50
Для фильтрации некоторых значений можно использовать условные операторы:
julia> [i for i in 1:10 if i % 2 == 1]
5-element Vector{Int64}:
1
3
5
7
9
Похожий синтаксис можно использовать для построения словарей:
julia> Dict("$(i)" => i for i in 1:10 if i % 2 == 1)
Dict{String, Int64} with 5 entries:
"1" => 1
"5" => 5
"7" => 7
"9" => 9
"3" => 3
Функции
Простая функция определяется следующим образом:
julia> function print_hello()
return println("hello")
end
print_hello (generic function with 1 method)
julia> print_hello()
hello
В функцию можно добавлять аргументы:
julia> function print_it(x)
return println(x)
end
print_it (generic function with 1 method)
julia> print_it("hello")
hello
julia> print_it(1.234)
1.234
julia> print_it(:my_id)
my_id
Также возможны необязательные именованные аргументы:
julia> function print_it(x; prefix = "value:")
return println("$(prefix) $(x)")
end
print_it (generic function with 1 method)
julia> print_it(1.234)
value: 1.234
julia> print_it(1.234; prefix = "val:")
val: 1.234
Ключевое слово return
служит для указания возвращаемых значений функции:
julia> function mult(x; y = 2.0)
return x * y
end
mult (generic function with 1 method)
julia> mult(4.0)
8.0
julia> mult(4.0; y = 5.0)
20.0
Анонимные функции
Посредством синтаксиса input -> output
создается анонимная функция. Они наиболее полезны при передаче в другие функции. Например:
julia> f = x -> x^2
#11 (generic function with 1 method)
julia> f(2)
4
julia> map(x -> x^2, 1:4)
4-element Vector{Int64}:
1
4
9
16
Параметры типов
Входные данные функции можно ограничить с помощью параметров типов. Они задаются посредством символов ::
, за которыми следует имя требуемого типа. Например:
julia> function foo(x::Int)
return x^2
end
foo (generic function with 1 method)
julia> function foo(x::Float64)
return exp(x)
end
foo (generic function with 2 methods)
julia> function foo(x::Number)
return x + 1
end
foo (generic function with 3 methods)
julia> foo(2)
4
julia> foo(2.0)
7.38905609893065
julia> foo(1 + 1im)
2 + 1im
Но что произойдет, если вызвать foo
с неподходящими входными данными?
julia> foo([1, 2, 3])
MethodError: no method matching foo(::Vector{Int64})
Closest candidates are:
foo(!Matched::Float64)
@ Main REPL[2]:1
foo(!Matched::Int64)
@ Main REPL[1]:1
foo(!Matched::Number)
@ Main REPL[3]:1
Ошибка MethodError
означает, что в функцию переданы данные типа, отличного от ожидаемого. В данном случае в сообщении об ошибке говорится о том, что функция не знает, как обрабатывать Vector{Int64}
, но знает, как обрабатывать Float64
, Int64
и Number
.
Чтобы узнать, какие типы требуются, внимательно прочитайте раздел Closest candidates в сообщении об ошибке. |
Трансляция
В приведенном выше примере не было определено, что делать при передаче объекта типа Vector
в f
. К счастью, в Julia есть удобный синтаксис для поэлементного применения f
к массивам. Просто добавьте точку (.
) между именем функции и открывающей скобкой (
. Это работает для любой функции, включая функции с несколькими аргументами. Например:
julia> foo.([1, 2, 3])
3-element Vector{Int64}:
1
4
9
Получаете ошибку |
Изменяемые и неизменяемые объекты
Некоторые типы в Julia являются изменяемыми, то есть содержащиеся в них значения можно изменять. Хорошим примером может служить массив. Содержимое массива можно изменять, не создавая новый массив.
Напротив, такие типы, как Float64
, являются неизменяемыми. Содержимое Float64
изменить нельзя.
Это необходимо учитывать при передаче типов в функции. Например:
julia> function mutability_example(mutable_type::Vector{Int}, immutable_type::Int)
mutable_type[1] += 1
immutable_type += 1
return
end
mutability_example (generic function with 1 method)
julia> mutable_type = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> immutable_type = 1
1
julia> mutability_example(mutable_type, immutable_type)
julia> println("mutable_type: $(mutable_type)")
mutable_type: [2, 2, 3]
julia> println("immutable_type: $(immutable_type)")
immutable_type: 1
Поскольку Vector{Int}
является изменяемым типом, изменение переменной внутри функции привело к изменению значения за ее пределами. Напротив, изменение immutable_type
не привело к изменению значения за пределами функции.
Проверить изменяемость можно с помощью функции isimmutable
:
julia> isimmutable([1, 2, 3])
false
julia> isimmutable(1)
true
Диспетчер пакетов
Установка пакетов
Каким бы замечательным ни был базовый язык Julia, в какой-то момент может понадобиться пакет расширения. Некоторые из таких пакетов являются встроенными, например, генерирование случайных чисел обеспечивается пакетом Random
стандартной библиотеки. Такие пакеты загружаются с помощью команд using
и import
.
julia> using Random # Эквивалент директивы `from Random import *` в Python
julia> import Random # Эквивалент директивы `import Random` в Python
julia> Random.seed!(33)
Random.TaskLocalRNG()
julia> [rand() for i in 1:10]
10-element Vector{Float64}:
0.4745319377345316
0.9650392357070774
0.8194019096093067
0.9297749959069098
0.3127122336048005
0.9684448191382753
0.9063743823581542
0.8386731983150535
0.5103924401614957
0.9296414851080324
Для установки пакетов, не входящих в стандартную библиотеку Julia, служит диспетчер пакетов.
Например, JuMP можно установить так:
using Pkg
Pkg.add("JuMP")
Полный перечень зарегистрированных пакетов Julia см. в списке пакетов на JuliaHub.
Иногда может понадобиться использовать незарегистрированный пакет Julia. В этом случае установить пакет можно по URL-адресу репозитория в git.
using Pkg
Pkg.add("https://github.com/user-name/MyPackage.jl.git")
Среды пакетов
По умолчанию команда Pkg.add
добавляет пакеты в глобальную среду Julia. Однако в Julia есть также встроенная поддержка виртуальных сред.
Активировать виртуальную среду можно так:
import Pkg; Pkg.activate("/path/to/environment")
Узнать, какие пакеты установлены в текущей среде, можно с помощью Pkg.status()
.
Мы настоятельно рекомендуем создавать среду Pkg для каждого проекта, создаваемого в Julia, и добавлять в нее только необходимые пакеты, чтобы не добавлять множество пакетов в глобальную среду. Дополнительные сведения по этой теме приводятся в документации по диспетчеру Pkg. |