【PyQt5 基础之 Python 备忘录】- 06 - 全局/局部变量、函数传值/传址(深入探析 python 对象引用)

6 全局/局部变量、函数传值/传址

python 的全局变量使用与 c++ 略有不同

6.1 局部变量 vs. 全局变量
  • 局部变量在函数内部有效,全局变量在整个 py 脚本有效

  • 函数内部的局部变量与全局重名,优先使用局部变量的值

  • 函数内部想修改全局变量的值,需要 global 声明

    num = 10
    def var_func1():
        num = 20
        print('local num = {}'.format(num))
    
    def var_func2():
        global num
        num = 20
        print('global num = {}'.format(num))
    
    var_func1()   # 局部变量覆盖全局
    print('global num = {}'.format(num)) # 全局变量未被修改
    var_func2()   # 全局变量被 global 修改
    print('global num = {}'.format(num)) # 全局变量被 global 修改 
    # ----- output
    local num = 20
    global num = 10
    global num = 20
    global num = 20
    
6.2 函数传值 vs. 传址

在 python 语言体系中,万物皆对象,皆是一段内存空间。python 参数传递是通过对象引用完成,而不是传值或传址。

6.2.1 python 函数参数是在传值 or 传址? 答:都不是
  • 先看下面的例子,num 是外部变量,传入函数内部后,打印其 id 跟原来的 num 是一致的,从这一点来看,参数传递后与外部变量的内存地址一致:

    num = 10
    def addr_func(x):
        print('x id = {}'.format(id(x)))
    
    print('num id = {}'.format(id(num)))
    addr_func(num)
    # ----- output
    num id = 94428851220224
    x id = 94428851220224
    
  • 如果是传地址,我们应该能在函数内部修改外部变量的值,那么再看下面的例子,在 python 函数内修改外部变量会新开辟内存空间,可以看到 x 的值修改后,与一开始的地址不相同了,但并不会修改外部变量 num 的值,这与我们的猜测是不一样的,那么 python 函数到底是在传值还是传地址呢?实际上都不是,见下文解释。

    num = 10
    def addr_func(x):
        print('x id = {}'.format(id(x)))
        print('x = {}'.format(x))
        x = 11
        print('x = {}'.format(x))
        print('x 修改值之后 id = {}'.format(id(x)))
        print('外部 num = {}'.format(num))
    
    print('num id = {}'.format(id(num)))
    addr_func(num)
    
    # ----- output
    num id = 94169056915200
    x id = 94169056915200
    x = 10
    x = 11
    x 修改值之后 id = 94169056915232
    外部 num = 10
    
6.2.2 python 的“整数对象池”
  • 插句嘴,为了提高效率,在交互式模式下 python 为 [-5, 257) 之间的整数预分配了内存空间,测试如下:

    >>> a = 257
    >>> b = 257
    >>> print(id(a),id(b))
    139885869239760 139885869240112 # 地址不一样
    >>> a,b = 256,256
    >>> print(id(a),id(b))
    94287553596864 94287553596864 # 地址一样
    
  • 而在 pycharm 编译器中,“小整数池”变为“大整数池”,所有同一代码块的大整数是同一个对象

    a, b, c, d, e = -6, -5, 10, 256, 257
    a1, b1, c1, d1, e1 = -6, -5, 10, 256, 257
    print(id(a), id(b), id(c), id(d), id(e))
    print(id(a1), id(b1), id(c1), id(d1), id(e1))
    # ----- output
    140623123977136 94500269169952 94500269170432 94500269178304 140623360587696
    140623123977136 94500269169952 94500269170432 94500269178304 140623360587696
    
6.2.3 python 函数参数传递的奥义

