一 、垃圾回收机制(了解) 垃圾回收机制(简称GC)是Python解释器自带一种机,专门用来回收不可用的变量值所占用的内存空间
1.1 垃圾回收机制介绍 1.1.1 什么是垃圾?
解释器:执行到定义变量的语句 –> 申请内存空间来存放变量的值
内存的容量是有限的
当一个变量值没有用了(简称垃圾)就应该将其占用的内存给回收掉。
怎么判断变量值没有用了?
毫无疑问,内存空间的申请与回收都是非常耗费精力的事情,而且存在很大的危险性,稍有不慎就有可能引发内存溢出
问题,好在CPython解释器
提供了自动垃圾回收机制
来帮我们解决了这件事。
1.1.2 为什么要用垃圾回收机制? 程序运行过程中会申请大量的内存空间,而对于一些无用的内存空间如果不及时清理的话会导致内存使用殆尽(内存溢出),导致程序崩溃,因此管理内存是一件重要且繁杂的事情,而python解释器自带的垃圾回收机制把程序员从繁杂的内存管理中解放出来。
1.1.3 垃圾回收机制原理分析 Python的GC模块主要运用了“引用计数”(reference counting)来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用的问题,并且通过“分代回收”(generation collection)以空间换取时间的方式来进一步提高垃圾回收的效率。
1.2 引用计数 1.2.1 什么是引用计数 引用计数就是:变量值被变量名关联的次数
age = 18
# 变量age --> 值18的内存地址 --- 引用计数为1
引用计数增加:
age=18 # 此时,值18的引用计数为1
m=age # 变量m也记录了值18的内存地址,所以值18的引用计数为2
引用计数减少:
age=10 # 变量age先与值18解除关联,再与值10建立了关联,变量值18的引用计数为1
del m # del的意思是解除变量名x与变量值18的关联关系,此时,值18的引用计数为0
值18的引用计数一旦变为0,其占用的内存地址就应该被解释器的垃圾回收机制回收
1.2.2 直接引用和间接引用 除了有引用计数,还有直接引用与间件引用的概念,我通过一个例子来分析
x = 18 # 直接引用,变量x记录了值18的内存地址,值18的引用计数为1
mylist = [1,x] # 间接引用,列表mylist间件记录了值18的内存地址,值18的引用计数为2
x = 200
print(mylist[1]) # 输出18还是200?答案还是输出18!像这种情况我们可以称值18是被mylist间件引用
这里又涉及到一个问题,即列表在内存中的存储方式 ,可以参考其它文章学习
1.2.3 引用计数带来的循环引用问题 引用计数机制还存在着一个致命的弱点,即循环引用(也称交叉引用)
# 如下我们定义了两个列表,简称列表1与列表2,变量名l1指向列表1,变量名l2指向列表2
>>> l1=['xxx'] # 列表1被引用一次,列表1的引用计数变为1
>>> l2=['yyy'] # 列表2被引用一次,列表2的引用计数变为1
>>> l1.append(l2) # 把列表2追加到l1中作为第二个元素,列表2的引用计数变为2
>>> l2.append(l1) # 把列表1追加到l2中作为第二个元素,列表1的引用计数变为2
# l1与l2之间有相互引用
# l1 = ['xxx'的内存地址,列表2的内存地址]
# l2 = ['yyy'的内存地址,列表1的内存地址]
>>> l1
['xxx', ['yyy', [...]]]
>>> l2
['yyy', ['xxx', [...]]]
>>> l1[1][1][0]
'xxx'
循环引用会导致:值不再被任何名字关联,但是值的引用计数并不会为0,应该被回收但不能被回收 ,什么意思呢?试想一下,请看如下操作
>>> del l1 # 列表1的引用计数减1,列表1的引用计数变为1
>>> del l2 # 列表2的引用计数减1,列表2的引用计数变为1
此时,只剩下列表1与列表2之间的相互引用,两个列表的引用计数均不为0,但两个列表不再被任何其他对象关联,没有任何人可以再引用到它们,所以它俩占用内存空间应该被回收,但由于相互引用的存在,每一个对象的引用计数都不为0,因此这些对象所占用的内存永远不会被释放,所以循环引用是致命的,这与手动进行内存管理所产生的内存泄露毫无区别。 所以Python引入了标记-清除
与分代回收
来分别解决引用计数的循环引用与效率低的问题
1.2.4 克服问题:标记/清除 “标记-清除”机制用来解决循环引用的问题
在了解标记清除算法前,我们需要明确一点,关于变量的存储,内存中有两块区域:堆区
与栈区
,在定义变
量时,变量名与值内存地址的关联关系存放于栈区,变量值存放于堆区,内存管理回收的则是堆区的内容,详解如下图,
定义了两个变量x = 10、y = 20
当我们执行x=y时,内存中的栈区与堆区变化如下
标记/清除算法的做法是当应用程序可用的内存空间被耗尽的时,就会停止整个程序,然后进行两项工作,第一项
则是标记,第二项则是清除。
标记:
遍历所有的GC Roots对象(栈区中的所有内容或者线程都可以作为GC Roots对象),然后将所有GC Roots的对象
可以直接或间接访问到的对象标记为存活的对象,其余的均为非存活对象,应该被清除。
通俗的讲就是:遍历一遍这条路,如果不通,说明是非存活的,需要清除
清除:
清除的过程将遍历堆中所有的对象,将没有标记存活的对象全部清除掉。
用1.2.3的例子
来分析运作流程:
直接引用指的是从栈区出发直接引用到的内存地址,间接引用指的是从栈区出发引用到堆区后再进一步引用到的内
存地址,以我们之前的两个列表l1与l2为例画出如下图像
当我们同时删除l1与l2时,会清理到栈区中l1与l2的内容
这样在启用标记清除算法时,发现栈区内不再有l1与l2(只剩下堆区内二者的相互引用),于是列表1与列表2都没有被标记为存活,二者会被清理掉,这样就解决了循环引用带来的内存泄漏问题。
1.2.5 克服问题:分代回收 分代回收机制用来解决引用计数效率慢
的问题
基于引用计数的回收机制,每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的,于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略。
分代:
分代回收的核心思想是:在历经多次扫描的情况下,都没有被回收的变量,gc机制就会认为,该变量是常用变量,gc对其扫描的频率会降低,具体实现原理如下:
分代指的是根据存活时间来为变量划分不同等级(也就是不同的代)
新定义的变量,放到新生代这个等级中,假设每隔1分钟扫描新生代一次,如果发现变量依然被引用,那么该对象的权重(权重
本质就是个整数)加一,当变量的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,青春代的gc扫描的频
率低于新生代(扫描时间间隔更长),假设5分钟扫描青春代一次,这样每次gc需要扫描的变量的总个数就变少了,节省了扫描
的总时间,接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的
频率越低
回收:
回收依然是使用引用计数作为回收的依据
虽然分代回收可以起到提升效率的效果,但也存在一定的缺点:
例如一个变量刚刚从新生代移入青春代,该变量的绑定关系就解除了,该变量应该被回收,但青春代的扫描频率低
于新生代,
二、用户交互 2.1 输入 计算机接收用户的输入,案例如下:
>>> username = input('请输入用户名: ')
请输入用户名: Goosh
>>> print(username, type(username))
Goosh <class 'str'>
注意:input会将用户输入的所有内容都存储成字符串类型
如果需要当作其它类型使用,可以使用类型转换
>>> num = input('请输入数字: ')
请输入数字: 80
>>> 20 > int(num) # 使用int()将字符串转换为整型
False
补充:
Python2和Python3在这里有不同:
Python3:
Python2:
raw_input() 输入都存储成字符串类型
input()输入的是什么类型,存储就是什么类型(自动判断)
2.2 输出 格式化输出字符串分为4种方式
%号
str.format(推荐)
f-String(兴趣了解)
标准库模板(兴趣了解)
2.2.1 %号方式 从python诞生之初就有,官方未弃用这种方式,但是并不推荐使用
# 普通方式传入:传入的值与s%必须位置对应
name = 'Goosh'
age = 60
print('My name is %s, age is %s' % (name, age))
# 字典方式传入:不需位置对应
print('My name is %(name)s, age is %(age)s' % {'age':28, 'name':'Goosh'})
在Python2.6种引入,是字符串类型的内置方法,该方法在性能和使用灵活性上都优于s%,推荐使用
# 使用位置参数
print('{} asked {} to do something'.format('Rick', 'Mody'))
# 输出:Rick asked Mody to do something
# 使用索引对应位置的值
print('{0}-{0}-{1}-{0}'.format('x', 'y'))
# 输出:x-x-y-x
# 使用关键字参数
print('My name is {name}, age is {age}'.format(name='Rick', age=18))
# 输出:My name is Rick, age is 18
# 填充与格式化
print('{0:*<10}'.format('开始执行')) # 左对齐,不足10个字符用*不全
print('{0:*>10}'.format('开始执行')) # 右对齐,不足10个字符用*不全
print('{0:*^10}'.format('开始执行')) # 居中,不足10个字符用*不全
'''
输出:
开始执行******
******开始执行
***开始执行***
'''
# 精度与进制转换
print('{salary:.3f}'.format(salary=123.456789)) # 保留3位小数
print('{0:b}'.format(123)) # 将123转换为2进制
print('{0:o}'.format(123)) # 将123转换为8进制
print('{0:x}'.format(123)) # 将123转换为16进制
print('{0:,}'.format(998998998998)) # 千分位显示
'''
输出
123.457
1111011
173
7b
998,998,998,998
'''
三、基本运算符 3.1 算术运算符 python支持的算数运算符与数学上计算的符号使用是一致的,我们以x=9,y=2为例来依次介绍它们
3.2 比较运算符 比较运算用来对两个值进行比较,返回的是布尔值True或False,我们以x=9,y=2为例来依次介绍它们
3.3 赋值运算符 python语法中除了有=号这种简单的赋值运算外,还支持增量赋值、链式赋值、交叉赋值、解压赋值,这些赋值运算符存在的意义都是为了让我们的代码看起来更加精简。我们以x=9,y=2为例先来介绍一下增量赋值
3.3.1 增量赋值
3.3.2 链式赋值 如果我们想把同一个值同时赋值给多个变量名,可以这么做
>>> z=10
>>> y=z
>>> x=y
>>> x,y,z
(10, 10, 10)
链式赋值指的是可以用一行代码搞定这件事
>>> x=y=z=10
>>> x,y,z
(10, 10, 10)
3.3.3 交叉赋值 我们定义两个变量m与n
如果我们想将m与n的值交换过来,可以这么做
>>> temp=m
>>> m=n
>>> n=temp
>>> m,n
(20, 10)
交叉赋值指的是一行代码可以搞定这件事
>>> m=10
>>> n=20
>>> m,n=n,m # 交叉赋值
>>> m,n
(20, 10)
3.3.4 解压赋值 如果我们想把列表中的多个值取出来依次赋值给多个变量名,可以这么做
>>> nums=[11,22,33,44,55]
>>>
>>> a=nums[0]
>>> b=nums[1]
>>> c=nums[2]
>>> d=nums[3]
>>> e=nums[4]
>>> a,b,c,d,e
(11, 22, 33, 44, 55)
解压赋值指的是一行代码可以搞定这件事
>>> a,b,c,d,e=nums # nums包含多个值,就好比一个压缩包,解压赋值因此得名
>>> a,b,c,d,e
(11, 22, 33, 44, 55)
注意,上述解压赋值,等号左边的变量名个数必须与右面包含值的个数相同,否则会报错
#1、变量名少了
>>> a,b=nums
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)
#2、变量名多了
>>> a,b,c,d,e,f=nums
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 6, got 5)
但如果我们只想取头尾的几个值,可以用*_匹配
>>> a,b,*_=nums
>>> a,b
(11, 22)
ps:字符串、字典、元组、集合类型都支持解压赋值
3.4 逻辑运算符 逻辑运算符用于连接多个条件,进行关联判断,会返回布尔值True或False
3.4.1 连续多个and 可以用and连接多个条件,会按照从左到右的顺序依次判断,一旦某一个条件为False,则无需再往右判断,可以立即判定最终结果就为False,只有在所有条件的结果都为True的情况下,最终结果才为True。
>>> 2 > 1 and 1 != 1 and True and 3 > 2 # 判断完第二个条件,就立即结束,得的最终结果为False
False
3.4.2 连续多个or 可以用or连接多个条件,会按照从左到右的顺序依次判断,一旦某一个条件为True,则无需再往右判断,可以立即判定最终结果就为True,只有在所有条件的结果都为False的情况下,最终结果才为False
>>> 2 > 1 or 1 != 1 or True or 3 > 2 # 判断完第一个条件,就立即结束,得的最终结果为True
True
3.4.3 优先级not>and>or #1、三者的优先级关系:not>and>or,同一优先级默认从左往右计算。
>>> 3>4 and 4>3 or 1==3 and 'x' == 'x' or 3 >3
False
#2、最好使用括号来区别优先级,其实意义与上面的一样
'''
原理为:
(1) not的优先级最高,就是把紧跟其后的那个条件结果取反,所以not与紧跟其后的条件不可分割
(2) 如果语句中全部是用and连接,或者全部用or连接,那么按照从左到右的顺序依次计算即可
(3) 如果语句中既有and也有or,那么先用括号把and的左右两个条件给括起来,然后再进行运算
'''
>>> (3>4 and 4>3) or (1==3 and 'x' == 'x') or 3 >3
False
#3、短路运算:逻辑运算的结果一旦可以确定,那么就以当前处计算到的值作为最终结果返回
>>> 10 and 0 or '' and 0 or 'abc' or 'egon' == 'dsb' and 333 or 10 > 4
我们用括号来明确一下优先级
>>> (10 and 0) or ('' and 0) or 'abc' or ('egon' == 'dsb' and 333) or 10 > 4
短路: 0 '' 'abc'
假 假 真
返回: 'abc'
#4、短路运算面试题:
>>> 1 or 3
1
>>> 1 and 3
3
>>> 0 and 2 and 1
0
>>> 0 and 2 or 1
1
>>> 0 and 2 or 1 or 4
1
>>> 0 or False and 1
False
3.5 成员运算符
注意:虽然下述两种判断可以达到相同的效果,但我们推荐使用第二种格式,因为not in语义更加明确
>>> not 'lili' in ['jack','tom','robin']
True
>>> 'lili' not in ['jack','tom','robin']
True
3.6 身份运算符
需要强调的是:==双等号比较的是value是否相等,而is比较的是id是否相等
#1. id相同,内存地址必定相同,意味着type和value必定相同
#2. value相同type肯定相同,但id可能不同,如下
>>> x='Info Tony:18'
>>> y='Info Tony:18'
>>> id(x),id(y) # x与y的id不同,但是二者的值相同
(4327422640, 4327422256)
>>> x == y # 等号比较的是value
True
>>> type(x),type(y) # 值相同type肯定相同
(<class 'str'>, <class 'str'>)
>>> x is y # is比较的是id,x与y的值相等但id可以不同
False