带你从内存的角度看Python中的变量

目录
  • 1、前言
  • 2、引用式变量
  • 3、赋值、浅拷贝与深拷贝
  • 4、is的用法和id()函数
  • 5、函数传参机制
  • 6、扩展阅读
  • 总结

1、前言

由于笔者并未系统地学习过Python,对Python某些底层的实现细节一概不清楚,以至于在实际使用的时候会写出一些奇奇怪怪的Bug(没错,别人写代码,我写Bug),比如对象的某些属性莫名奇妙地改变。究其原因,是对Python中的变量机制存在一些误解,毕竟以前一直是用C语言居多。无奈,只能深入学习这一部分的知识,并总结成此文。

阅读本文,你可以:

  • 了解Python中变量的“储存”机制。
  • 了解Python中赋值、浅拷贝于深拷贝的区别和使用场景。
  • 了解Python中的函数传参形式。

当然,你需要一点基础的编程和面向对象的知识才能看懂本文。

2、引用式变量

相信学过Python的小伙伴都听过这样一句话:Python中一切皆是对象。这意味着,哪怕是Python中的基本数据类型,其本质上也是对象,例如对于一个int类型的变量a,你可以调用int类对象的方法来求a的绝对值:

>>> a = -1
>>> a.__abs__()
1

在这个例子中,可以说:a是int类的一个实例对象,其值是-1。当然,这句话其实说的不对,因为a并不是一个对象,而是对象的引用。这听起来很奇怪,但事实就是如此。Python中的变量都是引用式变量,他并不像C/C++中的变量,储存着具体的数据类型或对象,他像是C++中的引用。通俗的讲,Python中的变量相当于对象的别名,如果你有C语言的基础,可以把它理解为C语言中的指针,通过它你可以在内存中找到对象。话不多说,先看图:

左边的图表示的就是C语言中的变量,变量相当于一个“盒子”,“盒子”里装着值,右边表示的就是Python中的引用式变量,a和b都是列表对象[1, 2, 3]的别名,像是贴在[1, 2, 3]上的”标签“,顺着这些”标签“,解释器可以在内存中找到他们对应的对象。你也许会问,这有啥区别,不都是变量吗。还是先看代码:

a = [1, 2, 3]
b = a
a[2] = 9
print(a)
print(b)

----运行结果----
[1, 2, 9]
[1, 2, 9]

意想不到的事情发生了,明明代码只改变了a的值,为什么b也跟着变了呢?这是因为,a、b都是列表的引用,并不是实际的列表,上述代码通过a这个”标签“改变了内存中列表[1, 2 ,3]的值,于是乎,你顺着b”标签“找到的列表,当然是改变了的。再看代码:

a = [1, 2, 3]
b = a
a = [1, 2, 9]
print(a)
print(b)

----运行结果----
[1, 2, 9]
[1, 2, 3]

在这个例程中,我们把[1, 2, 9]赋值给了a,然后再输出a和b,此时a已经发生变化,而b没有改变,a从列表[1, 2, 3]的引用变成了列表[1, 2, 9]的引用,列表[1, 2, 3]在内存中并未发生任何改变,这就是b输出的值不发生变化的原因。到这里,你应该可以理解上面说的:a是int类的一个实例对象,其值是-1为什么是错的了。这样的赋值语句在Python中的应该这样理解:创建一个int类对象-1,让a作为-1的引用。当然,右边的值是常量或是可变对象,解释器都会做出不同的反应,这将在下文进一步讲解。总之,啰啰嗦嗦说了这么多,就是希望大家都能搞明白这个问题,核心就是一句话:Python中的变量都是引用式变量,变量存储的不是值,而是引用。

3、赋值、浅拷贝与深拷贝

看完上一节,肯定有人会问,如果Python中的赋值都是引用,那我想创建一个变量的副本做备份怎么办?这在C语言中简单的一句b=a就可以实现的需求在Python中如何实现?Python中提供了三种复制的方式,即:

  • 赋值:创建对象的引用。
  • 浅拷贝:拷贝对象,但不拷贝对象内部的子对象。
  • 深拷贝:拷贝对象,并且拷贝对象内部的子对象。

一如既往地先看代码,毕竟代码最能说明问题:

import copy
a = [1, 2, [3, 3 , 3], [4, 4]]
b = a # 赋值
c = a.copy() # 浅拷贝,调用对象的copy()方法
d = copy.deepcopy(a) # 深拷贝,需要引入copy模块,使用deepcopy()方法
a[1] = -2  # 改变1
a[2] = [-3, -3, -3]  # 改变2
a[3][0] = -4  # 改变3
print(a)
print(b)
print(c)
print(d)

