3. Python之变量, 基本数据类型, 垃圾回收机制
2021-01-09 本文已影响0人
随便写写咯
1 变量
1.1 变量的基本使用
name = "David" # 先定义, 为变量值开辟内存空间, 把变量值存到内存空间里, 同时捆绑指针, 指向变量名.
print(name) # 后引用, 根据变量名, 找到变量值在内存的地址, 把变量值用print打印出来

1.2 变量的内存管理
垃圾回收机制: 回收没有关联任何变量名的值, 也就是如果一个值没有赋值给某个变量名, 无法被访问到, 那就是垃圾
垃圾回收机制-1
引用计数: 一个值捆绑了几个变量名. 如果一个值的引用计数为0了, 那就是垃圾了
# 引用计数增加
x = 10 # 10的引用计数为1
y = x # 10的引用计数为2, 把变量名y也指向10的内存地址
z = x # 10的引用计数为3, 把变量名z也指向10的内存地址
# 引用计数减少
del x # 解除变量名x与值10的绑定关系,10的引用计数变为2
print(x) # 没有结果
del y
print(y) # 没有结果
z = 12345 # 把12345赋值给z, 此时10的引用计数变为0, 因为一个变量名只能捆绑一个变量值
print(z)
>> 12345
总结:
一个变量名只能和一个变量值关联, 一个变量值可以和多个变量名关联
此时10的引用计数已经为0, 需要进行回收, Python解释器会一直扫描程序内存, 如果发现引用计数为0的值, 就会回收, 释放内存空间
变量的三大组成部分
1. 变量名: 是指向等号右侧变量值的内存地址的, 用来访问等号右侧的值
2. 赋值符号: 将变量值的内存地址绑定给变量名
3. 变量值: 代表记录的事物的状态
1.3 变量值的三个重要特征
id: 根据变量值的内存地址, 计算出来的id号码; 内存地址不同, id不同
type: 不同的变量类型, 记录不同的数据状态
name = 'dave'
age = 19
salary = 3.1
value: 值本身
# 查看变量的id号
name = 'james'
print(id(name))
>> 1382608549744
# 查看变量的type
print(type(name))
>> <class 'str'>
1.4 is 与 ==
is: 比较左右两个变量名的身份是否相等, 也就是捆绑的变量值的id是否相同, 也就是变量值的内存地址是否相同
==: 比较左右两个变量名, 它们的值是否相等, 也就是value是否相等, 也就是变量值是否相等

