Python中的参数传递方式


工作写Python脚本时,对于python中函数参数传递有疑惑,在加上最近在学习C++,恰好复习了C++中参数传递的传递方法,于是想把这两类对比一下。

Review:C++中的参数传递方式

C++中的参数传递,总结在这篇文章中,主要分为:

  • 按值传递;
  • 按引用传递;

不同的传递方式,有不同的好处,比如按引用传递,可以避免copy,节省资源;按照值传递,则不会影响原有数据,这些方式没有好坏,只有是否适合特定的场景。

Python中的参数传递方式

众所周知,python以其速度慢而被人诟病,因此对于函数中参数的传递,如果能够避免参数copy,可以节省资源,那么Python中函数参数究竟是则怎么传递的?

要知道,Python中没有引用的概念。因此,它的函数参数传递,有人叫做pass by value,但是其行为又像pass by reference。确定具体的方式,需要查询变量的内存地址,python中使用id(…)函数

Return the “identity” of an object. This is an integer (or long integer) which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same id() value.

这里举例说明。

Python中的赋值操作

a = 100
b = a
print("The address of a is {}".format(hex(id(a))))
print("The address of b is {}".format(hex(id(b))))
b = 200
print("The address of b is {} after assignments".format(hex(id(b))))

# output
# The address of a is 0x956a80
# The address of b is 0x956a80
# The address of b is 0x957700 after assignments

python中赋值操作,有点令人迷惑,不同于C++,每个变量的初始化都伴随着内存的分配,但是python中不是这样,执行上述的代码,内存分配的场景如下:

Python中的赋值操作

对于b = a中变量b的初始化,有点像Java中的字符串常量池的意思,如果已经有了相应的值,直接赋值即可,不重新分配内存。但是,当b的值发生变化后,其立刻就会重新分配内存空间,有点延迟分配的意思。

在C++中,函数的参数传递与变量的赋值遵循相同的规则,在python中是否也是一样呢?

Python中的可变对象和不可变对象

文章所示,python中存在可变和不可变的对象,所谓可变是指,对象的内容是可变的;而不可变则相反,表示其内容不可变。

  • 不可变对象:int,string,float,tuple。可理解为C++中,该参数为值传递。
    当不可变的对象发生变化后,并不违反不可变的原则,而是创建了一个新的对象,并将变量指向西对象,如下所示:

    i = 1
    print("The address of i is {}".format(hex(id(i))))
    i += 1
    print("The address of i is {} after change".format(hex(id(i))))
    # The address of i is 0x955e20
    # The address of i is 0x955e40 after change
    

    不可变对象的修改操作

    换句话说,当你不断修改一个不可变的对象时,就是不断在申请内存,OMG。

  • 可变对象:list,dictionary。可理解为C++中,该参数为指针传递。
    在可变对象中,修改其中的内存,不会造成重新申请内存。

    l = [1, 5, 10]
    print("The address of l is {}".format(hex(id(l))))
    print("The address of l is {}".format(hex(id(l[1]))))
    l[1] = 7
    print("The address of l is {} after change l[1]".format(hex(id(l))))
    print("The address of l is {} after change l[1]".format(hex(id(l[1]))))
    
    # The address of l is 0x7f0f25489540
    # The address of l is 0x955ea0
    # The address of l is 0x7f0f25489540 after change l[1]
    # The address of l is 0x955ee0 after change l[1]
    

    当修改可变对象中的值时,对象本身不会新申请内存,但是其中变化的值,需要重新申请内存。

参数传递

了解了变量赋值,可变和不可变对象,下面看一下函数的参数传递。