----运行结果----
[1, -2, [-3, -3, -3], [-4, 4]]
[1, -2, [-3, -3, -3], [-4, 4]]
[1, 2, [3, 3, 3], [-4, 4]]
[1, 2, [3, 3, 3], [4, 4]]

为了更方便阐述,这里我先给出这个例程中对象在内存中的变化情况,当然我更建议你自己去这个网站逐步可视化地运行上面的代码,甚至是本文中的所有代码,这能加深你的理解。

在这段代码中,首先创建了一个列表对象,这个列表的第3、4个元素也是列表对象,a是这个列表的引用,把a赋值给b,此时b也是同一个对象的引用,在内存中,它们指向同一个对象,因此可以看到无论怎么通过a改变这个对象,a和b都是相同的。c则是对a的浅拷贝,解释器新开辟了一块内存,存储了原列表的一个副本,但是由于是浅拷贝,对象内部的子对象没有被拷贝。因此,这个副本列表的后面两个元素依旧和原列表一样,是列表[3, 3 , 3]和[4, 4]的引用,在内存中指向同样的对象。代码中的改变2让原列表的第三个元素变成了另一个列表[-3, -3 , -3]的引用,但是这个副本列表的第三个元素还是[3, 3 , 3]的引用。改变3则修给了原列表第四个元素指向的列表中的一个元素,因此打印c你会发现它指向的列表对应位置的元素也改变了。而对于d,d是a的深拷贝,解释器新开辟了一块内存,完全复制了原列表对象(包括子列表对象)放在这块内存中。因此,d指向的对象和a指向的对象没有任何关系,无论怎么改变a指向的那个列表,都不会影响d指向的列表。

看到这里,你应该知道如何实现本节开头的需求了。

4、is的用法和id()函数

在Python中,每个对象都有各自的编号、类型和值,一个对象被创建以后,它的编号就不会改变,可以理解为对象在内存中的地址。id()函数可以获取对象的编号,在CPython解释器中,这个编号就是对象在内存中的地址。is是一个双目运算符,运算结果是布尔变量,用来比较两个对象的编号是否相同,准确的说,可以用于比较两个变量是否是同一个对象的引用。

a = [1, 2, 3]
b = a  # 赋值
c = a.copy()  # 浅拷贝
print(id(a))
print(id(b))
print(id(c))
print(a is b)
print(a is c)

----运行结果----
2667871075272
2667871075272
2667871075208
True
False

显然,a、b是同一个对象的引用,而c是浅拷贝的副本,因此a和c引用的不是同一个对象,即使这两个对象的值相等。不知你是否还记得,第1节中还提到在赋值语句中,右边是可变对象与不可变对象,解释器会由不同的操作,比如下面的代码:

a = 5
b = 5
print(a is b)
c = [1, 2, 3]
d = [1, 2, 3]
print(c is d)

----运行结果----
True
False

对a、b分别赋值为5,但是它们却是同一个对象的引用,这是因为,5是一个常量,对应的int类对象就是不可变的对象。Python解释器认为,这样的不可变对象,只需要在内存中存在一个就可以,因此,a和b指向同一个对象。而对于列表[1, 2, 3],由于列表是可变对象,即使这两个对象的值相同,但它们不指向同一个对象。毕竟,谁也不知道后面的程序中会不会改变其中一个列表中的值。说到这里,或许能够解释Python的作者为什么要将Python的变量设计成只有引用式变量了,按照笔者粗浅的理解,这样做的优势在于可以节约内存。毕竟,Python为了能够”简洁、优雅“,为了能够用一行代码解决C语言用20行代码才能解决的问题,在性能上牺牲了不少。

5、函数传参机制

在Python中,函数传参同样传递的是对象的引用,函数参数是不可变对象时,这没有什么讨论的价值。但是,倘若传递的参数是可变对象,如果你不注意这一点,Bug可能就会默默地在凝视你,譬如:

def test1(a):
    a[-1] = 'end'

a = [1, 2, 3]
test1(a)
print(a)

----运行结果----
[1, 2, 'end']

可以看到,在运行完函数test1后,a的值改变了,如果你不想让他改变,这是Bug就来啦。

同样,还有需要注意的一点是,不要把参数的默认值设置成一个可变对象,否则Bug大概已经在和你招手了:

