zoukankan      html  css  js  c++  java
  • 你对Python变量理解到位了没有,80%的人都不知道

    变量是编程的基础概念,Python 的变量也看似很简单,但是如果理解不当,生搬硬套,可能会遇到一些麻烦。

    下面用 10 个代码示例展示 Python 的 变量 本质。
    不过还要注意:不管你是为了Python就业还是兴趣爱好,记住:项目开发经验永远是核心,如果你没有2020最新python入门到高级实战视频教程,可以去小编的Python交流.裙 :七衣衣九七七巴而五(数字的谐音)转换下可以找到了,里面很多新python教程项目,还可以跟老司机交流讨教!

    1. 只是个名字

    当 a 出现在赋值语句的左侧时,它仅仅代表一个 名字:

    a = 1024
    a = 'davycloud'
    a = ['点赞', '关注', '收藏']
    复制代码

    赋值完成后,这个名字和右侧的对象就 绑定 在一起了。在这个过程中,

    • a 是否已经绑定了其它对象完全不用考虑,也就是说,不管前面 a 绑定了什么,或者什么也没有绑定,它都是个名字。
    • 只要名字是合法的,它可以绑定到任意的对象,完全不用考虑对象的类型

    2. 对象的引用

    绑定了对象的名字,也就是我们常说的 变量,当它出现在赋值语句的右侧时,它代表了 对象的引用:

    a = []
    b = a
    复制代码

    当把 a 赋给 b 的时候,a 代表的是列表对象的引用,也就是说:

    • 列表对象本体不受影响
    • 并不会复制出一个新的列表
    • a 和 b 在这之后都是这同一个列表的引用

    因为列表是可变对象,所以可以通过 a 和 b 任意一个变量改变对象,两者同时都会反映出对象的变化:

    a.append(1)   # a = [1], b = [1]
    b.append(2)   # a = [1, 2], b = [1, 2]
    复制代码

    可变对象下面会再次讨论

    3. 先右 后左

    把多个赋值语句连在一起时,处理的顺序是从右往左:

    a = b = []
    复制代码

    这里 b 首先是充当名字,绑定到一个列表对象;然后又充当对象的引用,赋给另一个名字 a,也就是说,上面的语句等价于:

    # 写法1
    b = []
    a = b
    复制代码

    它和下面的赋值方式有着截然不同的后果:

    # 写法2
    b = []
    a = []
    复制代码

    在这里,两个变量分别绑定了两个不同的列表,它们之间互相并无关联。

    但是这里有个有趣的地方,如果我们把 [] 换成一个整数 1 或者字符串,那么 写法 1 和 写法 2 两种赋值方式在结果上就并无不同。

    4. 变化取决于对象

    继续上面的例子,两个名字绑定到同一个列表,操作其中一个,另一个就受到影响:

    b = []
    a = b
    a.append(1)
    复制代码

    这是因为列表是一个 可变对象。而如果把列表换成数字或者字符串:

    b = 'davy'
    a = b
    a += 'cloud'
    复制代码

    对 a 的自增操作并不会影响到 b。先给出自增操作的等价形式:

    a = a + 'cloud'
    复制代码

    可见,这仍然是一次赋值而已。利用前面的结论:

    • 右边的 a 代表对象的引用,即 'davy',它和 'cloud' 加起来生成一个新的对象
    • 左边的 a 是一个名字,它再次和新对象,即 'davycloud' 绑定在一起

    也就是说,这中间总共产生了 3 个字符串对象。

    我们仔细观察不难看出,当我们想要 改变对象的时候,必须通过的是对象提供的接口(对于列表来说,就是 append 方法,或者下标操作,对于其它对象,可以是改变它的属性),而 不可能通过重新赋值改变对象 。

    赋值只是名字的绑定,再次强调。

    这里我们还能得到另外一个结论:

    不可变对象被多次引用/绑定不会产生副作用。比如说:

    # a, b 绑定到同一个对象
    a = b = 'davy'
    
    # a, b 分别绑定到一个对象
    a = 'davy'
    b = 'davy'
    复制代码

    在上面的示例中,两种绑定的语法含义是不同的,但是,因为字符串是不可变的,也就是说,即使 a 和 b 绑定到同一个字符串,它们也不会互相影响。既然这样,那么又何必在内存中重复创建两个一模一样的的 davy 字符串出来呢。不如直接复用好了,可以节省一点内存:

    >>> a = 'davy'
    >>> b = 'davy'
    >>> a is b
    True
    复制代码

    再次但是,这种优化并不是全局的,也就是说,并不是只要是相同的字符串就一定是唯一的对象:

    >>> a += 'cloud'
    >>> b += 'cloud'
    >>> a is b
    False
    >>> a
    'davycloud'
    >>> b
    'davycloud'
    >>> a == b
    True
    复制代码

    所以呢,大家知道有这种情况就好,对于不可变对象都要使用 == 去比较,而不要用 is,因为它可能会产生时而正确时而错误的诡异结果。

    5. 瞬间交换的秘密

    当一个赋值语句中右侧出现了多个对象,或者多个对象的引用,它们会自动打包成一个元组。

    在赋值语句的左边,需要有相同数量的名字供解包:

    a = [1024]
    b = 'davycloud'
    a, b = b, a
    复制代码

    这个例子中,综合利用前 3 个规则:

    • 先右后左
    • 变量是对象的引用
    • 左侧是名字

    不难得出一个结论,这里两个名字交换了对象的引用,对象本体并没有移动。

    6. 都是名字的错

    a 没有赋值,直接地运行结果:

    >>> print(a)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'a' is not defined
    复制代码

    这里看似很好解释,变量 a 没有定义嘛!加一行赋值语句不就行了。

    但是:

    • 这里报的错误是 NameError: name 'a' is not defined,名字未定义错误,并不是变量未定义
    • 仔细想想,这个 a 到底是什么呢?数字?字符串?函数?类?模块?

    7. 我们都是变量

    接上一个例子:

    a = 1
    
    def a():
        pass
    
    class a():
        pass
    
    import sys as a
    复制代码

    不仅是赋值,定义函数,定义类,导入模块或模块中的对象,都是在绑定名字。

    定义函数是把一个名字绑定到一个函数对象,定义类是把名字绑定到一个类对象,导入一个模块就是把一个名字绑定到一个模块对象。

    Python 中一切皆对象,所以,它们都是变量。

    8. 传参也不过是命名

    既然说到了函数,那就继续来看函数的传参:

    def func(x):
        return x
    复制代码

    这个函数毫无用处,但是正好用来解释参数的传入和传出。

    a = func(1024)
    b = func(a)
    复制代码

    函数中的参数 x 其实也是一个名字,只是它的 作用域 是限定在 func 函数的内部。

    给函数传参就如同是赋值,给这个内部名字绑定一个对象,而出参就好似出现在赋值右侧的变量,就是传出来一个对象引用:

    # 伪代码
    func(1024):
        x = 1024
    a = x
    复制代码

    关于变量的作用域这里点到为止,有机会再详细讨论。

    注意,上面的规则对所有的对象类型都是一样的,无论是可变对象还是不可变对象。

    根据前面的分析,很容易得出结论:

    • 如果参数/返回是不可变对象,那么它是不会产生副作用的
    • 如果参数/返回是可变对象,那么函数内部对它的操作都会影响其它绑定到这个对象上的变量

    因此,对于可变对象的传参需要格外谨慎。特别地,函数的默认参数不要使用可变对象。

    因为默认参数的绑定是在函数定义阶段发生的:

    def get_people(people=[]):
        return people
    复制代码

    在调用 get_people 时,除非给 people 指定一个参数,否则它绑定的总是在函数定义时刻产生的那个列表。而我们的本意可能是,如果默认没有参数,就生成一个空列表。

    一个常规的做法是,在函数内部新建对象:

    def get_people(people=None):
        if people is None:
            people = []
        return people
    复制代码

    9. 删不掉的对象

    Python 提供了 del 关键字可以用来

    删除
    变量,然而实际上这个操作的后果只是把变量名字和对象解绑,然后删掉这个名字。删掉的名字如果再访问,会触发 NameError,就好像它从来没存在过一样。

    而对象呢,它们只是减少了一个引用:

    a = [1, 2, 3]
    b = a
    del a    # 完全不会影响到 b
    复制代码

    一个对象有多个变量引用到的情况下不会被清理很好理解,其实即使当前没有任何名字绑定到这个对象,这个对象也不会立即删除掉:

    >>> a = []
    >>> id(a)
    2292904305736
    
    >>> del a
    >>> a
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'a' is not defined
    >>> b = []
    >>> id(b)
    2292904305736
    复制代码

    总而言之,del 的含义就是 解绑,别指望它删除对象。

    对象的引用计数和销毁是 Python 内部维护的,一般情况下我们无需关心。有兴趣的可以查阅 Python 的垃圾回收相关内容。

    10. 浅拷贝?零拷贝!

    终极例子:

    a = [[]] * 3
    a[0].append(1)
    print(a)     # [[1], [1], [1]]
    复制代码

    这里迷惑性比较强,因为用到了列表的 * 操作。当对一个列表乘法操作时,一般的理解是对其中的元素进行 复制(copy)。

    看到 copy 很容易又会提起所谓的 浅拷贝 和 深拷贝,这里显然不是深拷贝,那么想当然的很容易理解为是浅拷贝,错!

    这里不过又是一次隐形的赋值,让我们把它展开:

    x = []
    y = [x]
    a = [x, x, x]  # a = y * 3 的等价写法
    a[0].append(1)
    print(a)
    复制代码

    最重要的就是第 3 行代码:

    • y * 3 是要把 y 中的元素复制 3 份
    • 现在这个元素就是 x
    • 那么就让 x 重复出现 3 次吧
    • 实际的效果就是 [x, x, x]

    x 是对象的引用,所以我们只是把对象的引用复制了 3 份,对象本体完全没有触及。

    那么如果要真正复制这个列表应该怎么做呢?利用到系统提供的浅拷贝函数,或者是利用 切片:

    # 列表内置的 copy 方法
    a = [x.copy() for i in range(3)]
    
    # 利用切片
    a = [x[:] for i in range(3)]
    
    # 利用 copy 标准库
    from copy import copy
    a = [copy(x) for i in range(3)]
    复制代码

    前面两种方法是列表对象自带的接口,而 copy 模块则更加通用。

    小结

    • 变量指的是名字绑定了对象
    • 绑定时,变量就是名字
    • 使用时,变量代表对象的引用
    • 变量改变的只有绑定关系
    • 想要改变/复制对象,需要看对象有没有提供方法

    最后注意:不管你是为了Python就业还是兴趣爱好,记住:项目开发经验永远是核心,如果你没有2020最新python入门到高级实战视频教程,可以去小编的Python交流.裙 :七衣衣九七七巴而五(数字的谐音)转换下可以找到了,里面很多新python教程项目,还可以跟老司机交流讨教!

    本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理。

  • 相关阅读:
    设计模式-策略模式
    JavaCV开发详解之19:如何开启GPU硬件加速,使用JavaCV进行音视频的硬解码和硬编码(支持intel、amd和nvidia)
    javaCV开发详解之18:音视频转码(音频编解码和视频编解码)
    JavaCV入门指南:FrameConverter转换工具类及CanvasFrame图像预览工具类(javaCV教程完结篇)
    JavaCV入门指南:帧过滤器(FrameFilter)的原理与应用
    JavaCV入门指南:调用opencv原生API和JavaCV是如何封装了opencv的图像处理操作?
    javaCV开发详解之17:GIF和APNG动态图片推流和录制成视频文件(以gif转mp4和apng转mp4为例)
    javaCV开发详解之16:使用一张图片推流和一张图片录制成视频文件
    JavaCV入门指南:帧录制器/推流器(FrameRecorder)的原理与应用
    JavaCV入门指南:帧抓取器(FrameGrabber)的原理与应用
  • 原文地址:https://www.cnblogs.com/chengxuyuanaa/p/12726434.html
Copyright © 2011-2022 走看看