def func(i: int, f: float, s:str, t: tuple, l: list, d: dict):
    print("i-{}, f-{}, s-{}, t-{}, l-{}, d-{} after call func before change"
        .format(hex(id(i)) ,hex(id(f)), hex(id(s)), hex(id(t)), hex(id(l)) ,hex(id(d))))
    
    l[0] = 3
    d["a"] = 100
    print("i-{}, f-{}, s-{}, t-{}, l-{}, d-{} after call func after change object's objects"
        .format(hex(id(i)) ,hex(id(f)), hex(id(s)), hex(id(t)), hex(id(l)) ,hex(id(d))))
    
    i = 2
    f = 0.2
    s = "hi"
    t = (2, 6, 8)
    l = [2, 6, 8]
    d = {"a": 100, "b": 200}
    print("i-{}, f-{}, s-{}, t-{}, l-{}, d-{} after call func after change object itself"
        .format(hex(id(i)) ,hex(id(f)), hex(id(s)), hex(id(t)), hex(id(l)) ,hex(id(d))))

if __name__ == "__main__":
    i = 1
    f = 0.1
    s = "hello"
    t = (1, 5, 7)
    l = [1, 5, 7]
    d = {"a": 1, "b": 2}
    
    print("i-{}, f-{}, s-{}, t-{}, l-{}, d-{} for initialization"
        .format(hex(id(i)) ,hex(id(f)), hex(id(s)), hex(id(t)), hex(id(l)) ,hex(id(d))))
    func(i, f, s, t, l, d)
    print("i-{}, f-{}, s-{}, t-{}, l-{}, d-{} after call func"
        .format(hex(id(i)) ,hex(id(f)), hex(id(s)), hex(id(t)), hex(id(l)) ,hex(id(d))))
        
    print(i, f, s, t, l, d)

    # output
    # i-0x955e20, f-0x7f48221af2f0, s-0x7f48221441f0, t-0x7f482218b940, l-0x7f482220c540, d-0x7f482227a900 for initialization
    # i-0x955e20, f-0x7f48221af2f0, s-0x7f48221441f0, t-0x7f482218b940, l-0x7f482220c540, d-0x7f482227a900 after call func before change
    # i-0x955e20, f-0x7f48221af2f0, s-0x7f48221441f0, t-0x7f482218b940, l-0x7f482220c540, d-0x7f482227a900 after call func after change object's objects
    # i-0x955e40, f-0x7f48222aca90, s-0x7f4822144170, t-0x7f482216c740, l-0x7f4822136dc0, d-0x7f482224b680 after call func after change object itself
    # i-0x955e20, f-0x7f48221af2f0, s-0x7f48221441f0, t-0x7f482218b940, l-0x7f482220c540, d-0x7f482227a900 after call func
    # 1 0.1 hello (1, 5, 7) [3, 5, 7] {'a': 100, 'b': 2}

从上述的代码中可以看出:

  • 不管是可变还是不可变对象,作为函数参数传递的时刻,不会发生copy,也就不会申请新的内存,内存地址不变。
  • 当在函数内修改可变对象中的对象,如list和dict中的值,不会影响该可变对象的内存地址,但是会影响外部传入对象的值,与C++中的按引用传递一致
  • 不管是可变还是不可变对象,在函数内部进行修改后,都会重新申请内存,生成新的内存地址,与传入的参数解绑,对其的修改也不会影响外部的变量,此时与C++中的按值传递一致

总结

  • 如果传入函数的参数不被修改(无论对象可变还是不可变),就指向同一个地址;
  • 如果函数参数本身变化了(无论对象可变还是不可变),生成新地址,不影响外部变量,与C++中的按值传递一致
  • 如果函数参数中的对象变化了(只针对可变参数):内存地址不变,影响外部变量,与C++中的按引用传递一致

一些资料把这种参数传递方式称为 call by object referenceCall by Sharing

回顾到我们在写python脚本时,对于命令行输入的参数,一般会打包成一个dict,其作为参数传递到各个函数时,不会发生copy,但是要注意,在各个函数内对于该dict中的任意修改,都会影响全局的环境。

参考资料

  1. https://zhuanlan.zhihu.com/p/69746955
  2. https://www.codespeedy.com/find-the-memory-address-of-a-variable-in-python/
  3. https://www.onlinegdb.com/online_python_compiler
  4. https://python-course.eu/python-tutorial/passing-arguments.php

文章作者: alex Li
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 alex Li !
  目录