# 两个变量名. 捆绑的变量值一样, 那么两个变量名==成立
>>> x = "info of admin"
>>> y = "info of admin"
>>> print(x == y)
True
# 两个变量名. 捆绑的变量值一样, 那么两个变量名is不一定成立, 也就是两个变量名的id不一定相等
# 因为, 相同的值, 可能被分配到不同的内存空间, 有不同的内存地址, id号就不会相同
>>> print(id(x))
2156607436592
>>> print(id(y))
2156607437424
>>> print(x is y)
False
总结:
两个变量的变量值相同, 身份id不一定相同(is), 即两块不同的内存空间里可以存放相同的值; 注意: 在PyCharm中, 变量值相同, 变量id也会相同, PyCharm做了处理
两个变量的变量值相同, 那么变量value相同(==)
以上案例是分别给x和y赋值相同的变量值, 这时会给变量值分别开辟不同的内存空间, 存放变量值
而如果先给x赋值一个变量值, 再把x赋值给y, 那么结果不同, 因为把x赋值给y, 并没有产生新值, 因为这是把x指向的内存地址又给了y, 并不会给y开闭新的独立内存空间
变量赋值时, 如果 = 右边是一个值, 那么就会开辟新的内存空间, 如果 = 右边是一个变量, 那么新的变量和旧的变量指向同一个内存空间
>>> x = 1
>>> y = x # 把x指向的数字1的内存地址, 捆绑给y
>>> print(x is y) # x和y都指向同一块内存地址, 因此x is y成立
True
>>> print(x == y) # x == y自然也成立
True
1.5 Python预先定义的内存地址
小整数池 (-5 ~ 256)
从Python解释器启动的那一刻开始, 就会在内存中事先申请好一系列内存空间, 存放常用的整数, 字符等
程序运行中, 产生的值如果在小整数池里, 那么就不会开辟新的内存空间, 即使变量值是通过数学运算得出的, 只要在范围内, 就不会开辟内存空间
举例:
>>> m = 10
>>> n = 10
>>> res = 4+6
>>> print(id(m),id(n),id(res))
140487966202448 140487966202448 140487966202448
>>> x = 'aaa'
>>> y = 'aaa'
>>> id(x,y)
>>> id(x)
2156607437552
>>> id(y)
2156607437552
# m和n会共用预先申请好的数字10的内存地址
# 类似对于字符也一样, 常用的字符也都是事先申请好了内存地址,不会开辟新的
# Pycharm扩充了小整数池的范围, Pycharm启动时会申请更大范围的内存空间地址, 为了进一步在Pycharm下提升效率
# 注意: 最终的结果要以Python解释器为准, 因为程序最终是在解释器上运行, 而不是Pycharm
1.6 变量内存地址补充
id不同的情况下, 变量值有可能相同
id相同的情况下, 内存地址一样, 意味着值一定相同
当 x is y 成立, 那么 x == y, 一定成立
当 x == y 成立, x is y, 不一定成立
1.7 常量
不变的量
Python语法中, 没有常量的概念, 无法定义常量
但是在程序开发过程中会涉及常量的概念
Python中, 约定俗成, 把变量名用大写字母, 表示常量赋值, 这只是一种约定, 规范
2 基本数据类型
2.1 数字类型(整型和浮点型)
数学运算
level = 1
level += 1
print(level)
print(10 * 3)
整数可以和浮点数做数学运算
print(10 * 0.3)
print(10 + 3.9)
字符串可以和字符串相加, 列表可以和列表相加, 用来做拼接
print('abc' + ' ' + 'cde')
>> abc cde
l1 = [1,2,3]
l2 = ["4",5,6]
print(l1+l2)
>>>
[1, 2, 3, '4', 5, 6]
比较大小
age = 19
print(age > 19)
>> False
2.2 字符串类型
作用: 记录描述性质的状态, 名字, 一段话,
定义: 用引号包含的一串字符, 单引号, 双引号, 三引号都行, 在定义字符串上没区别
2.2.1 赋值
xxx: 代表的是变量名
'xxx': 代表的是变量值
111: 代表的是变量值
x=y : 把y变量名指向的内存地址付给x
Python中, 变量名必须要被赋值, 否则语法错误. 而如果一个变量值没有关联变量名, 是不会报错的, 解释器不会做任何处理
2.2.2 其他使用
字符串嵌套
print("my name is 'David'")
print ('my name is \'David\'') # 如果内层外层用一样的引号, 那么要加转义
字符串拼接
字符串可以相加做拼接, 但是仅限于字符串与字符串之间
不要用字符串拼接, 字符串相加效率极低
x = 'haha is '
y = 'lala'
print(x + y)
>> haha is lala
print('haha is'+' '+'lala')
>> haha is lala
字符串相乘
print("haha " * 5) # 乘号代表打印前面字符串n遍
>> haha haha haha haha haha
2.3 列表
hobbies = 'read write sing'
print(hobbies[0])
>> r # 通过索引取字符串, 会一个一个字符取
info = ['read',
'music',
'sleep'
]
print(info[2])
>> sleep
列表, 索引对应值, 索引从0开始, 0代表第一个
作用: 按位置记录多个值, 并且可以按照索引取指定的位置的值
定义: 在中括号内, [] 用逗号分隔开多个任意类型的值, 一个值称之为一个元素, 每个索引位对应逗号之间的元素
举例:
索引 0 1 2 3
l = [10, 3.1, 'aaa', ['bbb', 'ccc']]
l = [
10,
3,
1,
'aaa'
,
['bbb', 'ccc']
]
- 取完整列表
print(l)
>> 10, 3.1, 'aaa', ['bbb', 'ccc']]
- 取列表中的特定值, 利用索引编号
print(l[0])
>> 10
print(l[1])
>> 3.1
print(l[3][0]) # 先从l列表中, 取出索引编号为3的值, 再从子列表中取出索引编号为0的值
>> bbb
- 倒序取值
索引 -4 -3 -2 -1
l = [10, 3.1, 'aaa', ['bbb', 'ccc']]
print(l[-1][-1])
>> ccc
- 适合于用列表存储的数据
同种属性的值, 按照位置次序, 依次排开, 通过索引位置编号取值. 取第几个的xxx
比如: 一个人, 12个月全年的工资
- 列表嵌套
students_info = [
['tony', 18, ['jack']],
['jason', 18, ['play', 'sleep']]
]
取出第一个学生的第一个爱好
print(students_info[0][2][0])
>> jack
2.4 字典
对于列表来说, 取列表中的值, 需要能知道值在列表中的索引编号, 如果列表中的值太多, 很难记住每个值的具体位置
列表中的索引, 反映的是顺序, 位置, 对值没有描述性的功能
字典类型: key对应值, 其中key通常为字符串类型(也可以为整型, 但无意义), 因为key对字符串有描述性的功能
作用: 用来存多个值, 每个值都有唯一一个key与其对应, key对值有描述性, 比如一个人的信息
定义: 在花括号内, 用逗号分开多个key:value
举例:
d = {'a': 1, 'b': 2}
print(type(d))
>> <class 'dict'>
取值: 取key对应的值, key用引号引起来
print(d['a'])
print(d['b'])
关于字典补充
定义: {}内用逗号分隔开多个key:value, 其中value可以是任意类型, 但是key必须是不可变类型,通常是存字符串, key不能是可变类型
dic = {'k1':'aaa',
'k2':2222,
'k3':[3333,],
'k4':{'name':'admin'}
}
- 列表 vs 字典
列表有顺序, 取索引位置对应值 ; 字典, 没有顺序的概念, 因为是靠key来取值
列表没有描述性功能, 只能靠索引位置来取值, 字典有描述性功能, 可以靠key来取值
- 字典存个人信息
info = {'name': 'david', 'age': 18, 'sex': 'male', 'salary': 20000}
info = {
'name': 'david',
'age': 18,
'sex': 'male',
'salary': 20000
}
print(info['name'])
>> david
- 列表嵌套字典, 可以用来存放多个同种类型的信息, 比如, 全班同学的信息, 用字典存个人信息, 用列表来区分学号
先从列表中, 按照索引编号取出一组数据, 再按照字典key取对应值
students_info = [
{'name':'admin1','job':'IT','gender':'Female'},
{'name':'admin2','job':'Management','gender':'male'},
{'name':'admin3','job':'Accounting','gender':'Female'},
{'name':'admin4','job':'Finance','gender':'male'},
]
print(students_info[0]['name'])
>> admin1
print(students_info[3]['gender'])
>> Female
- 列表中可以包含重复元素, 而字典的key是唯一的
l5 = [1,2,3,1,1,1]
print(l5)
>> [1, 2, 3, 1, 1, 1]
d1 = {"a":1, "a":2, "b":1, "c":2}
print(d1)
>> {'a': 2, 'b': 1, 'c': 2}
2.5 Bool布尔类型
作用: 用来记录真假两种状态.
定义:
is_ok = True
is_ok = False
print(type(is_ok))
>> <class 'bool'>
使用:
只要能表示出来两种状态即可, True or False; 1 or 0都行, 但是1和0比True和False效率高, 因为计算机只能识别0和1
1 为真, 0|空字符串|None|空列表|空字典为False
通常用来当做判断的条件, 在if条件判断会经常使用到
print(bool(1)) # True
print(bool(100)) # True
print(bool(0)) # False
print(bool("aaa")) # True
print(bool("")) # False
print(bool([])) # False
print(bool({})) # False
x = 2 > 1
y = 3 > 4
print(x,y) ==> True False
类型转换
s1 = "100"
s2 = ""
s3 = "0"
s4 = 3.14
s5 = None
s6 = {}
s7 = []
print(bool(s1)) # True
print(bool(s2)) # False
print(bool(s3)) # True
print(bool(s4)) # True
print(bool(s5)) # False
print(bool(s6)) # False
print(bool(s7)) # False
2.6 基本数据类型总结
总结:如何选择合适的类型来记录状态
1、选取的类型是否可以明确标识事物的状态
2、存不是目的,存的目的是为了日后取出来用,并且方便的用
3、把自己想象成一台计算机,如果我是计算机,
我会如何以何种形式把事物的状态记到脑子里
然后再去Python中找相应的数据类型来让计算机像自己一样去记下事物的状态
3 垃圾回收机制
3.1 引用计数
引用计数: 引用计数增加, 引用计数减少, 引用计数为0时, Python垃圾回收机制会自动清除变量值
直接引用: 变量名直接关联变量值
x = 10
print(x)
>> 10