# 用可变对象做参数默认值带来的bug
# 例程来源于《流畅的Python》
class HauntedBus():
    def __init__(self, passengers=[]):
        self.passengers = passengers

    def pick(self, name): # 乘客上车
        self.passengers.append(name)

    def drop(self, name): # 乘客下车
        self.passengers.remove(name)

bus1 = HauntedBus(['zhang_san', 'li_si'])
bus1.pick('wang_mazi')
bus1.drop('zhang_san')
print(bus1.passengers)

bus2 = HauntedBus()
bus2.pick('zhao_wu')
print(bus2.passengers)

bus3 = HauntedBus()
print(bus3.passengers)
print(bus2.passengers is bus3.passengers)
print(bus3.passengers is bus1.passengers)

----运行结果----
['li_si', 'wang_mazi']
['zhao_wu']
['zhao_wu']
True
False

你会惊奇地发现,bus3.passengers难道不应该是空列表吗?这是因为,HauntedBus的构造函数中passengers的默认值是一个可变对象,在对bus2进行操作的时候,由于引用式变量的特性,改变了默认值指向的可变对象。于是乎,就出现了意向不到的Bug。

6、扩展阅读

讲到这里,其实本文的主要内容就基本讲完了。本节的内容,除非说你需要开发自己的Python库,否则了解与否都基本不会影响你使用Python,你完全可以跳过本节,完结撒花。

垃圾回收:在其他编程语言中都会讨论变量或对象的生存周期,会有垃圾回收机制,但在Python中好像很少谈及这个问题。实际上,Python也存在垃圾回收机制,Python中每个变量都是对象的引用,如果某个对象不再被引用,这个对象就会被销毁,这就是Python中的垃圾回收机制。del语句可以删除变量,解除变量对对象的引用,如果这是对象的最后一个引用,这个对象就会被销毁。

弱引用:弱引用不增加对象的引用数,若对象存在,通过弱引用可以获取对象。若对象已被销毁,则弱引用返回None,这常用于缓存中。

