1、Python内存管理机制
Python的内存管理机制主要包括三个方面:引用计数机制、垃圾回收机制和内存池机制。
无序列表内容
Python是动态语言,对象与引用分离,一个变量使用不需要事先声明,而在赋值时,变量可以重新赋值为任意值。
a=1 #那么1是对象,a是1的引用。 print(id(a)) # 返回对象的内存地址140722768506624
Python会缓存整数和短小的字符,以便重复使用。当我们创建多个等于1的引用时,实际上是让所有这些引用都指向同一个对象。(可以使用is来判断是不是同一个对象)
# a和b指向同一个内存地址,is的结果是True a=1 b=1 print(a is b)
在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)。我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数。需要注意都是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时引用。因此,getrefcount()得到的结果会比期望的多1.
Python的一个容器对象(container),比如列表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,而是指向各个元素对象的引用。
Python中对象可以引用对象,对象的相互引用会造成引用环。
如何减少引用计数:
del删除某一个对象的引用。
如果某个引用指向对象A,当这个引用重新指向别的对象,引用次数减少。
某个引用离开它的作用域。
对象从容器对象中删除。
2、Python垃圾回收机制
- 从基本原理上,当Python中的某个对象的引用计数为0时,说明没有任何引用指向该对象,该对象就要成为被回收的垃圾。但是,垃圾回收时,Python不能进行其它任务。因此,频繁的垃圾回收会降低Python的工作效率。如果内存中的对象不多,就没有必要总启动垃圾回收。所以,Python只在特定的条件下,自动进行垃圾回收。
- 当Python运行时,会记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。当两者的差值高于某个阈值时,垃圾回收才会启动。
- 我们可以通过gc模块的get_threshold()方法,查看阈值:
import sys print(gc.get_threshold()) #(700, 10, 10)
- 返回(700,10,10),后面两个10是与分代回收相关的阈值。700则是启动垃圾回收的阈值。这些阈值可以通过gc中的set_threshold()方法重新设置。也可以利用gc.collect()手动启动垃圾回收。
- 分代回收:Python采用了分代回收的策略。这一策略的假设是:存活越久的对象,越不可能在后面的程序中变成垃圾。我们的程序往往会产生大量的对象,许多对象很快产生和消失,但也有一些对象长期被使用。出于信任和效率,对于这些“长寿”的对象,我们相信它们的用处,所以在垃圾回收中减少扫描它们的频率。Python将所有的对象分成0,1,2三代。所有的新建对象都是0代对象,当某一代对象经历过垃圾回收之后,仍然存活,那么它就被归入下一代对象。垃圾回收启动时,一定会扫描所有0代对象,0代对象在经过一定次数的扫描之后,就启动对0代和1代的扫描清理;1代对象在经过一定次数的扫描之后,就启动对0代、1代和2代的扫描清理。这两个次数就是上面get_threshold()方法返回的(700,10,10)中的后两个10,即每10次0代垃圾回收,会配合1次1代的垃圾回收;每10次1代垃圾回收,会配合1次2代垃圾回收。
- 孤立的引用环:引用环的存在会给垃圾回收带来很大困难。这些引用环可能造成无法使用,但引用计数不为0的一些对象。比如创建两个对象a和b,并引用对方,这就形成了引用环。利用del删除a和b引用之后,这两个对象就不能再从程序中调用。但是这两个对象的引用计数都没有降到0,不会被垃圾回收。
a=[] b=[] a.append(b) b.append(a) del a del b
- Python采用“标记-清除”回收这样的循环引用。Python复制每个对象的引用计数,可以记为gc_ref。Python会遍历所有对象i,对于每个对象i引用的对象j,将相应的gc_ref_j减1。即假设有两个对象a和b,我们从a出发,发现一个对b的引用,则将b的引用计数减1;再从b出发,发现一个对a的引用,则将a的引用计数减1。这样就可以将a和b的环摘掉,再利用垃圾回收将a和b回收。
3、赋值、浅拷贝与深拷贝
- 对象赋值
will = ["Will", 28, ["Python", "C#", "JavaScript"]] wilber = will print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber] will[0] = "Wilber" will[2].append("CSS") print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber]
- 上述代码的输出结果为:
根据上图分析代码:
首先,创建一个名为will的变量,这个变量指向一个list对象,从下图可以看到所有对象的地址。(每次运行,结果可能不同)
然后,通过will变量对willber变量进行赋值,那么willber变量将指向will变量的对应的对象(内存地址),也就是说“willber is will”, “willber[i] is will[i]”
- 可以理解为,Python中,对象的赋值都是进行对象引用(内存地址)的传递。
第三张图中,由于will和willber实际上指向是同一个对象,所以当修改will时,willber也会随之修改。
- 这里要注意,str为不可变类型,所以对其进行修改时,会替换旧的对象,所以内存地址会发生变化。
- 浅拷贝
import copy will = ["Will", 28, ["Python", "C#", "JavaScript"]] wilber = copy.copy(will) print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber] will[0] = "Wilber" will[2].append("CSS") print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber]
- 上述代码输出结果为:
根据上图分析代码:
首先,依然是一个will变量,指向一个list对象。
然后,通过copy模块中的浅拷贝函数copy(),对will指向的对象进行浅拷贝,然后将浅拷贝生成的新对象赋值给willber变量。
浅拷贝会创建一个新对象,所以这个例子中,“willber is not will”。
但是,对于对象中的元素,浅拷贝就只会使用原始元素的引用(内存地址),也就是“willber[i] is will[i]”。
当对will进行修改的时候
- 由于list第一个元素是str(不可变类型),所以will对应的list的第一个元素会使用一个新的对象,即内存地址发生变化。
- 但是list的第三个元素是一个可变类型,修改操作不会产生新的对象,所以will的修改结果会相应的出现在willber中。
在Python中,使用以下操作会产生浅拷贝的效果:
- 使用切片操作[:]。
- 使用工厂函数——list、dir、set等。
- 使用copy模块中的copy()函数。
深拷贝
import copy will = ["Will", 28, ["Python", "C#", "JavaScript"]] wilber = copy.deepcopy(will) print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber] will[0] = "Wilber" will[2].append("CSS") print id(will) print will print [id(ele) for ele in will] print id(wilber) print wilber print [id(ele) for ele in wilber]
- 上述代码的输出结果为:
根据上图分析代码:
首先,同样是使用一个will变量,指向一个list对象。
然后,通过copy模块中的深拷贝函数deepcopy(),对will指向的对象进行深拷贝,再将深拷贝生成的新对象赋值给willber。
与浅拷贝类似,深拷贝也是生成一个新的对象,即“willber is not will”。
但是,对于对象中的每个元素,深拷贝都会重新生成一份,而不是简单的使用原始元素的引用(内存地址)。如will的第三个元素指向39737304,而willber的第三个元素指向397773088,也就是“willber[i] is not will[i]”。
当对will进行修改时:
- 同样,由于list第一个元素为不可变类型,所以will对应的list的第一个元素会使用一个新的对象(内存地址)。
- 但是,list的第三个元素是一个可变类型,修改操作不会产生新的对象,但是由于“willber[i] is not will[i]”,所以willber不会被修改。
拷贝的特殊情况:
对于非容器类型——数字、字符串和其他原子类型的对象,没有拷贝这个说法。也就是说,对于这些对象,“obj is copy.copy(obj)”和“obj is copy.deepcopy(obj)”。
如果元组变量只包含原子类型的对象,则不能深拷贝,示例如下。
总结:
Python中对象的赋值都是进行对象引用(内存地址)传递。
使用copy.copy(),可以进行对象的浅拷贝,它复制了对象,但对于对象中的元素,依然使用原始的引用。
如果需要复制一个容器对象,以及它里面的所有元素(包含元素的子元素),可以使用copy.deepcopy()进行深拷贝。
对于非容器类型的对象,没有拷贝。
如果元组变量只包含原子类型对象,则不能深拷贝。
4、Python中is和==的区别?
- Python中的对象包含三个基本要素,分别是:id(身份标识)、type(数据类型)和value(值)。
- ==是Python标准操作符中的比较操作符,用来比较两个对象的value是否相等。如下面代码:
a = [1] b = [1] print(a == b)
- 输出:
True
而is是被称为同一性运算符,这个运算符比较判断的是对象间的唯一身份标识,也就是id是否相同。
通过以下几个例子对is和==的用法进行说明:
>>> x = y = [4,5,6] >>> z = [4,5,6] >>> x == y True >>> x == z True >>> x is y True >>> x is z False >>> print id(x) >>> 3075326752 >>> print id(y) >>> 3075326752 >>> print id(z) >>> 3075328756
- 可以发现,前三个例子都是True,这是因为x、y和z的值都是一样的;而最后一个例子是False,通过打印x、y和z的id号可以发现,x是通过y进行赋值得到的,所以x和y的id一致;而z是重新创建了一个对象引用,所以z的id与它们的不一致,所以x is z会返回False。
>>> a = 1 #a和b为数值类型 >>> b = 1 >>> a is b True >>> id(a) 14318944 >>> id(b) 14318944 >>> a = 'cheesezh' #a和b为字符串类型 >>> b = 'cheesezh' >>> a is b True >>> id(a) 42111872 >>> id(b) 42111872 >>> a = (1,2,3) #a和b为元组类型 >>> b = (1,2,3) >>> a is b False >>> id(a) 15001280 >>> id(b) 14586320 >>> a = [1,2,3] #a和b为list类型 >>> b = [1,2,3] >>> a is b False >>> id(a) 13985632 >>> id(b) 13982205 >>> a = {'cheese':1,'zh':2} #a和b为dict类型 >>> b = {'cheese':1,'zh':2} >>> a is b False >>> id(a) 42019863 >>> id(b) 42013698 >>> a = set([1,2,3])#a和b为set类型 >>> b = set([1,2,3]) >>> a is b False >>> id(a) 42101616 >>> id(b) 42093856
- 从上述代码可以发现,只有在数字、字符串类型的情况下,a is b才会返回True;当a和b是list、dict、tuple和set类型时,返回False。
- 造成这样结果的原因是:Python为了优化速度,使用了小整数对象池,避免为整数频繁的申请和销毁内存空间。Python对小整数的定义为[-5, 257),只有数字在-5到256之间时,它们的id才相等。同理,字符串也有一个类似的缓冲池,超过区间范围也就不会相等了。
5、Python中的迭代器与生成器
迭代器
迭代是Python最强大的功能之一,是访问集合元素(字符串、列表、元组、集合、字典)的一种方式。迭代器是一个可以记住遍历的位置的对象。
迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完。迭代器只能往前,不能后退。(迭代器可以使用for语句进行遍历)
迭代器有两个基本的方法:iter()和next()。
代码:
a =[1, 2, 3, 4] b = iter(a) print(type(b)) while True: try: print(next(b)) except: print('over') break
- 输出:
<class 'list_iterator'> 1 2 3 4 over
生成器
Python中,使用了yield的函数被称为生成器。
与普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更通俗的理解就是生成器即迭代器。在调用生成器运行的过程中,每次遇到yield时,函数会暂停并保存当前所有的运行信息,返回yield的值,并在下一次执行next()方法时,从当前位置继续运行。调用一个生成器函数,返回一个迭代器对象。
代码:
def helper(n): i = 0 while i<n: yield i i += 1 f = helper(5) while True: try: print(next(f)) except: print('over') break
- 输出:
0 1 2 3 4 over
6、Python中的GIL锁
在非Python环境中,单核情况下,同时只能有一个任务执行。多核时可以支持多个线程同时执行。但是在Python中,无论有多少核,同时只能执行一个线程,即Python中的多线程无法在多个核上同时进行。
Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock。任何Python线程执行前,必须先获得GIL锁,即可以将GIL锁看成一个“通行证”,并且在一个Python进程中,只有一个GIL锁。GIL只在CPython中有,因为CPython调用的是C语言的原生线程,所以它不能直接操作CPU,只能利用GIL保证同一时间只有一个线程拿到数据。
Python多线程的工作过程:
拿到公共数据
申请GIL
Python解释器调用操作系统原生线程
操作系统操作CPU执行运算
当该线程执行时间到后,无论运算是否已经执行完,GIL都被要求释放
由其他线程重复上述过程
等其他线程执行完后,又会切换到之前的线程,整个过程是每个线程执行自己的运算,当执行时间到就进行切换。
当然,Python虽然不能利用多线程实现多核任务,但是可以使用多进程。因为每个Python进程有各自独立的GIL,互不干扰,这样就可以实现并行。
7、Python中pass语句的作用
- pass实际上就是一个占位符,当写一个函数时,不确定在里面写什么的时候,使用pass来占位。
def foo(): pass
8、Python中的input函数
- Python3中,input函数用来接收用户输入,所有输入都是字符串类型。再根据程序需要,转换类型即可。
9、列表与元组的区别
- 列表中的元素是可以修改的;元组中的元素是不可以修改的。
- 列表使用[]声明;元组使用()声明。
10、Python中的三元运算符
Python中的三元运算符是对简单条件语句的简写。
- sentence1 if expression else sentence2
格式如上,当expression语句为True时,返回sentence1;反之,返回sentence2.
11、Python中的继承
当一个类继承自另一个类,它就被称为子类/派生类。被继承的类就被称为父类/基类。子类会继承/获取所有父类的成员和方法。
Python的继承有以下几种:
单继承——一个类继承自单个基类
多继承——一个类继承自多个基类
多级继承——一个类继承自单个基类,后者也继承自另一个基类
分层继承——多个类继承自单个基类
混合继承——两种或多种类型继承的混合
12、Python中的help()和dir()函数
- help()函数是一个内置函数,用于查看函数或模块用途的详细说明。
- dir()函数也是内置函数,dir()函数不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表。
13、当退出Python时,是否释放全部内存?
- 不会释放全部内存。因为具有对象循环引用和全局命名空间应用的变量,在Python退出时不会完全释放。
14、Python中的基本数据类型
- 不可变数据类型——数字、字符串、元组
- 可变数据类型——列表、字典、集合
15、Python中的闭包函数
Python是一种面向对象的语言,在Python中一切皆对象,这样就使得变量所拥有的属性,函数也拥有。这样就可以理解在函数内部创建一个函数的行为是完全合法的。这种函数被称为内嵌函数,这种函数只可以在外部函数的作用域内被正常调用,在外部函数的作用域之外调用会报错,如下图所示:
如果内部函数里引用了外部函数里定义的对象,那么此时内部函数被称为闭包函数。闭包函数所引用的外部定义的变量被叫做自由变量。闭包可以将自己的代码和作用域以及外部函数的作用结合在一起。
def count(): a = 1 b = 1 def sum(): c = 1 return a + c # a - 自由变量 return sum
- 总结——闭包函数就是(1)函数内部定义的函数;(2)引用了外部变量而非全局变量。
16、Python中装饰器的作用
Python装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象。
实质:是一个函数。
参数:待装饰的函数名。
返回值:装饰完的函数名。
作用:为已经存在的对象添加额外的功能。
不需要对对象做任何代码上的改动。
函数装饰器
- 以添加计时功能为例:
import time def decorator(func): def wrapper(*args, **kwargs): start_time = time.time() func() end_time = time.time() print(end_time - start_time) return wrapper @decorator def func(): time.sleep(0.8) func() # 函数调用 # 输出:0.800644397735595
- 在上述代码中,func是待修饰的函数,用装饰器显示func函数的运行时间。@decorator相当于执行func = decorator(func),为func函数装饰并返回。
- 在装饰器-decorator中,函数传入的参数是func(被修饰的函数),返回参数是内层函数。这里的内层函数wrapper,就相当于是闭包函数,它起到装饰给定函数的作用。
- 类方法的函数装饰器
import time def decorator(func): def wrapper(me_instance): start_time = time.time() func(me_instance) end_time = time.time() print(end_time - start_time) return wrapper class Method(object): @decorator def func(self): time.sleep(0.8) p1 = Method() p1.func() # 函数调用
- 在上述代码中,对于类方法来说,都会有一个默认的参数self。它实际表示的是类的一个实例,所以在装饰器的内部函数wrapper中也要传入一个参数me_instance,这个参数就表示将实例p1传给wrapper。
类装饰器
前面提到的都是让函数作为装饰器去修饰别的函数或者方法,类也同样可以作为装饰器。
class Decorator(object): def __init__(self, f): self.f = f def __call__(self): print("decorator start") self.f() print("decorator end") @Decorator def func(): print("func") func()
- 注意:call()是一个特殊方法,它可以将一个类实例变成一个可调用对象。要使用类装饰器,就必须实现类中的call方法,这样就相当于把实例变成了一个方法。
17、Python字典、集合的底层实现原理
字典和集合都是通过散列表来实现的。
字典的散列实现:
key必须是可hash的,所有不可变类型的数据类型都是可hash的。因此列表、元组和集合都是不可hash的,即它们不能作为key。
内存消耗巨大。因为字典使用了散列表,而散列表又必须是稀疏的,这使得字典在空间效率上非常低。
查询很快,O(1)。hash表用空间换时间。
key的排列顺序,取决于添加顺序。并且当字典添加新数据时,原有的排列可能会被打乱,这是因为Python要保证大概还有1/3的表元是空的,所以当数据添加到一定程度的时候,Python会将原有数据复制到一个更大的空间中去,这就会导致乱序。
集合的散列实现
集合的实现和字典一样,集合中的元素相当于字典中的key,只不过集合没有value。
集合中的元素必须是可散列的。
集合很消耗内存。
集合是无序的。
18、Python的解释器
解释器:
- 一边解释,一边运行。一段程序在解释器中运行可能会编译很多遍,因为每次运行程序时,都要重新编译,所以开销也比较大。
程序执行流程:
- 解释器读取一句源代码,先进行词法分析和语法分析;然后将源代码转换为解释器能运行的中间代码(字节码);最后,由解释器将中间代码解释为可执行的机器命令。
19、一个包里有三个模块,demo1.py、demo2.py和demo3.py,当使用from tools import *时,如何保证只有demo1和demo3被导入?
- 增加init.py文件,并在文件中增加all = ['demo1', 'demo3']。
20、Python中range和xrange的区别
range和xrange都在循环中使用,输出结果一样。
range返回的是一个list对象,xrange返回的是一个生成器对象。
xrange不会生成一个list,而是每次调用才返回一个值,节省内存空间。
注意:Python3.x中已经去掉了xrange,全部用range代替。
21、Python中*args和kwargs的含义
- 当不知道向参数传递多少参数时,如传递一个列表或元组,使用*args。
- 当不知道向函数传递多少关键字参数时,使用**kwargs来收集关键字参数。
22、Python中的lambda怎么实现a+b
sum = lambda a, b:a+b print(sum(1, 1)) # 输出2
23、Python中的单下划线与双下划线
单下划线开头
- 单下划线的命名方式常被用于模块中,以单下划线开头的变量叫做保护变量,即只有类对象和子类对象可以访问到这些变量。当使用from my_module import *的时候,单下划线开头的变量是不会被导入的。
单下划线结尾
- 以单下划线结尾的变量也存在,只不过是起到一个区别关键字的作用,如class关键字,可以定义一个变量名为class_的变量。
双下划线开头
- 双下划线开头的变量有实际的作用,这种变量只能被类对象访问,代表类的私有成员。
双下划线开头和结尾
- 双下划线开头和结尾代表的是一些Python的“魔术对象”,代表Python里特殊方法专用的标识。如init、call、name等。
如下面的代码示例:
#实现一个简单的类 class MyClass(): def __init__(self): self._semiprivate = "Hello" self.__superprivate = "world!" #创建一个实例 mc = MyClass() #查看实例mc的字典,里面存放了该实例的变量 print(mc.__dict__) #输出:{'_semiprivate': 'Hello', '_MyClass__superprivate': 'world!'} #可以看到Name mangling的效果了:'__superprivate'被修改为'_MyClass__superprivate' #所以调用mc.__superprivate是肯定要出错的~ print(mc._semiprivate) #输出:Hello print(mc._MyClass__superprivate) #输出:world! print(mc.__superprivate) #报错:AttributeError: 'MyClass' object has no attribute '__superprivate'
24、
- 无序列表内容
- 无序列表内容