Почему / как определить, когда функция перезаписывает локальную переменную в Julia? - PullRequest
10 голосов
/ 04 мая 2020

Я относительно новичок в Джулии и работаю над портированием на некоторые C функции, чтобы проверить разницу в скорости. С этим я борюсь, это область действия переменных. В частности, иногда вызов функции в Julia перезаписывает локальную переменную, а иногда нет. Например, вот функция для вычисления минимального остовного дерева:

function mst(my_X::Array{Float64})
    n = size(my_X)[1]
    N = zeros(Int16,n,n)
    tree = []
    lv = maximum(my_X)+1
    my_X[diagind(my_X)] .=lv
    indexi = 1
    for ijk in 1:(n-1)
        tree = vcat(tree, indexi)
        m = minimum(my_X[:,tree],dims = 1)
        a = zeros(Int64, length(tree))
        print(tree)
        for k in 1:length(tree)
            a[k] = sortperm(my_X[:,tree[k]])[1,]
        end
        b = sortperm(vec(m))[1]
        indexj = tree[b]
        indexi = a[b]
        N[indexi,indexj] = 1
        N[indexj,indexi] = 1
        for j in tree
            my_X[indexi,j] = lv
            my_X[j,indexi] = lv
        end
    end
    return N
end

Теперь мы можем применить это к матрице расстояний X:

julia> X
5×5 Array{Float64,2}:
 0.0   0.54  1.08  1.12  0.95
 0.54  0.0   0.84  0.67  1.05
 1.08  0.84  0.0   0.86  1.14
 1.12  0.67  0.86  0.0   1.2
 0.95  1.05  1.14  1.2   0.0

Но когда я это сделаю, он перезаписывает все записи X

julia> M = mst(X)
julia> M
5×5 Array{Int16,2}:
 0  1  0  0  1
 1  0  1  1  0
 0  1  0  0  0
 0  1  0  0  0
 1  0  0  0  0
julia> X
5×5 Array{Float64,2}:
 2.2  2.2  2.2  2.2  2.2
 2.2  2.2  2.2  2.2  2.2
 2.2  2.2  2.2  2.2  2.2
 2.2  2.2  2.2  2.2  2.2
 2.2  2.2  2.2  2.2  2.2

Конечно, я могу переопределить это, если я явно добавлю что-то подобное в функцию:

function mst(my_Z::Array{Float64})
    my_X = copy(my_Z)
     .
     .
     .

Но похоже, что проблема глубже, чем это. Например, если я попытаюсь повторить это в простом примере, я не смогу воссоздать проблему:

function add_one(my_X::Int64)
    my_X = my_X + 1
    return my_X
end
julia> Z = 1
julia> W = add_one(Z)
julia> W
2
julia> Z
1

Что здесь происходит ?? Я прочитал и перечитал справочные документы julia по различным областям действия, и я не могу понять, в чем заключается различие.

1 Ответ

10 голосов
/ 04 мая 2020

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

  1. Значения в Julia могут быть изменяемыми или неизменными.
  2. Переменная в Julia связана со значением (которое может быть неизменным или изменяемым).
  3. Некоторые операции могут изменять изменяемое значение.

Таким образом, первый пункт касается изменчивости по сравнению с неизменяемостью значений. Обсуждение в руководстве Юлии дано здесь . Вы можете проверить, является ли значение изменчивым или нет, используя функцию isimmutable.

Типичные случаи:

  1. числа, строки, Tuple, NamedTuple, struct s неизменны
julia> isimmutable(1)
true

julia> isimmutable("sdaf")
false

julia> isimmutable((1,2,3))
true
Массивы, dicts, mutable structs et c. (в общем случае типы контейнеров, отличные от Tuple, NamedTuple и struct s), являются изменяемыми:
julia> isimmutable([1,2,3])
false

julia> isimmutable(Dict(1=>2))
false

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

julia> x = [1,2,3]
3-element Array{Int64,1}:
 1
 2
 3

julia> x[1] = 10
10

julia> x
3-element Array{Int64,1}:
 10
  2
  3

Теперь давайте разберем то, что мы видели здесь:

  • оператор присваивания x = [1, 2, 3] связывает значение (в данном случае вектор) в переменную x
  • оператор x[1] = 10 изменяет значение (вектор) на месте

Обратите внимание, что то же самое не получится для Tuple, поскольку он неизменен :

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