最后,本文的目的在于帮助那些像我一样从C语言转移到Python的人,或者是被Python的变量、拷贝整得晕头转向的人。为了让小白也有可能能看懂本文,我尽量写得通俗易懂。但是限于本人水平,难免会有谬误或疏漏之处,如有发现,烦请再评论区指正,over。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • Python中的变量赋值

    目录 1 变量.对象.引用 2 对象的垃圾回收机制 3 变量所指向的对象不同会有何不同? 引言: Python中的变量在使用中很流畅,可以不关注类型,任意赋值,对于开发来说效率得到了提升,但若不了解其中的机理,往往也会犯一些小错,让开发进行的不那么流畅,本文就是从语言设计和底层原理的角度,带大家理解Python中的变量. 下面我们从一个简单例子开始: a = 3 当我们代码中写入a=3时到底发生了啥,从概念上来说,Python会执行三个不同的步骤来完成这个请求: 创建了一个对象来代表值3 若是a

  • Python变量基础知识

    目录 1.什么是变量 2.变量的命名规则 3.python中的关键字和保留字 4.常用的变量名 5.变量的赋值 6.变量的数据类型 6.1.数字 6.2.布尔类型 6.3.字符串 6.4.type()函数 1.什么是变量 所谓变量,是指程序运行过程中其值可以改变的量. 举例:在数学中x和y就是变量,Python中不同的是变量不只是存储数字,它可以存储任意数据类型的值. 2.变量的命名规则 变量名只能包括字母.数字和下划线 一个字符不能使用数字 变量名区分英文大小写 不能使用关键字和保留字 3.p

  • Python中的变量与常量

    目录 一.变量.常量的区别 二.变量 1. Python中的变量不需要声明类型 2. 用"="号来给变量赋值 3. 赋值 4. 变量 5. "=" 6. Python允许同时为多个变量赋值 三.常量 四.总结 一.变量.常量的区别 变量:在程序运行过程中,值会发生变化的量. 常量:在程序运行过程中,值不会发生变化的量. 无论是变量还是常量,在创建时都会在内存中开辟一块空间,用于保存它的值. 二.变量 1. Python中的变量不需要声明类型 这是根据Python的动

  • 详解python的变量

    目录 1.Python 变量的概述: 2.Python 变量的命名 3.Python 变量赋值 3.1 Python 变量赋值概述 3.2 Python 变量的基本赋值格式 3.3 Python 变量的其他赋值格式 3.3.1 同时给多个变量赋同一个值 3.3.2 同时给多个变量赋不同的值 4 Python 变量值得交换 5 查看变量的数据类型 5.1 获取变量在内存中的 id 标识 总结 1.Python 变量的概述: 变量,英文叫做 Variable. 从形式上看,每个变量都拥有独一无二的名

  • 深入了解Python中的变量

    目录 1 Python变量概述 2 Python变量的命名 3 Python变量赋值 3.1 Python赋值概述 3.2 Python变量的基本格式 3.3 Python变量的其他赋值格式 3.3.1 同时给多个变量赋同一个值 3.3.2 同时给多个变量赋不同的值 4 Python变量值的交换 5 查看变量的数据类型 5.1 查看变量的数据类型 5.2 获取变量在内存中的id标识 参考: 总结 1 Python变量概述 变量,英文叫做 variable.在<计算机科学概述>中是这样定义的,&

  • 带你从内存的角度看Python中的变量

    目录 1.前言 2.引用式变量 3.赋值.浅拷贝与深拷贝 4.is的用法和id()函数 5.函数传参机制 6.扩展阅读 总结 1.前言 由于笔者并未系统地学习过Python,对Python某些底层的实现细节一概不清楚,以至于在实际使用的时候会写出一些奇奇怪怪的Bug(没错,别人写代码,我写Bug),比如对象的某些属性莫名奇妙地改变.究其原因,是对Python中的变量机制存在一些误解,毕竟以前一直是用C语言居多.无奈,只能深入学习这一部分的知识,并总结成此文. 阅读本文,你可以: 了解Python

  • python中的变量如何开辟内存

    python下的变量 不需要预先声明变量的类型,变量的类型和值在赋值的那一刻被初始化(声明和定义的过程一起完成) 在python中, 每一个变量在内存中创建,我们可以通过变量来查看内存中的值 哈哈,这里是不是很熟悉,跟c中的指针一样啊(访问内存中的值) 首先大家需要了解一点:在python中: x =5之后,我们要了解它的过程:系统先是找了一块内存,将5存储了进去,紧接着x指向了当前的这块内存 预测1:python下的变量是一个指针 >>> x = 4 >>> y =

  • python中查看变量内存地址的方法

    本文实例讲述了python中查看变量内存地址的方法.分享给大家供大家参考.具体实现方法如下: 这里可以使用id >>> print id.__doc__ id(object) -> integer Return the identity of an object. This is guaranteed to be unique among simultaneously existing objects. (Hint: it's the object's memory address

  • Python中查看变量的类型内存地址所占字节的大小

    Python中查看变量的类型,内存地址,所占字节的大小 查看变量的类型 #利用内置type()函数 >>> nfc=["Packers","49"] >>> afc=["Ravens","48"] >>> combine=zip(nfc,afc) >>> type(combine) <class 'zip'> 查看变量的内存地址 #利用内置函数

  • Python中的变量,参数和模块介绍

    目录 前言 1 变量 2 参数 3 模块 前言 简单的使用python函数之后,我们在日常开发中还需要经常使用的三个地方,分别是变量.参数和模块.其中,Python的变量类型已经在语法介绍中做了简单的使用描述.在本篇文章中,会更加强调变量的作用域,并分别介绍参数和模块的使用. 1 变量 首先,在python中,变量是存储在内存的值,程序在执行创建变量时会在内存中创建一个空间,并且根据变量的数据类型,python解析器会分配指定内存.变量标记或者指向一个值. 示例如下:与剧中的 color 就是一

  • 解析Python中的变量、引用、拷贝和作用域的问题

    在Python中,变量是没有类型的,这和以往看到的大部分编辑语言都不一样.在使用变量的时候,不需要提前声明,只需要给这个变量赋值即可.但是,当用变量的时候,必须要给这个变量赋值:如果只写一个变量,而没有赋值,那么Python认为这个变量没有定义.如下: >>> a Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'a'

  • Python中可变变量与不可变变量详解

    目录 一 .常见的变量分类 1.变量的创建 二.变量分类 1..常见的不可变变量 2.常见的可变变量 三.拷贝的差别 四.参数传递的差别 前言: C++不同于Python的显著特点,就是有指针和引用,这让我们在调用参数的时候更加清晰明朗.但Python中没有指针和引用的概念,导致很多时候参数的传递和调用的时候会产生疑问:我到底是复制了一份新的做操作还是在它指向的内存操作? 这个问题根本上和可变.不可变变量有关,我想把这个二者的区别和联系做一个总结,以更深入地理解Python内部的操作.我本身非科

随机推荐