我对 Julia 的参数传递理解

起因

我之前一直没有搞懂 Julia 的参数到底是怎么传的(自然 Python 我也就没有懂),不知道何时会改变参数的值,何时不会。最近在学 C语言,明显感到传参很清晰,就是传值,要改变函数外面的对象,就传指针进去。因此为了搞懂 Julia 到底是怎么传参数的(我以前有些函数写得莫名其妙,因为不知道会不会改变传入的对象),又去翻了文档,可惜没看懂。

Julia 函数参数遵循有时称为 “pass-by-sharing” 的约定,这意味着变量在被传递给函数时其值并不会被复制。函数参数本身充当新的变量绑定(指向变量值的新地址),它们所指向的值与所传递变量的值完全相同。调用者可以看到对函数内可变值(如数组)的修改。这与 Scheme,大多数 Lisps,Python,Ruby 和 Perl 以及其他动态语言中的行为相同。

尝试

当我尝试去搜索 pass-by-sharing 的时候,其实并没有太合适的结果,直到昨天,我又一次去搜索,点进有可能的结果里面一个个看,看到了 这篇文章 深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?, 就有点懂了,发现叫 call by sharing. 最后一下就搜索出来了。(现在再次搜索发现是我把第一个搜索结果 Evaluation strategy 给忽略了,它里面基本上讲明白了。 :sweat_smile:

我的理解

看了文章与 wiki 之后感觉有点明白了。我的理解是这样的,不知道对不对。

如果是对对象整体进行的操作,就不会改变原始的对象,但是如果是直接修改对象的部分,就会改变原来的对象。

举例

x=reshape(collect(1:9),3,3)
function f(x)
    x=x^2
end
println(f(x)) #[30 66 102; 36 81 126; 42 96 150]
println(x)  #[1 4 7; 2 5 8; 3 6 9]

此处 x 不变,是因为 函数内的 x 和外面的 x 共享同一个对象,后来 x=x^2 给函数内的 x 重新赋值,它不再共享函数外面的 x 的那个对象, 我们也没有修改那个对象。因此,外面的 x 没有改变。

用指针的思路来理解的话,就是 函数内的 x 确实指向 函数外的 x 的那个对象,但是在 x=x^2 这里,它指向了中间生成的临时变量 x^2, 不再指向函数外部的 x. 因此 函数外部的 x 不变。

x=reshape(collect(1:9),3,3)
function g(x)
    x.=x^2
end
println(g(x)) #[30 66 102; 36 81 126; 42 96 150]
println(x) #[30 66 102; 36 81 126; 42 96 150]

这里我的理解是,x^2 生成了一个新的矩阵, 然后 x .= x^2 相当于是

y=x^2
for i in eachindex(x)
    x[i]=y[i] # 这里按位置修改了 x的值。
end

这里,我们直接修改了函数内的 x 所共享的对象,由于函数内外的两个 x 共享同一个对象,因此外面的 x 也被改变了。

按照指针的思路来理解的话,就是我们先创建了一个临时变量 y=x^2, 然后通过指针,将 x 上的值一个个变成y 的值。最后 函数外的x 自然就变了。

写在最后

以上是个人理解,感觉其实并不是十分准确,希望有明白的人能够再讲的细一点。这样我就知道,我到底什么时候该给函数加上! 了(因为有时候函数并不按照我想的那样修改了参数的值。)

4 个赞

我开始也是觉得很不习惯。在R中基本上直接用<- 或者= 就直接传值过来。
个人感觉传指针应该是减少不必要的内存消耗。不过有时候也容易引起混乱。
大部分情况下如果需要传值的话我会用y = copy(x)这样的方式。

记住一点就行,Julia/Python的赋值语句产生新的对象。

例:

a = [1,2,3]
b = a
# a = [1, 2, 3]
# b = [1, 2, 3]
# 此时a, b是同一个东西
a[1] = 2
# a = [2, 2, 3]
# b = [2, 2, 3]
# 此时a还是原来的a,只是被修改了(赋值语句作用的是a[1]不是a,a本身的对象不改变)
a = [4,3,2,1]
# a = [4, 3, 2, 1]
# b = [2, 2, 3]
# 此时 a= 为赋值语句,产生新的对象,旧对象仍然不变,只是失去了a这个名字

函数传参还是一个道理,只是函数因为作用域,里面新定义的量哪怕和外面重名也不影响外面,除非使用 global

例:

a = [1,2,3]

function fun1(a)
    a = [3,2,1]
    # 此处赋值语句产生新的对象,函数里的a与送进来的
end

function fun2(a)
    a[1] = 3
    # 此处a仍然是送进来的a,函数外的将会改变
end

这种传参方式可能在将来的动态语言中会长期主流,因为习惯了之后最不容易犯错(比如大量复制传的参数消耗内存)。

另外,如果希望函数可以修改参数自身,使用 Ref,如:

a = Ref(1)

function change_a!(a::Ref{Int})
    a[] = 2
end

change_a!(a)

# a = Ref(2)
# a[] = 2
3 个赞

注意区分一下copydeepcopy,因为对象可能是层层复合的,前者只复制最外层

我怎么感觉是Copy on Write?? :slightly_smiling_face:

是copy on write,但是也确实只复制最外层

a = [[1, 2], 3]
b = copy(a)
c = deepcopy(a)

a[1][1] = 2
# a = [[2, 2], 3]
# b = [[2, 2], 3]
# c = [[1, 2], 3]

如果函数要修改的参数是个Vector{Float64}应该怎么办?

可以 Ref{Vector{Float64}}

建议不要用c语言的指针理解julia的传参,call-by-xxx从来就没有过准确的定义。julia其实只用记住一个标准, 函数里的变量名绑定的对象和和函数域外的变量绑定的 identical, x_in === x_out 。

function是hard scope,函数里的变量名 x_in 会在函数的域下生成一个新的binding到 x_out 的对象值,但它并不会破坏原有的x_out的绑定。当绑定的对象是一个mutable时,比如array,如果对x_in进行mutation操作(x_in[i] = SOME_VALUE)就改变了对象的内容,又因为 x_out 绑定的也是这个对象,故而 x_out 的内容也发生同样的改变。

但如果函数内进行的不是mutation操作而是assign,比如 x_in = x_in + SOME_VALUE,那么 x_in 就bindging 到了另一个对象,这时 x_out 的值并没有变,因为它的 binding 还是原先的对象。

2 个赞