Python 常见面试题锦集 [持续更新]

函数

参数传递

def extendList(val, lst=[]):
lst.append(val)
return lst
lst1 = extendList(10)
lst2 = extendList(100, [])
lst3 = extendList('a')
print 'lst1: ',lst1
print 'lst2: ',lst2
print 'lst3: ',lst3

输出结果

lst1: [10, 'a']
lst2: [100]
lst3: [10, 'a']

lst的默认值是列表,函数定义时就创建了一个默认列表,函数调用在没有指定参数lst时,lst1, lst3都是使用同一个默认列表, 不会因为函数调用而重新初始化lst的默认值
lst2是通过传递一个空列表作为lst参数的默认值,所以值也就是[100]

可以通过以下例子来达到想要的效果

def extendList(val, lst=None):
if lst == None:
lst = []
lst.append(val)
return lst

再看下面这个例子

a = 1
def foo(a):
a = 2
foo(a)
print a # 1

b = []
def bar(b):
b.append(1)
bar(b)
print b # [1]
  1. 参数传递实质上是引用传递
  2. Python的对象分为可变对象(例如: list, dict, set)和不可变对象(例如:string, tuples)
  3. 变量其实就是对象的一个引用
    因此:
  • 当传递一个可变对象时,函数获得一个引用,这个引用指向了可变对象, 所以函数bar外的b跟函数内的b其实指向同一个对象, 在函数内b.append时,函数外b也会有影响。不过当参数重新赋值,也即重新指向一个对象(引用改变),那么函数内的变量和函数外的作用域的变量将没有任何关联,也就是在函数内对变量的操作不会影响到初始对象

    b = []
    def bar(b):
    b = []
    b.append(1)
    bar(b)
    print b # []
  • 当传递不可变对象时,函数内对参数的操作都不会影响到函数外的变量, 所以print a 为1

闭包的延迟绑定

首先说什么是闭包。闭包是一个表达式/函数,具有自由变量以及绑定这些变量的上下文环境,闭包允许你将一些行为封装,将它像一个对象一样传递,而且它依然能够访问到原来第一次声明时的上下文。下面是一个例子

def func():
base = 3
def closure(x):
return x ** base
return closure
closure = func()
print closure(4) # 64

外部函数func()内的closure()就是一个闭包,closure()在其内部引用了base变量,当func返回函数closure并被调用时,closure函数仍然能够访问到第一次声明时的上下文即base变量。
更多的关于闭包的内容可以->>戳这里

下面我们来看问题

def multipliers():
return [lambda x: i * x for i in range(4)]
print [m(2) for m in multipliers()]

这里的输出结果是 [6, 6, 6, 6],而不是我们预期的 [0, 2, 4, 6]。为什么会导致这样的结果呢?我们来看看发生了什么。
函数 multipliers() 这里定义了一个lambda表达式, 并返回了一个函数对象序列。从表面上看,函数中每一个lambda表达式的i都是不一样的。但是当实际输入x进行函数调用的时候,i的值都是一样的,都是最后一次i的值3。因为闭包的延迟绑定使得函数内部的执行内容是在函数调用时才确定的,而此时i值在产生序列时,最终被赋值为3,因此每一个lambda表达式上下文的i都是3。

下面是一些解决问题的方案
利用函数默认参数立即绑定特性,我们可以创建一个立即绑定参数的闭包

def multipliers():
return [lambda x, i=i: i * x for i in range(4)]
print [m(2) for m in multipliers()] # [0, 2, 4, 6]

利用偏函数

from functools import partial
def multipliers():
mul = lambda x, y: x * y
return [partial(mul, y=i) for i in range(4)]
print [m(2) for m in multipliers()] #[0, 2, 4, 6]

还有一种是利用Python生成器

def multipliers():
for i in range(4):
yield lambda x: x * i
print [m(2) for m in multipliers()] #[0, 2, 4, 6]

类变量

class Parent:
x = 1
class Child1(Parent): pass
class Child2(Parent): pass
print Parent.x, Child1.x, Child2.x # 1 1 1
Child1.x = 2
print Parent.x, Child1.x, Child2.x # 1 2 1
Parent.x = 3
print Parent.x, Child1.x, Child2.x # 3 2 3

为什么最后一行是 3 2 3 而不是 3 2 1 呢?为什么在改变了Parent.x的值同时也改变了Child2.x的值而不会影响到Child1.x的值?
回答这个问题的关键是Python中类变量在内部是以字典的形式进行传递,当在当前类没有找到相应的类变量时,会在父类中进行搜索直到最顶父类(没有找到则报错),这也就是为什么第一行是1 1 1
当子类覆写了值时,那么会在子类的内部字典创建一对新键值而不会影响到父类,所以第二行是 1 2 1
当更改父类的类变量时,这些改变将会影响到子类中还没进行覆写的类变量,所以第三行是 3 2 3
看下面的例子就会明白了

>>> class Parent(): x = 1
>>> class c1(Parent): pass
>>> class c2(Parent): pass
>>> vars(Parent)
{'x': 1, '__module__': '__console__', '__doc__': None}
>>> vars(c1)
{'__module__': '__console__', '__doc__': None}
>>> c1.x = 2
>>> vars(c1)
{'x': 2, '__module__': '__console__', '__doc__': None}
>>> vars(c2)
{'__module__': '__console__', '__doc__': None}
>>> Parent.x = 3
>>> vars(c1)
{'x': 2, '__module__': '__console__', '__doc__': None}
>>> vars(c2)
{'__module__': '__console__', '__doc__': None}

列表

列表的创建

lst = [[]] * 5
print lst # [[], [], [], [], []]
lst[1].append(10)
print lst # [[10], [10], [10], [10], [10]]
lst[2].append(20)
print lst # [[10, 20], [10, 20], [10, 20], [10, 20], [10, 20]]
lst.append(30)
print lst # [[10, 20], [10, 20], [10, 20], [10, 20], [10, 20], 30]

要理解 lst = [[]] * 5这一行做了什么,它不是创建了一个包含5个独立列表的列表,而是创建了一个包含对同一个列表五次引用的列表。也就是对列表中包含的任意列表的操作都会影响到其他列表, 也就是为什么第二三行是这样的结果

想要达到预期的效果可以这样做

lst = [[] for _ in range(5)]
lst[1].append(10)
print lst # [[], [10], [], [], []]

坚持原创技术分享