间接引用: 只出现在容器里, 比如列表, 字典, 通过列表间接获得变量值
x = 10
l = ['a', x]
# 变量名l直接指向该列表在内存中的地址
# 这里的x就是上面预先定义好的变量, 但是是放在列表里面, 通过列表间接引用, 列表中存变量名实际就是存的变量名对应的变量值内存地址
# 此时10的引用计数为2
print(l[1])
>> 10
print(id(x))
print(id(l[1]))
>>
140732149143488 # id相同, 证明了列表中的x对应的就是10在内存中的地址, 通过x和l[1]取到的都是10在内存中的相同的地址
140732149143488
3.2 列表在内存中的存储方式
3.2.1 列表中存变量值时的存储方式

执行l=['a','b'], 会在内存中的栈区开辟空间, 存放变量名l和列表本身在堆区的内存地址的对应关系
在堆区中, 列表的内存空间存的实际是索引值和真正的值在内存中的地址的对应关系
至于真正的值'a'和'b'是存在单独的内存空间
执行l[0], 会通过变量名l和列表的内存地址的对应关系, 找到堆区中的列表
然后, 在堆区列表的内存空间中, 通过索引值0找到对应值'a'的内存地址, 进而再找到'a'
3.2.2 列表中存变量名的存储方式

l变量名直接指向的是其对应的列表在内存中的地址, 这是直接引用
再通过索引2, 取到变量值10在内存中的地址, 这就是间接引用
3.3.3 修改x的赋值会发生什么

