Python基础---面试题汇总
前言
本文只涉及Python相关的面试题,面向中高级Python开发,太基本的题目不收录。
更希望通过代码演示,原理探究等来深入讲解某一知识点,做到融会贯通。
另外部分演示代码有兴趣的可以找我拿。
语言基础篇
1、Python的基本数据类型
Python3 中有六个标准的数据类型:
-
Number(数字)(包括整型、浮点型、复数、布尔型等)
-
String(字符串)
-
List(列表)
-
Tuple(元组)
-
Set(集合)
-
Dictionary(字典)
Python3 的六个标准数据类型中:
-
不可变数据(3 个):Number(数字)、String(字符串)、Tuple(元组);
-
可变数据(3 个):List(列表)、Dictionary(字典)、Set(集合)。
2、Python是静态还是动态类型?是强类型还是弱类型?
-
动态强类型语言(不少人误以为是弱类型)
-
动态还是静态指的是编译器还是运行期确定类型
-
强类型指的是不会发生隐式类型转换
js就是典型的弱类型语言,例如在console下面模拟一下数字和字符串相加,会发现发生了类型转换。
而Python会报TypeError
3、什么是鸭子类型
“当一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
鸭子类型关注的是对象的行为,而不是类型。比如file,StringIO,socket对象都支持read/write方法,再比如定义了__iter__魔术方法的对象可以用for迭代。
下面用一个例子来模拟鸭子的类型:
pythonclass Duck: def say(self): print("嘎嘎") class Dog: def say(self): print("汪汪") def speak(duck): duck.say() duck = Duck() dog = Dog() speak(duck) # 嘎嘎 speak(dog) # 汪汪
4、什么是自省
自省是运行时判断一个对象类型的能力。
python一切皆对象,用type, id, isinstance获取对象类型信息。
自省,也可以说是反射,自省在计算机编程中通常指这种能力:检查某些事物以确定它是什么、它知道什么以及它能做什么。
与其相关的主要方法:
-
hasattr(object, name)检查对象是否具体 name 属性。返回 bool.
-
getattr(object, name, default)获取对象的name属性。
-
setattr(object, name, default)给对象设置name属性
-
delattr(object, name)给对象删除name属性
-
dir([object])获取对象大部分的属性
-
isinstance(name, object)检查name是不是object对象
-
type(object)查看对象的类型
-
callable(object)判断对象是否是可调用对象
5、python3和python2的对比
-
print成为函数
-
编码问题。python3不再有unicode对象,默认str就是unicode
-
除法变化。python3除号返回浮点数,如果要返回整数,应使用//
-
类型注解。帮助IDE实现类型检查
-
优化的super()方便直接调用父类函数。Python3.x 和 Python2.x 的一个区别是: Python 3 可以使用直接使用 super().xxx 代替 super(Class, self).xxx :
-
高级解包操作。a, b, *rest = range(10)
-
keyword only arguments。限定关键字参数
-
chained exceptions。python3重新抛出异常不会丢失栈信息
-
一切返回迭代器。range, zip, map, dict.values, etc. are all iterators
-
性能优化等。。。
6、python如何传递参数
python官方文档上的话:
“Remember that arguments are passed by assignment in Python. Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and so no call-by-reference per Se.”
准确地说,Python 的参数传递是赋值传递 (pass by assignment),或者叫作对象的引用传递(pass by object reference)。Python 里所有的数据类型都是对象,所以参数传递时,只是让新变量与原变量指向相同的对象而已,并不存在值传递或是引用传递一说。
根据对象的引用来传递,根据对象是可变对象还是不可变对象,得到两种不同的结果。如果是可变对象,则直接修改。如果是不可变对象,则生产新对象,让形参指向新对象
可以具体结合下面的代码实例来模拟:
pythondef flist(l): l.append(0) print(id(l)) # 每次打印的id相同 print(l) ll = [] print(id(ll)) flist(ll) # [0] flist(ll) # [0,0] print("=" * 10) def fstr(s): print(id(s)) # 和入参ss的id相同 s += "a" print(id(s)) # 和入参ss的id不同,每次打印结果不相同 print(s) ss = "sun" print(id(ss)) fstr(ss) # a fstr(ss) # a
7、python的可变/不可变对象
不可变对象: bool/int/float/tuple/str/frozenset 可变对象:list/set/dict
这里继续看两个代码例子,看下输出的是什么
pythondef clear_list(l): l = [] ll = [1,2,3] clear_list(ll) print(ll) def fl(l=[1]): l.append(1) print(l) fl() fl()
答案是
[1,2,3] [1] [1,1]
对于第一题,l = []这一步,创建了一个新的对象,并将l贴上去(注意函数里面的l和外面的l是形参和实参的区别,不要以为是同一个),所以原来的 l 并没有改变
对于第二题,默认参数只计算一次。
有兴趣的小伙伴可以再试一下这个例子:
pythona = 1 def fun(a): print("func_in",id(a)) a = 2 print("re-point",id(a), id(2)) print("func_out",id(a), id(1)) fun(a)
答案是:
func_out 2602672810288 2602672810288 func_in 2602672810288 re-point 2602672810320 2602672810320
关于Python的参数传递,可变/不可变对象,再推荐一个stackoverflow上面的回答。
Arguments are passed by assignment. The rationale behind this is twofold: the parameter passed in is actually a reference to an object (but the reference is passed by value)some data types are mutable, but others aren't So: If you pass a mutable object into a method, the method gets a reference to that same object and you can mutate it to your heart's delight, but if you rebind the reference in the method, the outer scope will know nothing about it, and after you're done, the outer reference will still point at the original object. If you pass an immutable object to a method, you still can't rebind the outer reference, and you can't even mutate the object.
8、Python中的 *args 和 **kwargs
用来处理可变参数,*args被打包成tuple,**kwargs被打包成dict
我们看一些代码例子:
pythondef print_multiple_args(*args): print(type(args), args) for idx, val in enumerate(args): # enumerate()枚举函数 print(idx, val) print_multiple_args('a', 'b', 'c') # 通过将列表前加*打包成关键字参数,指明了接收值参数必须是*args print_multiple_args(*['a', 'b', 'c']) def print_kwargs(**kwargs): print(type(kwargs), kwargs) for k, v in kwargs.items(): print('{}: {}'.format(k, v)) print_kwargs(a=1, b=2) # 给字典前加**打包成关键字参数,指明接收值的参数必须是**kwargs print_kwargs(**dict(a=1, b=2)) def print_all(a, *args, **kwargs): print(a) if args: print(args) if kwargs: print(kwargs) print_all('hello', 'world', name='monki')
输出为:
<class 'tuple'> ('a', 'b', 'c') 0 a 1 b 2 c <class 'tuple'> ('a', 'b', 'c') 0 a 1 b 2 c <class 'dict'> {'a': 1, 'b': 2} a: 1 b: 2 <class 'dict'> {'a': 1, 'b': 2} a: 1 b: 2 hello ('world',) {'name': 'monki'}
9、python异常机制
可参考Python官方文档上的异常层级分类
docs.python.org/zh-cn/3/lib…
python异常代码块示例:
pythontry: # func # 可能会抛出异常的代码 except (Exception1, Exception2) as e: # 可以捕获多个异常并处理 # 异常处理的代码 else: # pass # 异常没有发生的时候代码逻辑 finally: pass # 无论异常有没有发生都会执行的代码,一般处理资源的关闭和释放
10、什么是Python中的GIL?
全局解释器锁 GIL,英文名称为 Global Interpreter Lock,它是解释器中一种线程同步的方式。
对于每一个解释器进程都具有一个 GIL ,它的直接作用是限制单个解释器进程中多线程的并行执行,使得即使在多核处理器上对于单个解释器进程来说,在同一时刻运行的线程仅限一个。 对于 Python 来讲,GIL 并不是它语言本身的特性,而是 CPython 解释器的实现特性。
Python 代码被编译后的字节码会在解释器中执行,在执行过程中,存在于 CPython 解释器中的 GIL 会致使在同一时刻只有一个线程可以执行字节码。 GIL 的存在引起的最直接的问题便是:在一个解释器进程中通过多线程的方式无法利用多核处理器来实现真正的并行。
因此,Python的多线程是伪多线程,无法利用多核资源,同一个时刻只有一个线程在真正的运行。
GIL的限制了程序的多核执行
-
同一个时间只能有一个线程执行字节码
-
CPU密集程序难以利用多核优势
-
IO期间会释放GIL,对IO密集程序影响不大
面对GIL的存在,我们有可以有多个方法帮助我们提升性能
-
在 IO 密集型任务下,我们可以使用多线程或者协程来完成。
-
可以选择更换 Jython 等没有 GIL 的解释器,但并不推荐更换解释器,因为会错过众多 C 语言模块中的有用特性。
-
CPU密集可以使用多进程+进程池。
-
将计算密集型任务转移到 Python 的 C / C++ 扩展模块中完成。
11、为什么有了GIL还要关注线程安全?
GIL 保证的是每一条字节码在执行过程中的独占性,即每一条字节码的执行都是原子性的。GIL 具有释放机制,所以 GIL 并不会保证字节码在执行过程中线程不会进行切换,即在多个字节码之间,线程具有切换的可能性。
我们可以用python的dis模块去查看a += 1执行的字节码,发现需要有多个字节码去完成,线程具有切换的可能性,所以它是非线程安全的。
一个操作如果是一个字节码指令可以完成就是原子的,非原子操作不是线程安全的,原子的是可以保证线程安全的。
GIL 和线程互斥锁的粒度是不同的,GIL 是 Python 解释器级别的互斥,保证的是解释器级别共享资源的一致性,而线程互斥锁则是代码级(或用户级)的互斥,保证的是 Python 程序级别共享数据的一致性,所以我们仍需要线程互斥锁及其他线程同步方式来保证数据一致。
具体关于Python的GIL的介绍,可参考我的另一篇文章《详解Python中的GIL》
12、什么是迭代器和生成器?
这张图比较精彩,把各种概念都总结了。
容器(container)
container 可以理解为把多个元素组织在一起的数据结构,container 中的元素可以逐个地迭代获取,可以用 in, not in 关键字判断元素是否包含在容器中。比如Python中常见的container对象有list,deque,set
可迭代对象(iterables)
大部分的 container 都是可迭代对象,比如 list or set 都是可迭代对象,可以说只要是可以返回一个迭代器的都可以称作可迭代对象。
迭代器(iterator)
python中的容器有许多,比如列表、元组、字典、集合等,对于容器,可以很直观地想象成多个元素在一起的单元,所有的容器都是可迭代的(iterable)。
我们通常使用for in 语句对可迭代的对象进行枚举,其底层机制在于:
而可迭代对象,通过 iter() 函数返回一个迭代器(iterator),迭代器提供了一个 next 的方法。调用用这个方法后,你要么得到这个容器的下一个对象,要么得到一个StopIteration 的错误。
举个例子:
python>>> x = [1, 2, 3] >>> # Get the iterator >>> y = iter(items) # Invokes items.__iter__() >>> # Run the iterator >>> next(y) # Invokes it.__next__() 1 >>> next(y) 2 >>> next(y) 3 >>> type(x) <class 'list'> >>> type(y) <class 'list_iterator'> >>> next(y) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>>
上面的例子中,x=[1,2,3]是可迭代对象,这里也叫容器。y=iter(x)则是迭代器,且实现了__iter__和__next__方法。
它们之间的关系如下图所示:
可见通过 iter 方法后就是迭代器。它是一个带状态的对象,调用 next 方法的时候返回容器中的下一个值,可以说任何实现了iter和 next 方法的对象都是迭代器,iter返回迭代器自身,next 返回容器中的下一个值,如果容器中没有更多元素了,则抛异常。
迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。
生成器(generator)
生成器(generator)可以简单理解为懒人版本的迭代器。
它相比于迭代器的优势是,生成器并不会像迭代器一样占用大量内存。比如声明一个迭代器:[i for i in range(100000000)]就可以声明一个包含一亿个元素的列表,每个元素在生成后都会保存到内存中。但实际上我们也许并不需要保存那么多东西,只希望在你用 next() 函数的时候,才会生成下一个变量,因此生成器应运而生,在python中的写法为(i for i in range(100000000))
此外,生成器还可以有别的形式,比如生成器函数,通过yield关键字,把结果返回到next()方法中,举个例子:
pythondef frange(start, stop, increment): x = start while x < stop: yield x x += increment for n in frange(0, 2, 0.5): print(n) 0 0.5 1.0 1.5
相比于迭代器,生成器具有以下优点:
-
减少内存
-
延迟计算
-
有效提高代码可读性
我曾经对生成器和迭代器有过总结:《Python中的迭代器和生成器》
stackoverflow上有个关于yield的高赞回答:
stackoverflow.com/questions/2…
13、什么是协程?
内容较多,具体可以看我的这篇文章。《详解Python协程》
14、什么是闭包?
在函数内部再定义一个函数,并且这个函数用到了外边函数的变量,那么将这个函数以及用到的一些变量称之为闭包。
简单的说,如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。来看几个简单的例子:
最简单的例子,实现加法
pythondef addx(x): def adder(y): return x + y return adder c = addx(8) print(type(c)) print(c.__name__) print(c(10)) 复制代码 <class 'function'> adder 18
利用闭包实现斐波那契数列
pythonfrom functools import wraps def cache(func): store = {} @wraps(func) def _(n): if n in store: return store[n] else: res = func(n) store[n] = res return res return _ @cache def f(n): if n <= 1: return 1 return f(n-1) + f(n-2) print(f(10))