Python’s argument passing model is neither “Pass by Value” nor “Pass by Reference” but it is “Pass by Object Reference”.

  • 针对 python 参数传递机制而言,对于已经接触过诸如 c++ 这种面向对象的编程语言的同志是会产生误解的,而这份误解主要是纠结 python 中到底是传值还是传址,如果我想传值或传址应该怎么做?

  • 先上结论,准确来说,python 参数传递既不是传值也不是传址,而是传递对象的引用,而有趣的一点是,你作为 python 的使用者,也不能随心所欲的决定是传值还是传址。

  • 这一特性让我浑身难受,那为什么会这样呢,来看下面这个例子[1],在 c++ 中(忽略语法)下面的两句表达式的意思是,我开辟了一段内存空间,给他一个别名叫 a,然后存储了数值 1,随后,这段内存空间的值更新为了 2

    a = 1
    a = 2
    
  • 但是!!! python 中的运作机制并非如此,还是那句经典台词,在 python 中,万物皆对象,上面 6.2.2 为啥要插播一节整数池,因为整数在 python 中也是一个个对象。

  • 用 python 的对象机制来解释上面的表达,是这样的:一个整数对象的值是 1,而 a 一开始作为该对象的引用出现了,然后又被重新指定成为另一个值为 2 的整数对象的引用,这两个整数对象会一直存在,即使 a 不再引用他们,他们也可以被程序中的其他引用所共享。

  • 在 python 中,当调用含参数的函数时,会创建一个被传递参数对象的新的引用,这与函数内部调用时的引用是完全独立的,参考下面的例子,首先默念口诀 “万物皆对象”,一起看,self.variable 是字符串对象 'Original' 的引用,当调用 change(self, var) 函数时,var新创建的一个引用

  • 一开始,它在没执行 var = 'Changed'这句时,它是字符串对象 'Original' 的另一个引用,也就是说,这时候你去查 varid() 你会发现和 self.variable 一样,执行 var = 'Changed'这句之后,var 就成了一个新的字符串对象 'Changed' 的引用;

  • 从这个角度来理解,不论参数里的 var 是可变(列表)还是不可变(数字、字符串)对象,你想直接利用 var = XX 来改变外部参数的值都是不可行的。

    class PassByReference:
        def __init__(self):
            self.variable = 'Original'
            self.change(self.variable)
            print(self.variable)
    
        def change(self, var):
            var = 'Changed'
            
    a = PassByReference()
    # ----- output
    Original # self.variable 并没有被改变
    
6.2.4 关于 python 参数传递的思考
  • 大多数博客和文章都把 python 的参数传递机制分为 可变和不可变参数的传递,这里借用 stack over flow 上回答的一张图片,来解释 python 中参数和变量的关系:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u1owqQAy-1652950081251)(.assets/image-20220519162149529.png)]

  • 当 B 是函数参数,A 是外部变量时,如果修改 B 内部的元素,那么 A 也会被修改;而如果在函数内直接给 B 整体赋值,则 A 不会被修改,此时 B 会成为新的对象 ‘Hello’ 的引用。

  • 从这里进一步看,如果 B 本身是可变对象,例如列表,自然可以进行 B.append() 之类的操作,如果 B 是字符串,是不可变对象,我们本身无法修改字符串中的字母,我们使用内置函数去修改,会返回新的字符串对象,所以大多数博客中所说 “不可变对象是传值”,也可以这么理解。

  • 还有一点要注意,即使我们传的参数是可变对象,我们也不能通过直接整体赋值去改变外部的对象值

    def change_list(list_out):
        print('list_out = {}'.format(list_out))
        print('list_out.id() = {}'.format(id(list_out)))
        list_out[0] = 5
        print('list_out after change [0] = {}'.format(list_out))
        print('list_out.id() = {}'.format(id(list_out)))
        list_out = [7, 8, 9]
        print('list_out after reassign = {}'.format(list_out))
        print('list_out.id() = {}'.format(id(list_out)))
    
    
    print('ls = {}'.format(ls))
    print('ls.id() = {}'.format(id(ls)))
    change_list(ls)
    print('ls after change reassign = {}'.format(ls))
    print('ls.id() = {}'.format(id(ls)))
    
    # ----- output
    ls = [1, 2, 3, 4] # 外部的 ls 是一个列表对象的引用
    ls.id() = 140130142424832 # 原始的列表对象 id
    list_out = [1, 2, 3, 4] # 传入函数内部引用,新建引用 list_out
    list_out.id() = 140130142424832 # 新引用list_out也是原始列表对象[1, 2, 3, 4]的引用
    list_out after change [0] = [5, 2, 3, 4] # 修改0号元素
    list_out.id() = 140130142424832 # id 并未变化,说明直接操作对象[1, 2, 3, 4]
    list_out after reassign = [7, 8, 9] # 直接给 list_out 引用重新赋值
    list_out.id() = 140130142457600 # list_out 不再指向 [1, 2, 3, 4]
    ls after change reassign = [5, 2, 3, 4] # 但直接赋值不影响外部的引用 ls
    ls.id() = 140130142424832 # ls 的 id 也不会变还是 对象[1, 2, 3, 4]
    
6.2.5 静不下心来看的话…
  • 上面的讲解需要一步步仔细的去看,相信你进入这篇博客也是为了寻求一个确定的答案,答案就是:
  • 对象作为函数参数传递时:
    • 更新对象中的元素值(前提是可变对象),例如 A[0] = B,会同步修改函数外部的对象;
    • 更新整个对象的值,例如 A = B不会修改外部对象;
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值