x和10的赋值关系会清除, 把111赋值给x, 那么就会在内存中开辟空间给111, 然后和x绑定, x指向111的内存地址
然而, 此时, 对列表是没有影响的, 因为, 列表中, 无论是存变量值, 还是变量名, 最终存的都是实际的值的内存地址, 只要这个内存地址不变, 列表就是不变的
总结:
列表名直接指向列表的内存空间, 间接通过内存空间里的索引对应内存地址指向变量值
3.3.4 列表在堆区, 栈区的存储方式详解
- 定义一个列表
l1 = [111,222,333]
- 将l1赋值给l2
l2 = l1
- 此时内存中列表的存储方式, 如图

- 此时如果执行
l1[0] = 'abcd'
会发生什么?

l1[0] = 'abcd'
print(l1[0],l2[0])
>> abcd abcd
- 同样如果执行
l2[1] = 'efgh'
会发生什么?

l2[1] = 'efgh'
print(l1[1],l2[1])]
>> 'efgh'
3.3 引用计数缺点 - 循环引用
示例:
l1 = [111]
l2 = [222]
l1.append(l2) # 将l2追加到l1, 此时l1存的是['111'的内存地址, l2列表的内存地址]
l2.append(l1) # 将l1追加到l2, 此时l2存的是['222'的内存地址, l1列表的内存地址]
print(id(l2),id(l1[1]))
>> 2060062406144 2060062406144 # 通过id值相等, 证明l1追加的就是l2
print(id(l1),id(l2[1]))
>> 2060062407104 2060062407104 # 通过id值相等, 证明l2追加的就是l1
此时, l1和l2是互相引用的关系
取l1有两种方式
print(l1) # 直接引用
print(l2[1]) # 间接引用
>>
[111, [222, [...]]]
[111, [222, [...]]]
取l2有两种方式
print(l2)
print(l1[1])
>>
[222, [111, [...]]]
[222, [111, [...]]]
# 取111和222也有两种方式
print(l1[0], l2[1][0])
>> 111 111
因为容器中存的是具体的内存地址, 因此, 彼此之间是可以循环引用的
但是, 循环引用带来的内存泄漏问题,容器之间相互引用, 清除了所有容器的直接引用后, 其间接引用依然存在, 但是无法获取, 然而引用计数不为0, 因此无法回收, 所以程序中不要做循环引用