julia> x[1] = 10
ERROR: MethodError: no method matching setindex!(::Tuple{Int64,Int64,Int64}, ::Int64, ::Int64)

Теперь мы подошли ко второму пункту - привязке значения к имени переменной. Обычно это делается с помощью оператора =, если с левой стороны мы видим имя переменной, как указано выше, с x = [1,2,3] или x = (1,2,3).

Обратите внимание, что, в частности, также += (и аналогичные) выполняем повторное связывание, например:

julia> x = [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

julia> y = x
3-element Array{Int64,1}:
 1
 2
 3

julia> x += [1,2,3]
3-element Array{Int64,1}:
 2
 4
 6

julia> x
3-element Array{Int64,1}:
 2
 4
 6

julia> y
3-element Array{Int64,1}:
 1
 2
 3

, так как в этом случае это просто сокращение x = x + [1, 2, 3], и мы знаем, что = повторное связывание.

В частности (как @pszufe в комментарии) если вы передаете значение функции, то ничего не копируется. Здесь происходит то, что переменная, которая находится в сигнатуре функции, связана с переданным значением (этот тип поведения иногда называется pass by share ). Итак, у вас есть:

julia> x = [1,2,3]
3-element Array{Int64,1}:
 1
 2
 3

julia> f(y) = y
f (generic function with 1 method)

julia> f(x) === x
true

По сути, то, что происходит, «как если бы» вы написали y = x. Разница в том, что функция создает переменную y в новой области (область действия функции), тогда как y = x создает привязку значения, с которым x связана с переменной y в области действия, где оператор y = x присутствует.

Теперь, с другой стороны, такие вещи, как x[1] = 10 (по сути, приложение функции setindex!) или x .= [1,2,3], являются операциями на месте (они не перепривязывают значение но попробуйте изменить контейнер). Так что это работает на месте (обратите внимание, что в примере я объединяю вещание с +=, чтобы сделать его на месте):

julia> x = [1,2,3]
3-element Array{Int64,1}:
 1
 2
 3

julia> y = x
3-element Array{Int64,1}:
 1
 2
 3

julia> x .+= [1,2,3]
3-element Array{Int64,1}:
 2
 4
 6

julia> y
3-element Array{Int64,1}:
 2
 4
 6

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

julia> x = 10
10

julia> x .+= 1
ERROR: MethodError: no method matching copyto!(::Int64, ::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{0},Tuple{},typeof(+),Tuple{Int64,Int64}})

То же самое с установкой индекса для неизменяемого значения:

julia> x = 10
10

julia> x[] = 1
ERROR: MethodError: no method matching setindex!(::Int64, ::Int64)

Наконец, третье - какие операции пытаются изменить значение на месте. Мы уже отметили некоторые из них (например, setindex!: x[10] = 10 и широковещательное задание x .= [1,2,3]). В общем, не всегда легко определить, будет ли вызов f(x) видоизменяться x, если f является некоторой общей функцией (он может или не может видоизменять x, если x является изменяемым). Поэтому в Джулии существует соглашение о добавлении ! в конце имен функций, которые могут изменять свои аргументы, чтобы визуально сигнализировать об этом (следует подчеркнуть, что это только соглашение - в частности, просто добавление ! в конце имя функции не имеет прямого влияния на то, как она работает). Мы уже видели это с setindex! (для которого сокращение x[1] = 10, как обсуждалось), но вот другой пример:

julia> x = [1, 2, 3]
3-element Array{Int64,1}:
 1
 2
 3

julia> filter(==(1), x) # no ! so a new vector is created
1-element Array{Int64,1}:
 1

julia> x
3-element Array{Int64,1}:
 1
 2
 3

julia> filter!(==(1), x) # ! so x is mutated in place
1-element Array{Int64,1}:
 1

julia> x
1-element Array{Int64,1}:
 1

Если вы используете функцию (например, setindex!), которая мутирует свой аргумент и хочет избежать мутации, используя copy при передаче ему аргумента (или deepcopy, если ваша структура многократно вложена и потенциально мутация может произойти на более глубоком уровне - но это редко).

Так в нашем примере:

julia> x = [1,2,3]
3-element Array{Int64,1}:
 1
 2
 3

julia> y = filter!(==(1), copy(x))
1-element Array{Int64,1}:
 1

julia> y
1-element Array{Int64,1}:
 1

julia> x
3-element Array{Int64,1}:
 1
 2
 3
...