-------------------------------------------------------------------------
补充:
引用计数的另一个问题就是效率, 每次引用计数的增加或者减少都会引发引用计数机制的执行, 这存在明显的效率问题
3.4 标记清除
3.4.1 堆和栈
堆区和栈区: 堆区存变量值, 栈区存变量名
栈区存放变量名和其变量值的内存地址, 通过这个内存地址, 变量名可以找到变量值, Python的垃圾回收机制管理的是堆区, 当堆区中变量值的引用计数为0, 会被回收, 同时其栈区中的变量名也会被回收
开发人员只能操作栈区, 而堆区是由Python解释器去管理的


直接引用指的是从栈区出发直接引用到的内存地址。
间接引用指的是从栈区出发引用到堆区后,再通过进一步引用才能到达的内存地址。

3.4.2 标记清除
标记/清除算法的做法是当应用程序可用的内存空间即将被耗尽时,就会停止整个程序,然后进行两项工作,第一项则是标记,第二项则是清除
#1、标记
通俗地讲就是:
栈区相当于“根”,凡是从根出发可以访达(直接或间接引用)的,都称之为“有根之人”,有根之人当活,无根之人当死。
具体地:标记的过程其实就是,遍历所有的GC Roots对象(栈区中的所有内容或者线程都可以作为GC Roots对象),然后将所有GC Roots的对象可以直接或间接访问到的对象标记为存活的对象,其余的均为非存活对象,应该被清除。
#2、清除
清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
总结: 堆区中的变量值, 至少是能被栈区中的变量名间接引用的, 才会标记为存活, 如果只是在堆区中, 变量值与变量值之间互相引用, 就标记为清除
3.5 分代回收
3.5.1 引用计数缺点2: 效率问题
基于引用计数的回收机制,每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的,于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略
分代:
分代回收的核心思想是:在历经多次扫描的情况下,都没有被回收的变量,gc机制就会认为,该变量是常用变量,gc对其扫描的频率会降低,具体实现原理如下:
分代指的是根据存活时间来为变量划分不同等级(也就是不同的代)
新定义的变量,放到新生代这个等级中,假设每隔1分钟扫描新生代一次,如果发现变量依然被引用,那么该对象的权重(权重本质就是个整数)加一,当变量的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,青春代的gc扫描的频率低于新生代(扫描时间间隔更长),假设5分钟扫描青春代一次,这样每次gc需要扫描的变量的总个数就变少了,节省了扫描的总时间,接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的频率越低
回收:
回收依然是使用引用计数作为回收的依据

虽然分代回收可以起到提升效率的效果,但也存在一定的缺点
#例如一个变量刚刚从新生代移入青春代,该变量的绑定关系就解除了,该变量应该被回收,但青春代的扫描频率低于新生代,这就到导致了应该被回收的垃圾没有得到及时地清理。
没有十全十美的方案:
毫无疑问,如果没有分代回收,即引用计数机制一直不停地对所有变量进行全体扫描,可以更及时地清理掉垃圾占用的内存,但这种一直不停地对所有变量进行全体扫描的方式效率极低,所以我们只能将二者中和。
综上
垃圾回收机制是在清理垃圾&释放内存的大背景下,允许分代回收以极小部分垃圾不会被及时释放为代价,以此换取引用计数整体扫描频率的降低,从而提升其性能,这是一种以空间换时间的解决方案
习题:
1、什么是变量?为何要有变量?
变量是用来记录事务状态
方便以后取出来用
2、变量的三大组成部分是?每部分的作用是什么?
变量名: 通过变量名对应的内存地址, 找到变量值
= : 赋值符号
变量值: 实际的数据
3、变量名的命名原则、规范、风格
变量: 小写字母, 数字, 下划线分开, 见名知意, 不能以数字开头
常量: 大写字母
4、变量值的三个特征是什么?
id: 根据变量值所在内存地址计算出来的id号码
type: 标明变量值类型, 整型, 浮点, 字符串, 列表, 字典...
value: 变量值
5、is与==的区别
is用来判断两个变量名的id是否相同
==用来判断两个变量名对应的变量值是否相同
6、id相同值是否可以相同?
id相同, 那么变量值一定相同, 因为id是根据变量值所在的内存地址计算出来的id号, id相同说明变量值所处的内存地址相同, 那么变量值也一定相同
7、id不同值是否可以相同?
id不同, 变量值也可以相同, 因为, 相同的值, 可以存在不同的内存空间, 不同的内存空间地址对应不同的id, 那么这时即使id不同, 变量值也是相同的
8、用变量的定义说明int、float、str、list、dict、bool类型用于记录何种状态,每种类型,至少写出三个示例,如下所示
int型: 记录整型数字
age = 10
level = 3
year = 1990
float型: 记录小数
salary = 2.5
height = 1.80
weight = 110.5
字符串: 记录字符
name = 'admin'
gender = 'male'
list: 列表, 按位置, 依次排开, 记录相同属性的不同值, 按照索引位置取值
hobby = ['read','sleep','sing']
字典: 用来记录一件事物的不同属性信息, 按照key取value, 字典是无序的
info = {'name':'admin','age':20,'title':'ceo'}
list vs dict:
相同点: 都可以用来存多个值, 属于容器类型
不同点:
1. list是按照索引取值, dict是按照key取value
2. 列表是有序的, 字典是无序的
3. list没有描述性功能, 字典有描述性功能, 因为key一般都是字符串, 而字符串是有描述性的
---------------------------------------------------------------------------------------------
- 如何选择合适的类型来记录状态???
1、选取的类型是否可以明确标识事物的状态
2、存不是目的,存的目的是为了日后取出来用,并且方便的用
3、把自己想象成一台计算机,如果我是计算机,
我会如何以何种形式把事物的状态记到脑子里
然后再去python中找相应的数据类型来让计算机像自己一样去记下事物的状态
- 认真读题,需要自己从题目中分析出应该存储的状态,然后选择合适的类型加以存储
1、病毒程序需要定期将监控到的数据写入日志文件,请记录下日志文件路径C:\a\b\c\adhsvc.dll.system32,方便后期处理
path = 'C:\a\b\c\adhsvc.dll.system32'
2、病毒程序在上传文件时,发送的报头数据里需要包含文件信息:文件名a.txt、大小360,请记录下文件信息
info = {'file_name':'a.txt','size':360}
3、程序运行过程中有一段错误日志需要记录下来,错误日志为"上传文件失败"
error = '文件上传失败'
4、假设我收到一条信息要记录,信息为中病毒客户端的信息"[2020-02-18-17:00:48] 癞蛤蟆病毒感染者-> 80.82.70.187:33649 正在上传数据"
list = ["[2020-02-18-17:00:48]', "癞蛤蟆病毒感染者-> 80.82.70.187:33649", "正在上传数据"]
5、把服务端ip地址存放下来,ip地址为10.0.10.11
ip = "10.0.10.11"
6、病毒程序需要每隔3秒才运行一次,请记录下这个时间间隔
interval = 3
- 嵌套取值操作
1、students_info=[['Jack',18,['play',]],['John',18,['play','sleep']]]
请取出第一个学生的第一个爱好
print(students_info[0][2])
>> play
2、针对字典
info={
'name':'Jack',
'hobbies':['play','sleep'],
'company_info':{
'name':'Anshan',
'type':'education',
'emp_num':40,
}
}
请取出取公司名
print(info['company_info']['name'])
>> Anshan
3、针对下述类型
students=[
{'name':'John','age':38,'hobbies':['play','sleep']},
{'name':'Jack','age':18,'hobbies':['read','sleep']},
{'name':'Lily','age':58,'hobbies':['music','read','sleep']},
]
取第二个学生的第二个爱好
print(students[1]['hobbies'][1])
>> sleep
- 编写用户登录接口
#1、输入账号密码完成验证,验证通过后输出"登录成功"
#2、可以登录不同的用户
#3、同一账号输错三次锁定(附加功能,在程序一直运行的情况下,一旦锁定,则锁定5分钟后自动解锁)
#扩展需求:在3的基础上,完成用户一旦锁定,无论程序是否关闭,都锁定5分钟