Python入门到放弃
python3笔记
Python基础
变量、基本数据类型、运算符
变量赋值
通过“=”实现
变量命名规则
1.一般由字母、数字、下划线组成
2.不能以数字开头
3.不能使用系统保留关键字system,type,print ,del.....
4.区分大小写
基本数据类型
Numbers(数字)、
String(字符串)
List(列表)、
Tuple(元组)
Sets(集合)、
Dictionaries(字典)
运算符
#算术运算符+ - * / % ** //
#比较运算符==; !=; <>; >;<;>=;<=
#赋值运算符=; +=; -=;*=; /=; %=; **= ; //=
基本数据类型
列表(List)
列表基础知识
#列表的特点支持异构:一个列表中含不同类型的对象支持嵌套:即列表中的元素可以是列表可变:支持通过索引,切片,进行修改
#如何定义列表方法一(推荐):[];方法二:使用内置函数list() 内建函数list(a_sequence) 可以将一个序列转为列表
#访问列表中的值list1[0] 、 list2[1:5]#更新列表list[2] = 2001; #列表的索引和切片classmates[1] 、 classmates[3] ;[1:-2],[1:3];[::2]
列表相关方法
#append(): 向列表中追加元素list.append('test')
#extend(): 追加可迭代对象中所有元素list1.extend(list2)
#insert():在指定索引(下标从0开始)位置前插入list1.insert(2,'louis')
#pop():按照索引弹出元素,返回值为该元素。默认弹出最后一个元素a = list1.pop()
#remove():按照元素value值移除第一个匹配项list1.remove('cheng')
#count():统计指定值出现的次数list1.count('louis')
#index():某个值第一次出现的位置list1.index('cheng')
#reverse():反向存放list1.reverse()
#查询遍历for i in list1: print(i)for i, k in enumerate(list1): print(i,k)
#sort():排序list1.sort()
#list.clear(): 移除列表中的所有项,等于del a[:]list1.clear() --> []
#list.copy(): 返回列表的浅复制,等于a[:]浅拷贝:list.copy()和list[:] ---> id(list1),id(list2) 不一样深拷贝:list1 = list2 ---> id(list1),id(list2) 不样
#len(): 返回列表的长度len(list1)
#max() 最大值 、min() 最小值max(list1) 、 min(list1)
列表生成式
#Python内置的一种极其强大的生成list的表达式基于已有列表生成新列表
#语法结构格式:list6 = [x * x for x in range(1, 11)]把要生成的元素 x*x 放到前面后面跟上for循环,for后面还可以加上if判断
#1到10之间的偶数平方再乘以2 作为新列表元素list1 = [i ** 2 for i in range(1, 11) if i % 2 == 0]
#1到10之间的偶数 ,作为新列表元素list2 = [i for i in range(1, 11) if i % 2 == 0]
#特点:一次性生成新列表,如果列表数据过大,会消耗大量资源
字符串(String)
索引和切片
#索引s[0]从左向右:索引从0开始 str1[0:2]从右向左:索引从-1开始
#切片str1[:3] 从第一个字符开始截取str1[0:] str1[:] 从第一个字符开始截取,一直截取到最后
#扩展切片:指定步长s[0:9:2] 步长为2取其中5个字符
#格式化输出格式print("字符串 %format1 %format2… " %(变量或值1,变量或值2...))print("My name is %s and weight is %d kg!" % ('Zara', 21))%d 十进制整数%f 浮点数%s 字符串%% 用于输出符号%
强制类型转化
int():强制转换为整型注意:只能将由数字组成的字符串转换为整型test1 = int(str1)
float():强制转换为浮点型例如
str():强制转换为字符串例如
字符串常用方法
#s.find(t):查找子串t,找到返回首次出现位置的索引值,找不到则返回-1str1.find('c')
#s.split(sep):使用sep作为分割符,列表返回结果str=”www.baidu.com”str.split('.')
#s.join(iter) : 使用s作为连接符连接序列iter中所有元素返回连接后的字符串'.'.join(list2)
#s.replace(old,new):替换str1.replace('wang','louis')
#s. strip(t): 删掉开头和结尾的字符t(没有参数t则删除空白符)str1.strip('w')
#s.isalpha()/s.isdigit():返回 True,False /是否为字母/是否为数字
#s.upper()/ s.lower():所有字母转化为大写字母/小写字母,
#s.count(t) 统计t出现的次数
字典(Dict)
字典的特点
无序: 通过键实现元素存取,故无序
可变:可以通过键进行改变值
异构:支持多种数据类型
嵌套:可以在键值对中的值部分出现字典#定义字典1.方法一:{key1:value1,key2:value2,...}键与值用冒号“:”分开; 项与项用逗号“,”分开; 字典中的键必须是唯一的,而值可以不唯一dict = {'Alice': '16', ' MadKing ': '17', 'Cecil': '18'}2.方法二:dict()内置函数:指定键值d2=dict(name="jerry",age="45",gender="m")
#访问字典里的值d2['name']#修改字典更新已经存在的dict['Age'] = 8不存在则添加 dict['School'] = "DPS School"#删除字典元素del dict['Name']; # 删除键是'Name'的条目dict.clear(); # 清空词典所有条目 ,清空内容,结果为:{}del dict ; # 删除词典,是指删除这个字典注意:删除元素尽量用字典内置方法,不需要直接del#字典键的特性1.字典值 可以没有限制地取任何python对象,既可以是标准的对象,也可以是用户定义的2.同一个键 不允许出现两次。创建时如果同一个键被赋值两次,后一个值会被记住3.键必须不可变,所以可以用数,字符串或元组充当,所以用列表就不行4.Python不支持dict的key为list或dict类型,因为list和dict类型是unhashable(不可哈希)的{['Name']: 'Zara', 'Age': 7}
字典相关方法
#添加元素:dict1[key] = value#items():变量解包:即将字典转化为键、值作为元组的列表 dict1.items()dict_items([('Alice', '16'), (' MadKing ', '17'), ('Cecil', '18')])#keys();键列表 dict1.keys()dict_keys(['Alice', ' MadKing ', 'Cecil'])#values():值列表 dict1.values()dict_values(['16', '17', '18'])#len():取得字典中键值对个数len(dict1)#update():在原字典上合并字典,键相同时会被覆盖dict1.update(dict2)#get():取得某个键对应的值dict1.get('Alice')#pop():弹出指定键对应的值dict1.pop('Alice')#popitem():随机弹出键值对dict1.popitem()#clear():清除所有元素dict1.clear() ---> {}#str(dict): 输出字典以可打印的字符串表示str(dict1)#dict1.copy() 返回一个字典的浅复制#dict1.fromkeys(seq,value)创建一个新字典,以序列seq中元素做字典的键,val为字典所有键对应的初始值seq = ('name', 'age', 'sex')dict = dict.fromkeys(seq) --> {'name': None, 'age': None, 'sex': None}dict = dict.fromkeys(seq, 10) ---> {'name': 10, 'age': 10, 'sex': 10}#setdefault() 方法和get()方法类似, 如果键不已经存在于字典中,将会添加键并将值设为默认值dict.setdefault(key, default=None)dict = {'Name': 'W3CSchool', 'Age': 7}dict.setdefault('Age', None)dict.setdefault('Sex', None) ---> {'Age': 7, 'Name': 'W3CSchool', 'Sex': None}
字典的遍历
字典的遍历for key, value in dict1.items():print(key, value)for key in dict1:print(key, dict1[key])
元组(Tuple)
元组的特点
#元组的特点元组与列表类似,也是一种容器类型,不同之处在于元组的元素不能修改元组使用小括号,列表使用方括号支持异构 、 支持嵌套 、 不可变元组使用小括号,列表使用方括号元组与字符串类似,下标索引从0开始,可以进行截取,组合等。
#元组创建在括号中添加元素,并使用逗号隔开即可tup1 = ('physics', 'chemistry', 1997, 2000);tup2 = (1, 2, 3, 4, 5 );tup3 = "a", "b", "c", "d";创建空元组tup1 =();元组中只包含一个元素时,需要在元素后面添加逗号tup1 =(50,);
#访问元组使用下标索引tup1[0]tup2[1:5]
#修改元组元组中的元素值是不允许修改的,但我们可以对元组进行连接组合修改元组元素操作是非法的tup1[0] = 100; --报错tup1 = (12, 34.56);tup2 = ('abc', 'xyz');tup3 = tup1 + tup2;
#删除元组元组中的元素值是不允许删除的,但我们可以使用del语句来删除整个元组tup = ('physics', 'chemistry', 1997, 2000);del tup;
#元组运算符与字符串一样,元组之间可以使用 + 号和 * 号进行运算元祖 可以组合和复制,运算后会生成一个新的元组计算元素个数 len((1, 2, 3)) --> 3连接元祖(1, 2, 3) + (4, 5, 6) ---->(1, 2, 3, 4, 5, 6)复制元祖(12, 34.56)*4判断 元素是否存在3 in (1, 2, 3) -----> True迭代for i in tup1:print(i)
#元组索引,截取元组也是一个序列可以访问元组中的指定位置的元素也可以截取索引中的一段元素L =('spam', 'Spam', 'SPAM!')读取第三个元素 L[2]反向读取;读取倒数第二个元素 L[-2]截取元素 L[1:]
#无关闭分隔符任意对象 以逗号隔开,默认为元组'abc', -4.24e93, 18+6.6j, 'xyz';
元组内置函数
len(t) 计算元组元素个数
max(t) 返回元组中元素最大值
min(t) 返回元组中元素最小值
tuple(list) 将列表转换为元组
index():返回指定元素第一次出现的索引位置tuple.index('wang')
count():指定元素出现的次数tuple.count('wang')
数字(Numbers)
数字类型的特点
数值类型是不允许改变的,如果改变数字数据类型的值,将重新分配内存空间#三种不同的数值类型1.整型(Int)通常被称为是整型或整数,是正或负整数,不带小数点。Python3 整型是没有限制大小的,可以当作 Long 类型使用,所以 Python3 没有 Python2 的 Long 类型可以使用十六进制和八进制来代表整数number = 0xA0F # 十六进制number=0o37 # 八进制2.浮点型(float) 浮点型由整数部分与小数部分组成,浮点型也可以使用科学计数法表示(2.5e2 = 2.5 x 102 = 250)3.复数 (complex)复数由实数部分和虚数部分构成,可以用a + bj,或者complex(a,b)表示, 复数的实部a和虚部b都是浮点型#数字类型转换int(x) 将x转换为一个整数。float(x) 将x转换到一个浮点数。complex(x) 将x转换到一个复数,实数部分为 x,虚数部分为 0。complex(x, y) 将 x 和 y 转换到一个复数,实数部分为 x,虚数部分为 y。x 和 y 是数字表达式
#数字运算+, -, * 和 /
随机数函数
#choice(seq) 从序列的元素中随机挑选一个元素,比如random.choice(range(10)),从0到9中随机挑选一个整数。
#randrange ([start,] stop [,step]) 从指定范围内,按指定基数递增的集合中获取一个随机数,基数默认值为 1
#random() 随机生成下一个实数,它在[0,1)范围内。
#seed([x]) 改变随机数生成器的种子seed。如果你不了解其原理,你不必特别去设定seed,Python会帮你选择seed。
#shuffle(lst) 将序列的所有元素随机排序
#uniform(x, y) 随机生成下一个实数,它在[x,y]范围内。
三角函数
#acos(x) 返回x的反余弦弧度值。
#asin(x) 返回x的反正弦弧度值。
#atan(x) 返回x的反正切弧度值。
#atan2(y, x) 返回给定的 X 及 Y 坐标值的反正切值。
#cos(x) 返回x的弧度的余弦值。
#hypot(x, y) 返回欧几里德范数 sqrt(x*x + y*y)。
#sin(x) 返回的x弧度的正弦值。
#tan(x) 返回x弧度的正切值。
#degrees(x) 将弧度转换为角度,如degrees(math.pi/2) , 返回90.0
#radians(x) 将角度转换为弧度#数学常量pi 数学常量 pi(圆周率,一般以π来表示)e 数学常量 e,e即自然常数(自然常数)。
集合(Set)
#集合的定义集合(set)是一个无序不重复元素的集基本功能是 进行成员关系测试和消除重复元素
#创建集合可以使用大括号 {} 或者 set() 函数# 重复的元素被自动去掉
student = {'Tom', 'Jim', 'Mary', 'Tom', 'Jack', 'Rose'}创建一个空集合必须用 set() 而不是 { },因为{ }是用来创建一个空字典
#set可以进行集合运算a = set('abracadabra') -->{'a', 'b', 'r', 'd', 'c'}b = set('alacazam') --> {'l', 'a', 'z', 'c', 'm'}a - b # a和b的差集{'b', 'd', 'r'}a | b # a和b的并集{'l', 'm', 'a', 'b', 'c', 'd', 'z', 'r'}a & b # a和b的交集{'a', 'c'}a ^ b # a和b中不同时存在的元素{'l', 'm', 'b', 'd', 'z', 'r'}
循环条件
while 循环
while 循环在给定的判断条件为 true 时执行循环体,否则退出循环体计算 1 到 100 的总和sum = 0
i = 1
while i <= 100:sum += ii += 1
print(sum)
for 循环
for 循环可以遍历任何序列的项目,如一个列表或者一个字符串languages = ["C", "C++", "Perl", "Python"]
for x in languages:print(x)
for循环输出99乘法表
break 语句
break 语句 在语句块执行过程中终止循环,并且跳出整个循环edibles = ["ham", "spam", "eggs", "nuts"]
for food in edibles:if food == "spam":print("No more spam please!")break # 在语句块执行过程中终止循环,并且跳出整个循环print("Great, delicious " + food)
else:print("I am so glad: No spam!") # break后,和for并列的else也不会执行
print("Finally, I finished stuffing myself")#break后,print 会执行i = 1
while 1: # 循环条件为1必定成立print(i) # 输出1~10i += 1if i > 10: # 当i大于10时跳出循环break
continue 语句
continue 语句在语句块执行过程中终止当前循环,跳出该次循环,执行下一次循环i = 1
while i < 10:i += 1if i % 2 > 0: # 非双数时 跳过输出(即跳出while循环)continueprint(i) # 输出双数2、4、6、8、10
range()函数
range()函数#需要遍历数字序列,可以使用内置 range() 函数。它会生成数列for i in range(5):print(i)for i in range(5,9) :print(i)for i in range(0, 10, 3) :print(i)#创建一个列表 list(range(5))range() 和 len() 函数以遍历一个序列的索引,a = ['Mary', 'had', 'a', 'little', 'lamb']for i in range(len(a)):print(i, a[i])从一个列表中每次取出两个,找出所有组合方式
return 语句
return 语句退出函数,选择性地向调用方返回一个表达式。不带参数值的 return 语句返回 None
pass 语句
pass 语句pass是空语句,是为了保持程序结构的完整性
函数
定义函数
函数代码块以 def 关键词开头,后接函数标识符名称和圆括号 ()。任何传入参数和自变量必须放在圆括号中间。圆括号之间可以用于定义参数。函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。函数内容以冒号起始,并且缩进。Return[expression] 结束函数,选择性地返回一个值给调用方。不带表达式的 return 相当于返回 None。
参数传递
#1.在 Python 中,类型属于对象,变量是没有类型的a=[1,2,3]a="w3cschool"[1,2,3] 是 list 类型,"w3cschool"是 string 类型,而变量 a 是没有类型,它仅仅是一个对象的引用(一个指针), 可以是 list 类型对象,也可以指向 是 string 类型对象#2.可更改 (mutable) 与不可更改 (immutable) 对象strings, tuples, 和 numbers 是不可更改的对象,list, dict 等则是 可以修改的对象.【不可变类型】:变量赋值 a=5 后再赋值 a=10,这里实际是新生成一个 int 值对象 10,再让 a 指向它,而 5 被丢弃,不是改变 a 的值,相当于新生成了 a.【可变类型】:变量赋值 la=[1,2,3,4] 后再赋值 la[2]=5 则是将 list la 的第三个元素值更改,本身 la 没有动,只是其内部的一部分值被修改了.#3.Python 函数的参数传递【不可变类型】:类似 c++的值传递,如 整数、字符串、元组。如 fun(a),传递的只是 a 的值,没有影响 a 对象本身。比如在 fun(a)内部修改 a 的值,只是修改另一个复制的对象,不会影响 a 本身。【可变类型】:类似 c++的引用传递,如 列表,字典。如 fun(la),则是将 la 真正的传过去,修改后 fun 外部的 la 也会受影响。Python 中一切都是对象,严格意义我们不能说值传递还是引用传递,我们应该说传不可变对象和传可变对象。#4.Python 传不可变对象实例def ChangeInt(a):a = 10b = 2ChangeInt(b)print(b) # 结果是 2实例中有 int 对象 2,指向它的变量是 b,在传递给 ChangeInt 函数时,按传值的方式复制了变量 b,a 和 b 都指向了同一个 int 对象,在 a=10 时,则新生成一个 int 值对象 10,并让 a 指向它 #5.按值传递参数和按引用传递参数(传可变对象实例)所有参数(自变量)在 Python 里都是按引用传递。如果你在函数里修改了参数,那么在调用这个函数的函数里,原始的参数也被改变了def changeme(mylist):"修改传入的列表"mylist.append([1, 2, 3, 4])print("函数内取值: ", mylist) #结果:[10, 20, 30, [1, 2, 3, 4]]return# 调用changeme函数mylist = [10, 20, 30]changeme(mylist)print("函数外取值: ", mylist)#结果:[10, 20, 30, [1, 2, 3, 4]]传入函数的和在末尾添加新内容的对象用的是同一个引用
参数类型
#必备参数以正确的顺序传入函数。调用时的数量必须和声明时的一样#关键字参数使用关键字参数允许函数调用时参数的顺序与声明时不一致,因为 Python 解释器能够用参数名匹配参数值#命名参数调用方用参数的命名确定传入的参数值。你可以跳过不传的参数或者乱序传参,因为 Python 解释器能够用参数名匹配参数值#缺省参数调用函数时,缺省参数的值如果没有传入,则被认为是默认值#不定长参数可能需要一个函数能处理比当初声明时更多的参数加了星号(*)的变量名会存放所有未命名的变量参数。选择不多传参数也可# 可写函数说明def printinfo(arg1, *vartuple):"打印任何传入的参数"print("输出: ", arg1) # 输出: 10,70for var in vartuple:print(var)# 调用printinfo 函数printinfo(10)printinfo(70, 60, 50) # 60,50
匿名函数
#匿名函数#python 使用 lambda 来创建匿名函数lambda 只是一个表达式,函数体比 def 简单很多。lambda 的主体是一个表达式,而不是一个代码块。仅仅能在 lambda 表达式中封装有限的逻辑进去。lambda 函数拥有自己的命名空间,且不能访问自有参数列表之外或全局命名空间里的参数。虽然 lambda 函数看起来只能写一行,却不等同于 C 或 C++ 的内联函数,后者的目的是调用小函数时不占用栈内存从而增加运行效率sum = lambda arg1, arg2: arg1 + arg2print(sum(10,20)) # 30
变量作用域
#全局变量定义在函数外的拥有全局作用域,全局变量可以在整个程序范围内访问
#局部变量
定义在函数内部的变量拥有一个局部作用域,局部变量只能在其被声明的函数内部访问#调用函数时,所有在函数内声明的变量名称都将被加入到作用域中total = 0 # global variable.# 可写函数说明def sum(arg1, arg2):# 返回2个参数的和."total = arg1 + arg2 # total在这里是局部变量.print("Inside local: ", total) # 30return total# 调用sum函数sum(10, 20)print("Outside global total : ", total) #0
递归函数
#递归函数
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数
计算阶乘n! = 1 x 2 x 3 x ... x n,用函数fact(n)表示
fact(n)=n!=1×2×3×⋅⋅⋅×(n−1)×n=(n−1)!×n=fact(n−1)×n
fact(n)可以表示为n x fact(n-1),只有n=1时需要特殊处理def fact(n):if n==1:return 1return n * fact(n - 1)
Python高级
高级特性
切片
#切片取一个list或tuple的部分元素是非常常见的操作
#1. L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']取前3个元素 [L[0], L[1], L[2]]r = []n = 3for i in range(n):r.append(L[i])print(r)L[0:3] 或者 L[:3]
#2. 创建一个0-99的数列L = list(range(100))前10个数 L[:10]后10个数 L[-10:]前11-20个数 L[10:20]前10个数,每两个取一个 L[:10:2]所有数,每5个取一个 L[::5]原样复制一个 listL[:]
#3. tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple(0, 1, 2, 3, 4, 5)[:3]#4.字符串'xxx'也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串'ABCDEFG'[:3]
迭代
#1.给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)#2.比如C,Java语言,迭代list是通过下标完成的for (i=0; i<length; i++) {n = list[i];}#3.在Python中,迭代是通过for ... in来完成的#4.list这种数据类型虽然有下标,但很多其他数据类型是没有下标的,但是,只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代d = {'a': 1, 'b': 2, 'c': 3}for key in d:print(key) 如果要迭代value,可以用for value in d.values()如果要同时迭代key和value,可以用for k, v in d.items()#5.字符串也是可迭代对象,因此,也可以作用于for循环for ch in 'ABC':print(ch)#6.当我们使用for循环时,只要作用于一个可迭代对象,for循环就可以正常运行,而我们不太关心该对象究竟是list还是其他数据类型通过collections.abc模块的Iterable类型判断一个对象是可迭代对象from collections.abc import Iterableprint(isinstance('abc', Iterable)) #Trueprint(isinstance([1, 2, 3], Iterable)) #Trueprint(isinstance((1, 2, 3), Iterable))#Trueprint(isinstance({1, 2, 3}, Iterable))#Trueprint(isinstance({"AGE": 1, "name": 2, "high": 3}, Iterable))#Trueprint(isinstance(123, Iterable)) #False#7.如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身for i, value in enumerate(['A', 'B', 'C']):print(i, value)#8.for循环里,同时引用了两个变量,在Python里是很常见的for x, y in [(1, 1), (2, 4), (3, 9)]:print(x, y)
列表生成式
#1.是Python内置的非常简单却强大的可以用来创建list的生成式#2.要生成list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]可以用list(range(1, 11))#3.生成[1x1, 2x2, 3x3, ..., 10x10]L = []for i in range(1,11):L.append(i*i)[x * x for x in range(1, 11)]#4.写列表生成式时,把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来#5.for循环后面还可以加上if判断,可以筛选出仅偶数的平方[x * x for x in range(1, 11) if x %2==0]#6.可以使用两层循环,可以生成全排列[m+n for m in 'ABC' for n in 'XYZ']['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']#7.列出当前目录下的所有文件和目录名[d for d in os.listdir('.')] # os.listdir可以列出文件和目录#8.同时迭代key和valued = {'x': 'A', 'y': 'B', 'z': 'C'}for k, v in d.items():print(k, " = ", v)[k + " = " + v for k, v in d.items()]['x = A', 'y = B', 'z = C']#9.把一个list中所有的字符串变成小写L = ['Hello', 'World', 'IBM', 'Apple']print([s.lower() for s in L])#10.正常输出偶数[x for x in range(1, 11) if x % 2 == 0]我们不能在最后的if加上else[x for x in range(1, 11) if x % 2 == 0 else 0] -- 报错发现把if写在for前面必须加else,否则报错这是因为for前面的部分是一个表达式,它必须根据x计算出一个结果。因此,考察表达式:x if x % 2 == 0,它无法根据x计算出结果,因为缺少else,必须加上else#11.在一个列表生成式中,for前面的if ... else是表达式,而for后面的if是过滤条件,不能带else
生成器
生成器
#1.在Python中,这种一边循环一边计算的机制,称为生成器
#2.定义生成器:只要把一个列表生成式的[]改成(),就创建了一个generatorL = [x * x for x in range(10)]g = (x * x for x in range(10)) ----> <generator object <genexpr> at 0x1022ef630>创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator可以通过next()函数获得generator的下一个返回值next(g)next(g)next(g)
#3.generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误g = (x * x for x in range(10)):for n in g:print(n)#4.斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易def fib(max):n, a, b = 0, 0, 1while n < max:print(b)a, b = b, a + bn = n + 1return 'done'a, b = b, a + b相当于:t = (b, a + b) # t是一个tuplea = t[0]b = t[1]
#5.要把fib函数变成generator,只需要把print(b)改为yield b就可以了如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generatordef fib(max):n, a, b = 0, 0, 1while n < max:yield ba, b = b, a + bn = n + 1return 'done'
#6.函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行
迭代器
#迭代器
#1.可以直接作用于for循环的数据类型有一类是集合数据类型,如list、tuple、dict、set、str等一类是generator,包括生成器和带yield的generator function这些可以直接作用于for循环的对象统称为 可迭代对象:Iterable#2.可迭代对象:Iterable可以直接作用于for循环的对象统称为 可迭代对象:Iterable可以使用isinstance()判断一个对象是否是 Iterable对象>>> from collections.abc import Iterable>>> isinstance([], Iterable)True>>> isinstance({}, Iterable)True>>> isinstance('abc', Iterable)True>>> isinstance((x for x in range(10)), Iterable)True>>> isinstance(100, Iterable)False#3.迭代器:Iterator可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator可以使用isinstance()判断一个对象是否是Iterator对象>>> from collections.abc import Iterator>>> isinstance((x for x in range(10)), Iterator)True>>> isinstance([], Iterator)False>>> isinstance({}, Iterator)False>>> isinstance('abc', Iterator)False#4.生成器都是Iterator对象,但list、tuple、dict、str虽然是Iterable,却不是 迭代器:Iterator#5.把list、dict、str等Iterable变成Iterator可以使用iter()函数>>> isinstance(iter([]), Iterator)True>>> isinstance(iter('abc'), Iterator)True
#6.总结:1.凡是可作用于for循环的对象都是Iterable类型;2.凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;3.集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。4.Python的for循环本质上就是通过不断调用next()函数实现的for x in [1, 2, 3, 4, 5]:pass# 首先获得Iterator对象:it = iter([1, 2, 3, 4, 5])# 循环:while True:try:# 获得下一个值:x = next(it)except StopIteration:# 遇到StopIteration就退出循环break
函数式编程
高阶函数
#1.map(fx,Iterable)1.map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回2.有一个函数f(x)=x^2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上def f(x):return x * xr = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])print(r) -- r是一个Iteratorprint(next(r))print(list(r))通过list()函数让它把整个序列都计算出来并返回一个list3.把一个list所有数字转为字符串list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))#2. reduce(f, [x1, x2.....])1.reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)2.对一个序列求和,就可以用reduce实现 sum()函数也可以from functools import reducedef add(x, y):return x + yprint(reduce(add, [1, 3, 5, 7, 9])) # 1+3+5+7+9=25sum([1, 3, 5, 7, 9])3.把序列[1, 3, 5, 7, 9]变换成整数13579def fn(x, y):return x * 10 + yprint(reduce(fn, [1, 3, 5, 7, 9]))4.字符串转intDIGITS = {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}def str2int(s):def fn(x, y):return x * 10 + ydef char2num(s):return DIGITS[s]return reduce(fn, map(char2num, s))print(str2int('54353'))#3. filter(fx,[x1,x2....])1. filter()用于过滤序列filter()也接收一个函数和一个序列filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素2.在一个list中,删掉偶数,只保留奇数def is_odd(n):return n % 2 ==1print(list(filter(is_odd,[1, 2, 4, 5, 6, 9, 10, 15]))) # [1, 5, 9, 15]3.把一个序列中的空字符串删掉def not_empty(s):return s and s.strip()list(filter(not_empty, ['A', '', 'B', None, 'C', ' ']))# 结果: ['A', 'B', 'C']4.注意到filter()函数返回的也是一个Iterator也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list#4.sorted()1.排序算法2.sorted()函数就可以对list进行排序sorted([36, 5, -12, 9, -21])[-21, -12, 5, 9, 36]3.接收一个key函数来实现自定义的排序,例如按绝对值大小排序sorted([36, 5, -12, 9, -21], key=abs)[5, 9, -12, -21, 36]4.我们给sorted传入key函数,即可实现忽略大小写的排序sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)['about', 'bob', 'Credit', 'Zoo']5.要进行反向排序,不必改动key函数,可以传入第三个参数reverse=Truesorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)['Zoo', 'Credit', 'bob', 'about']
返回函数 (闭包)
#1.函数作为返回值1.实现一个可变参数的求和def calc_sum(*args):ax = 0for n in args:ax = ax + nreturn axprint(calc_sum(1,2,3))2.如果不需要立刻求和,可以不返回求和的结果,而是返回求和的函数def lazy_sum(*args):def sum():ax = 0for n in args:ax = ax + nreturn axreturn sumf = lazy_sum(1, 3, 5, 7, 9)print(f) #返回的并不是求和结果,而是求和函数print(f())#函数f时,才真正计算求和的结果#2.闭包1.在函数lazy_sum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为闭包.2.当我们调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数f1 = lazy_sum(1, 3, 5, 7, 9)f2 = lazy_sum(1, 3, 5, 7, 9)print(f1==f2) #False3.返回的函数并没有立刻执行,而是直到调用了f()才执行def count():fs = []for i in range(1, 4):def f():return i * ifs.append(f)return fsf1, f2, f3 = count()print(f1()) # 9print(f2()) # 9print(f3()) # 9原因就在于返回的函数引用了变量i,但它并非立刻执行。等到3个函数都返回时,它们所引用的变量i已经变成了3,因此最终结果为94.返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量
匿名函数
匿名函数#1.计算f(x)=x2list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))#2.匿名函数lambda x: x * x = def f(x):return x * x #3.匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数f = lambda x: x * xprint(f(5))
装饰器
#1.由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数def now():print('2021-07-25')f = nowf()#2.在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)def log(func):def wrapper(*args, **kw):print('call %s():' % func.__name__)return func(*args,**kw)return wrapper@logdef now2():print('2021-07-25-002')now2()
模块
面向对象编程
类和实例
#1.__init__方法1.__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身2.有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去
访问限制
#1.如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问
继承和多态
#1.继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写
获取对象信息
#1.type()#2.hasattr()class MyObject(object):def __init__(self):self.x = 9def power(self):return self.x * self.xobj = MyObject()print(hasattr(obj,'x')) # 有属性'x'吗?print(obj.x)print(hasattr(obj,'y'))# 有属性'y'吗?setattr(obj,'y',19)print(hasattr(obj,'y'))# 有属性'y'吗?print(getattr(obj,'y'))# 获取属性'y'print(obj.y)
实例属性和类属性
1.给实例绑定属性的方法是通过实例变量,或者通过self变量
2.实例属性属于各个实例所有,互不干扰;
3.类属性属于类所有,所有实例共享一个属性;
4.不要对实例属性和类属性使用相同的名字,否则将产生难以发现的错误。
面向对象高级编程
使用 slots
#定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性class Student(object):__slots__ = ('name','age')# 用tuple定义允许绑定的属性名称s = Student()# 创建新的实例s.name ='Michael' # 绑定属性'name's.age = 30#AttributeError: 'Student' object has no attribute 'score'#s.score = 99 # 绑定属性'score'try:s.score = 99except AttributeError as e:print('AttributeError:',e)
使用@property
#1.对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的#2.@property的使用:把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:class Student(object):@propertydef score(self):return self._score@score.setterdef score(self, value):if not isinstance(value, int):raise ValueError('score must be an integer!')if value < 0 or value > 100:raise ValueError('score must between 0 ~ 100!')self._score = values = Student()s.score = 60print('s.score = ',s.score)s.score = 9999
定制类
#1.__str__1.打印对象的实例值class Student(object):def __init__(self, name):self.name = namedef __str__(self):return 'Student object (name:%s)' % self.name__repr__ = __str__print(Student('Michael')) #Student object (name:Michael)#2.__iter__1.如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环class Fib(object):def __init__(self):self.a, self.b = 0, 1 # 初始化两个计数器a,bdef __iter__(self):return self # 实例本身就是迭代对象,故返回自己def __next__(self):self.a, self.b = self.b, self.a + self.b # 计算下一个值if self.a > 100000: # 退出循环的条件raise StopIteration()return self.a # 返回下一个值for n in Fib():print(n)#3.__getitem__要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:class Fib(object):def __getitem__(self, n):a, b = 1, 1for x in range(n):a, b = b, a + breturn a#4.__getattr__调用不存在的score属性,为了避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性class Student(object):def __init__(self):self.name = 'Michael'def __getattr__(self, attr):if attr=='score':return 99#5.__call__定义一个__call__()方法,就可以直接对实例进行调用class Student(object):def __init__(self, name):self.name = namedef __call__(self, *args, **kwargs):print('my name is %s' % self.name)s = Student('Michael')s() #my name is Michael
枚举
枚举from enum import Enum, unique@uniqueclass Weekday(Enum):Sun = 0Mon = 1day1 = Weekday.Monprint('day1 =', day1) # day1 = Weekday.Monprint('Weekday.Tue =', Weekday.Tue)print('Weekday.Tue =', Weekday['Tue'])print('Weekday.Tue =', Weekday.Tue.value) # Weekday.Tue = 2
元类
#1.type()#2.metaclass当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例# metaclass是创建类,所以必须从`type`类型派生:class ListMetaclass(type):def __new__(cls, name, bases, attrs):attrs['add'] = lambda self, value: self.append(value)return type.__new__(cls, name, bases, attrs)# 指示使用ListMetaclass来定制类class MyList(list, metaclass=ListMetaclass):passL = MyList()L.add(1)
IO编程
文件读写
文件读写with open('test.txt', 'r', encoding='utf-8') as f:s = f.read()print('open for read...')print(s)with open('test.txt', 'w', encoding='utf-8')as f:f.write('今天是 ')f.write(datetime.now().strftime('%Y-%m-%d'))
StringIO和BytesIO
StringIO和BytesIO1.StringIO顾名思义就是在内存中读写str,要把str写入StringIO,我们需要先创建一个StringIO,然后,像文件一样写入即可:from io import StringIO# 写入f = StringIO()f.write('hello')f.write(' ')f.write('world!')# 读取print(f.getvalue())2.StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO
操作文件和目录
操作文件和目录# 查看当前目录的绝对路径:>>> os.path.abspath('.')'/Users/michael'# 在某个目录下创建一个新目录,首先把新目录的完整路径表示出来:>>> os.path.join('/Users/michael', 'testdir')'/Users/michael/testdir'# 然后创建一个目录:>>> os.mkdir('/Users/michael/testdir')# 删掉一个目录:>>> os.rmdir('/Users/michael/testdir')把两个路径合成一个时,不要直接拼字符串,而要通过os.path.join()函数要拆分路径时,也不要直接去拆字符串,而要通过os.path.split()函数
序列化
1.我们把变量从内存中变成可存储或传输的过程称之为序列化2.把变量内容从序列化的对象重新读到内存里称之为反序列化
import json
d = dict(name='Bob', age=20, score=88)
data = json.dumps(d) #序列化成 json字符串print('json data is a str:', data)
reborn = json.loads(data) #反序列化为 python对象
print(reborn)
进程和线程
多进程
#1.multiprocessing-多进程模块multiprocessing模块提供了一个Process类来代表一个进程对象.from multiprocessing import Processimport os# 子进程要执行的代码def run_proc(name):print('Run child process %s (%s)...' % (name, os.getpid()))if __name__ == '__main__':print('Parent process %s.' % os.getpid())p = Process(target=run_proc, args=('test',))print('Child process will start.')p.start() # 用start()方法启动p.join() # join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。print('Child process end.')#2.Pool-进程池如果要启动大量的子进程,可以用进程池的方式批量创建子进程#3.进程间通信Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据
线程
#1.threading模块启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行#2.线程锁创建一个锁就是通过threading.Lock()来实现先要获取锁: lock.acquire()释放锁; lock.release()import time, threading# 假定这是你的银行存款:balance = 0lock = threading.Lock()def change_it(n):# 先存后取,结果应该为0:global balancebalance = balance + nbalance = balance - ndef run_thread(n):for i in range(2000000):# 先要获取锁:lock.acquire()try:# 放心地改吧:change_it(n)finally:# 改完了一定要释放锁:lock.release()t1 = threading.Thread(target=run_thread, args=(5,))t2 = threading.Thread(target=run_thread, args=(8,))t1.start()t2.start()t1.join()t2.join()print(balance)1.当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。2.获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。3.锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止
正则表达式
常用内建模块
##datetime#collectionsnamedtupledequedefaultdictOrderedDictChainMapCounter#base64Base64是一种用64个字符来表示任意二进制数据的方法如果要让记事本这样的文本处理软件能处理二进制数据(jpg,pdf...),就需要一个二进制到字符串的转换方法。Base64是一种最常见的二进制编码方法#struct解决bytes和其他二进制数据类型的转换#hashlibhashlib提供了常见的摘要算法,如MD5,SHA1等摘要算法又称哈希算法、散列算法。它通过一个函数,把任意长度的数据转换为一个长度固定的数据串(通常用16进制的字符串表示)#hmacHmac算法 它通过一个标准算法,在计算哈希的过程中,把key混入计算过程中#itertools内建模块itertools提供了非常有用的用于操作迭代对象的函数
Python
Python学习
2、python中一切皆对象
2.1 py中一切都是对象
python中函数和类也是对象,属于python的一等公民:
1:赋值给一个变量
2:可以添加到集合(如list)对象中
3: 可以作为参数传递给函数
4:可以当做函数的返回值
# test01_all_is_object.py
def ask(name='boby'):# 一个函数没有retrun,默认返回nullprint(name)#1.将 函数赋值给 变量
my_func = ask
# 对 变量进行操作,实际上就是对函数进行操作
my_func("bobby111111")class Person:def __init__(self):print('boddy222222')# 将 类 赋值给 变量
my_class = Person
# 对 变量进行操作,实际上就是对 类 进行操作
my_class()#2.可以添加到集合(如list)对象中
obj_list = []
obj_list.append(ask)
obj_list.append(Person)
for item in obj_list:print(item())def print_type(item):print(type(item))def decorator_func():print('desc start')# 一个函数中 可以返回另一个函数 (装饰器 的 实现原理)return ask # 返回函数名称my_ask = decorator_func()
my_ask('tom')
输出:
bobby111111
boddy222222
boby
None
boddy222222
<__main__.Person object at 0x000002CD473E35C0> #返回类的对象
desc start
tom
2.2 type,object,class 的关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eKRZGMvY-1647763303240)(C:\Users\d\AppData\Roaming\Typora\typora-user-images\image-20211128210517775.png)]
#test02_type_object_class.py
a = 1
b = 'abc'
print(type(1)) # <class 'int'>
print(type(int)) # <class 'type'>
print(type(b)) # <class 'str'>
print(type(str)) # <class 'type'>
# type 生成了-->int 生成了-->1
# type -->class -->obj
# type是用来生成类(class)的,平时看到的对象是 类的对象
# object是最顶层的基类
# type也是一个类,同时 type也是一个对象
class Student:pass
class MyStudent(Student):pass
stu = Student()
print(type(stu)) # <class '__main__.Student'>
print(type(Student)) # <class 'type'>
print(int.__bases__) # (<class 'object'>,)
print(str.__bases__) # (<class 'object'>,)
print(Student.__bases__) # (<class 'object'>,)
print(MyStudent.__bases__) # (<class '__main__.Student'>,)
print(type.__bases__) # (<class 'object'>,)
print(object.__bases__) # ()
print(type(object)) # <class 'type'>
2.3 python中常见的内置类型
#对象的三个特征:
1:身份 id 2:类型 3:值
>>> a = 2
>>> id(a)
1693871152
# None(全局只有1个)
>>> a = None
>>> b = None
>>> id(a) == id(b)
True
# 数值
1:int 2:float 3:complex(复数) 4:bool
# 迭代类型
迭代器 生成器 :用for循环取值
# 序列类型
1:list 2:bytes 3:range 4:tuple 5:str 6:array
# 映射(dict)
1:dict
# 集合
1:set 2:frozenset
# 上下文管理类型(with)
# 其他
1:模块类型
2:class和实例
3:函数类型
4:方法类型
5:代码类型
6:object类型
7:type类型
8:ellipsis类型
9:notimplemented类对象
3、魔法方法
3.1 什么是魔法函数
双下划线函数(python内置函数),自己定义的没有用
魔法函数和 本身类的继承没有关系,可以写在任意一个类下面
# company.py
class Company(object):def __init__(self, employee_list):self.employee = employee_list# Company对象 会变成 可迭代类型# 可以切片def __getitem__(self, item):return self.employee[item]def __len__(self):return len(self.employee)
company = Company(['tom', 'bob', 'jane'])
company_list1 = company[:2]
print(len(company)) # 注释__getitem__ 会报错emploee = company.employee
for em in emploee:print(em)# for语句实际 会去找Company对象,
# for 循环先尝试有没有 迭代器(__iter__),没有则去调用__getitem__
# python解释器会去调用__getitem__,直到调用抛异常为止才结束
for item in company:print(item)for item in company_list1:print(item)
3.2 python的数据模型
3.3 常见的魔法函数
非数学运算
#字符串
__repr__ :开发模式下会用到
__str__ :字符串格式化
#集合,序列
__len__ __getitem__ __setitem__ __delitem__ __contains__
#迭代相关
__iter__ __next__
#可调用
__call__
#with上下文管理器
__enter__ __exit__
#数值转换
__abs__ __boll__ __int__ __float__ __hash__ __index__
#元类相关
__new__ __init__
#属性相关
__getattr__ __setattr__ _getattribute__ __setattribute__ __dir__
#属性描述符
__get__ __get__ __delete__
#协程
__await__ __aiter__ __anext__ __aenter__ __aexit__
数学运算
#一元运算符
__neg__ (负数) __pos__ (正数) __abs__(绝对值)
#二元运算符
__lt__ (<) __le__(<=) __eq__(==) __ne__(!=) __gt__(>) __ge__(<)
#算术运算符
__add__(+) __sub__(-) __mul__(*) __truediv__(/)
#反向算术运算符
__radd__ __rsub__ __rmul__ __rtruediv__
#增量赋值算术
__iadd__ __isub__ __imul__ __itruediv__
#位运算符
__invert__(~) __lshift__(<<) __rshift__(>>) __and__(&) __or__(|)
3.4 举例说明
4、深入类和对象
5、自定义序列类
6、深入set和dict
7、对象引用、可变性和垃圾回收
8、元类编程
9、迭代器和生成器
10、socket编程
11、多线程,多进程,线程池
12、协程和异步IO
13、asyncio并发编程
Python面试题
50道高频Python面试编程题
题目001: 在Python中如何实现单例模式。
点评:单例模式是指让一个类只能创建出唯一的实例,这个题目在面试中出现的频率极高,因为它考察的不仅仅是单例模式,更是对Python语言到底掌握到何种程度,建议大家用装饰器和元类这两种方式来实现单例模式,因为这两种方式的通用性最强,而且也可以顺便展示自己对装饰器和元类中两个关键知识点的理解。
方法一:使用装饰器实现单例模式。
from functools import wrapsdef singleton(cls):"""单例类装饰器"""instances = {}@wraps(cls)def wrapper(*args, **kwargs):if cls not in instances:instances[cls] = cls(*args, **kwargs)return instances[cls]return wrapper@singleton
class President:pass
扩展:装饰器是Python中非常有特色的语法,用一个函数去装饰另一个函数或类,为其添加额外的能力。通常通过装饰来实现的功能都属横切关注功能,也就是跟正常的业务逻辑没有必然联系,可以动态添加或移除的功能。装饰器可以为代码提供缓存、代理、上下文环境等服务,它是对设计模式中代理模式的践行。在写装饰器的时候,带装饰功能的函数(上面代码中的
wrapper
函数)通常都会用functools
模块中的wraps
再加以装饰,这个装饰器最重要的作用是给被装饰的类或函数动态添加一个__wrapped__
属性,这个属性会将被装饰之前的类或函数保留下来,这样在我们不需要装饰功能的时候,可以通过它来取消装饰器,例如可以使用President = President.__wrapped__
来取消对President
类做的单例处理。需要提醒大家的是:上面的单例并不是线程安全的,如果要做到线程安全,需要对创建对象的代码进行加锁的处理。在Python中可以使用threading
模块的RLock
对象来提供锁,可以使用锁对象的acquire
和release
方法来实现加锁和解锁的操作。当然,更为简便的做法是使用锁对象的with
上下文语法来进行隐式的加锁和解锁操作。
方法二:使用元类实现单例模式。
class SingletonMeta(type):"""自定义单例元类"""def __init__(cls, *args, **kwargs):cls.__instance = Nonesuper().__init__(*args, **kwargs)def __call__(cls, *args, **kwargs):if cls.__instance is None:cls.__instance = super().__call__(*args, **kwargs)return cls.__instanceclass President(metaclass=SingletonMeta):pass
扩展:Python是面向对象的编程语言,在面向对象的世界中,一切皆为对象。对象是通过类来创建的,而类本身也是对象,类这样的对象是通过元类来创建的。我们在定义类时,如果没有给一个类指定父类,那么默认的父类是
object
,如果没有给一个类指定元类,那么默认的元类是type
。通过自定义的元类,我们可以改变一个类默认的行为,就如同上面的代码中,我们通过元类的__call__
魔术方法,改变了President
类的构造器那样。补充:关于单例模式,在面试中还有可能被问到它的应用场景。通常一个对象的状态是被其他对象共享的,就可以将其设计为单例,例如项目中使用的数据库连接池对象和配置对象通常都是单例,这样才能保证所有地方获取到的数据库连接和配置信息是完全一致的;而且由于对象只有唯一的实例,因此从根本上避免了重复创建对象造成的时间和空间上的开销,也避免了对资源的多重占用。再举个例子,项目中的日志操作通常也会使用单例模式,这是因为共享的日志文件一直处于打开状态,只能有一个实例去操作它,否则在写入日志的时候会产生混乱。
题目002:不使用中间变量,交换两个变量a
和b
的值。
点评:典型的送人头的题目,通常交换两个变量需要借助一个中间变量,如果不允许使用中间变量,在其他编程语言中可以使用异或运算的方式来实现交换两个变量的值,但是Python中有更为简单明了的做法。
方法一:
a = a ^ b
b = a ^ b
a = a ^ b
方法二:
a, b = b, a
扩展:需要注意,
a, b = b, a
这种做法其实并不是元组解包,虽然很多人都这样认为。Python字节码指令中有ROT_TWO
指令来支持这个操作,类似的还有ROT_THREE
,对于3个以上的元素,如a, b, c, d = b, c, d, a
,才会用到创建元组和元组解包。想知道你的代码对应的字节码指令,可以使用Python标准库中dis
模块的dis
函数来反汇编你的Python代码。
题目003:写一个删除列表中重复元素的函数,要求去重后元素相对位置保持不变。
点评:这个题目在初中级Python岗位面试的时候经常出现,题目源于《Python Cookbook》这本书第一章的第10个问题,有很多面试题其实都是这本书上的原题,所以建议大家有时间好好研读一下这本书。
def dedup(items):no_dup_items = []seen = set()for item in items:if item not in seen:no_dup_items.append(item)seen.add(item)return no_dup_items
如果愿意也可以把上面的函数改造成一个生成器,代码如下所示。
def dedup(items):seen = set()for item in items:if item not in seen:yield itemseen.add(item)
扩展:由于Python中的集合底层使用哈希存储,所以集合的
in
和not in
成员运算在性能上远远优于列表,所以上面的代码我们使用了集合来保存已经出现过的元素。集合中的元素必须是hashable
对象,因此上面的代码在列表元素不是hashable
对象时会失效,要解决这个问题可以给函数增加一个参数,该参数可以设计为返回哈希码或hashable
对象的函数。
题目004:假设你使用的是官方的CPython,说出下面代码的运行结果。
点评:下面的程序对实际开发并没有什么意义,但却是CPython中的一个大坑,这道题旨在考察面试者对官方的Python解释器到底了解到什么程度。
a, b, c, d = 1, 1, 1000, 1000
print(a is b, c is d)def foo():e = 1000f = 1000print(e is f, e is d)g = 1print(g is a)foo()
运行结果:
True False
True False
True
上面代码中a is b
的结果是True
但c is d
的结果是False
,这一点的确让人费解。CPython解释器出于性能优化的考虑,把频繁使用的整数对象用一个叫small_ints
的对象池缓存起来造成的。small_ints
缓存的整数值被设定为[-5, 256]
这个区间,也就是说,在任何引用这些整数的地方,都不需要重新创建int
对象,而是直接引用缓存池中的对象。如果整数不在该范围内,那么即便两个整数的值相同,它们也是不同的对象。
CPython底层为了进一步提升性能还做了另一个设定,对于同一个代码块中值不在small_ints
缓存范围内的整数,如果同一个代码块中已经存在一个值与其相同的整数对象,那么就直接引用该对象,否则创建新的int
对象。需要大家注意的是,这条规则对数值型适用,但对字符串则需要考虑字符串的长度,这一点大家可以自行证明。
扩展:如果你用PyPy(另一种Python解释器实现,支持JIT,对CPython的缺点进行了改良,在性能上优于CPython,但对三方库的支持略差)来运行上面的代码,你会发现所有的输出都是True。
题目005:Lambda函数是什么,举例说明的它的应用场景。
点评:这个题目主要想考察的是Lambda函数的应用场景,潜台词是问你在项目中有没有使用过Lambda函数,具体在什么场景下会用到Lambda函数,借此来判断你写代码的能力。因为Lambda函数通常用在高阶函数中,主要的作用是通过向函数传入函数或让函数返回函数最终实现代码的解耦合。
Lambda函数也叫匿名函数,它是功能简单用一行代码就能实现的小型函数。Python中的Lambda函数只能写一个表达式,这个表达式的执行结果就是函数的返回值,不用写return
关键字。Lambda函数因为没有名字,所以也不会跟其他函数发生命名冲突的问题。
扩展:面试的时候有可能还会考你用Lambda函数来实现一些功能,也就是用一行代码来实现题目要求的功能,例如:用一行代码实现求阶乘的函数,用一行代码实现求最大公约数的函数等。
fac = lambda x: __import__('functools').reduce(int.__mul__, range(1, x + 1), 1) gcd = lambda x, y: y % x and gcd(y % x, x) or x
Lambda函数其实最为主要的用途是把一个函数传入另一个高阶函数(如Python内置的filter
、map
等)中来为函数做解耦合,增强函数的灵活性和通用性。下面的例子通过使用filter
和map
函数,实现了从列表中筛选出奇数并求平方构成新列表的操作,因为用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过另外一个函数传入的,因此这filter
和map
函数没有跟特定的过滤和映射数据的规则耦合在一起。
items = [12, 5, 7, 10, 8, 19]
items = list(map(lambda x: x ** 2, filter(lambda x: x % 2, items)))
print(items) # [25, 49, 361]
扩展:用列表的生成式来实现上面的代码会更加简单明了,代码如下所示。
items = [12, 5, 7, 10, 8, 19] items = [x ** 2 for x in items if x % 2] print(items) # [25, 49, 361]
题目006:说说Python中的浅拷贝和深拷贝。
点评:这个题目本身出现的频率非常高,但是就题论题而言没有什么技术含量。对于这种面试题,在回答的时候一定要让你的答案能够超出面试官的预期,这样才能获得更好的印象分。所以回答这个题目的要点不仅仅是能够说出浅拷贝和深拷贝的区别,深拷贝的时候可能遇到的两大问题,还要说出Python标准库对浅拷贝和深拷贝的支持,然后可以说说列表、字典如何实现拷贝操作以及如何通过序列化和反序列的方式实现深拷贝,最后还可以提到设计模式中的原型模式以及它在项目中的应用。
浅拷贝通常只复制对象本身,而深拷贝不仅会复制对象,还会递归的复制对象所关联的对象。深拷贝可能会遇到两个问题:一是一个对象如果直接或间接的引用了自身,会导致无休止的递归拷贝;二是深拷贝可能对原本设计为多个对象共享的数据也进行拷贝。Python通过copy
模块中的copy
和deepcopy
函数来实现浅拷贝和深拷贝操作,其中deepcopy
可以通过memo
字典来保存已经拷贝过的对象,从而避免刚才所说的自引用递归问题;此外,可以通过copyreg
模块的pickle
函数来定制指定类型对象的拷贝行为。
deepcopy
函数的本质其实就是对象的一次序列化和一次返回序列化,面试题中还考过用自定义函数实现对象的深拷贝操作,显然我们可以使用pickle
模块的dumps
和loads
来做到,代码如下所示。
import picklemy_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))
列表的切片操作[:]
相当于实现了列表对象的浅拷贝,而字典的copy
方法可以实现字典对象的浅拷贝。对象拷贝其实是更为快捷的创建对象的方式。在Python中,通过构造器创建对象属于两阶段构造,首先是分配内存空间,然后是初始化。在创建对象时,我们也可以基于“原型”对象来创建新对象,通过对原型对象的拷贝(复制内存)就完成了对象的创建和初始化,这种做法更加高效,这也就是设计模式中的原型模式。在Python中,我们可以通过元类的方式来实现原型模式,代码如下所示。
import copyclass PrototypeMeta(type):"""实现原型模式的元类"""def __init__(cls, *args, **kwargs):super().__init__(*args, **kwargs)# 为对象绑定clone方法来实现对象拷贝cls.clone = lambda self, is_deep=True: \copy.deepcopy(self) if is_deep else copy.copy(self)class Person(metaclass=PrototypeMeta):passp1 = Person()
p2 = p1.clone() # 深拷贝
p3 = p1.clone(is_deep=False) # 浅拷贝
题目007:Python是如何实现内存管理的?
点评:当面试官问到这个问题的时候,一个展示自己的机会就摆在面前了。你要先反问面试官:“你说的是官方的CPython解释器吗?”。这个反问可以展示出你了解过Python解释器的不同的实现版本,而且你也知道面试官想问的是CPython。当然,很多面试官对不同的Python解释器底层实现到底有什么差别也没有概念。所以,千万不要觉得面试官一定比你强,怀揣着这份自信可以让你更好的完成面试。
Python提供了自动化的内存管理,也就是说内存空间的分配与释放都是由Python解释器在运行时自动进行的,自动管理内存功能极大的减轻程序员的工作负担,也能够帮助程序员在一定程度上解决内存泄露的问题。以CPython解释器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。
引用计数:对于CPython解释器来说,Python中的每一个对象其实就是PyObject
结构体,它的内部有一个名为ob_refcnt
的引用计数器成员变量。程序在运行的过程中ob_refcnt
的值会被更新并藉此来反映引用有多少个变量引用到该对象。当对象的引用计数值为0时,它的内存就会被释放掉。
typedef struct _object {_PyObject_HEAD_EXTRAPy_ssize_t ob_refcnt;struct _typeobject *ob_type;
} PyObject;
以下情况会导致引用计数加1
:
- 对象被创建
- 对象被引用
- 对象作为参数传入到一个函数中
- 对象作为元素存储到一个容器中
以下情况会导致引用计数减1
:
- 用
del
语句显示删除对象引用 - 对象引用被重新赋值其他对象
- 一个对象离开它所在的作用域
- 持有该对象的容器自身被销毁
- 持有该对象的容器删除该对象
可以通过sys
模块的getrefcount
函数来获得对象的引用计数。引用计数的内存管理方式在遇到循环引用的时候就会出现致命伤,因此需要其他的垃圾回收算法对其进行补充。
标记清理:CPython使用了“标记-清理”(Mark and Sweep)算法解决容器类型可能产生的循环引用问题。该算法在垃圾回收时分为两个阶段:标记阶段,遍历所有的对象,如果对象是可达的(被其他对象引用),那么就标记该对象为可达;清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。CPython底层维护了两个双端链表,一个链表存放着需要被扫描的容器对象(姑且称之为链表A),另一个链表存放着临时不可达对象(姑且称之为链表B)。为了实现“标记-清理”算法,链表中的每个节点除了有记录当前引用计数的ref_count
变量外,还有一个gc_ref
变量,这个gc_ref
是ref_count
的一个副本,所以初始值为ref_count
的大小。执行垃圾回收时,首先遍历链表A中的节点,并且将当前对象所引用的所有对象的gc_ref
减1
,这一步主要作用是解除循环引用对引用计数的影响。再次遍历链表A中的节点,如果节点的gc_ref
值为0
,那么这个对象就被标记为“暂时不可达”(GC_TENTATIVELY_UNREACHABLE
)并被移动到链表B中;如果节点的gc_ref
不为0
,那么这个对象就会被标记为“可达“(GC_REACHABLE
),对于”可达“对象,还要递归的将该节点可以到达的节点标记为”可达“;链表B中被标记为”可达“的节点要重新放回到链表A中。在两次遍历之后,链表B中的节点就是需要释放内存的节点。
分代回收:在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收效率。分代回收的基本思想是:对象存在的时间越长,是垃圾的可能性就越小,应该尽量不对这样的对象进行垃圾回收。CPython将对象分为三种世代分别记为0
、1
、2
,每一个新生对象都在第0
代中,如果该对象在一轮垃圾回收扫描中存活下来,那么它将被移到第1
代中,存在于第1
代的对象将较少的被垃圾回收扫描到;如果在对第1
代进行垃圾回收扫描时,这个对象又存活下来,那么它将被移至第2代中,在那里它被垃圾回收扫描的次数将会更少。分代回收扫描的门限值可以通过gc
模块的get_threshold
函数来获得,该函数返回一个三元组,分别表示多少次内存分配操作后会执行0
代垃圾回收,多少次0
代垃圾回收后会执行1
代垃圾回收,多少次1
代垃圾回收后会执行2
代垃圾回收。需要说明的是,如果执行一次2
代垃圾回收,那么比它年轻的代都要执行垃圾回收。如果想修改这几个门限值,可以通过gc
模块的set_threshold
函数来做到。
题目008:说一下你对Python中迭代器和生成器的理解。
点评:很多人面试者都会写迭代器和生成器,但是却无法准确的解释什么是迭代器和生成器。如果你也有同样的困惑,可以参考下面的回答。
迭代器是实现了迭代器协议的对象。跟其他编程语言不通,Python中没有用于定义协议或表示约定的关键字,像interface
、protocol
这些单词并不在Python语言的关键字列表中。Python语言通过魔法方法来表示约定,也就是我们所说的协议,而__next__
和__iter__
这两个魔法方法就代表了迭代器协议。可以通过for-in
循环从迭代器对象中取出值,也可以使用next
函数取出迭代器对象中的下一个值。生成器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。
扩展:面试中经常让写生成斐波那契数列的迭代器,大家可以参考下面的代码。
class Fib(object):def __init__(self, num):self.num = numself.a, self.b = 0, 1self.idx = 0def __iter__(self):return selfdef __next__(self):if self.idx < self.num:self.a, self.b = self.b, self.a + self.bself.idx += 1return self.araise StopIteration()
如果用生成器的语法来改写上面的代码,代码会简单优雅很多。
def fib(num):a, b = 0, 1for _ in range(num):a, b = b, a + byield a
题目009:正则表达式的match方法和search方法有什么区别?
点评:正则表达式是字符串处理的重要工具,所以也是面试中经常考察的知识点。在Python中,使用正则表达式有两种方式,一种是直接调用
re
模块中的函数,传入正则表达式和需要处理的字符串;一种是先通过re
模块的compile
函数创建正则表达式对象,然后再通过对象调用方法并传入需要处理的字符串。如果一个正则表达式被频繁的使用,我们推荐用re.compile
函数创建正则表达式对象,这样会减少频繁编译同一个正则表达式所造成的开销。
match
方法是从字符串的起始位置进行正则表达式匹配,返回Match
对象或None。search
方法会扫描整个字符串来找寻匹配的模式,同样也是返回Match对象或None。
题目010:下面这段代码的执行结果是什么。
def multiply():return [lambda x: i * x for i in range(4)]print([m(100) for m in multiply()])
运行结果:
[300, 300, 300, 300]
上面代码的运行结果很容易被误判为[0, 100, 200, 300]
。首先需要注意的是multiply
函数用生成式语法返回了一个列表,列表中保存了4个Lambda函数,这4个Lambda函数会返回传入的参数乘以i
的结果。需要注意的是这里有闭包(closure)现象,multiply
函数中的局部变量i
的生命周期被延展了,由于i
最终的值是3
,所以通过m(100)
调列表中的Lambda函数时会返回300
,而且4个调用都是如此。
如果想得到[0, 100, 200, 300]
这个结果,可以按照下面几种方式来修改multiply
函数。
方法一:使用生成器,让函数获得i
的当前值。
def multiply():return (lambda x: i * x for i in range(4))print([m(100) for m in multiply()])
或者
def multiply():for i in range(4):yield lambda x: x * iprint([m(100) for m in multiply()])
方法二:使用偏函数,彻底避开闭包。
from functools import partial
from operator import __mul__def multiply():return [partial(__mul__, i) for i in range(4)]print([m(100) for m in multiply()])
题目011:Python中为什么没有函数重载?
点评:C++、Java、C#等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分。重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为编译时多态性或者叫前绑定。这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解Python是动态类型语言,是否知道Python中函数的可变参数、关键字参数这些概念。
首先Python是解释型语言,函数重载现象通常出现在编译型语言中。其次Python是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者Python中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生不同的行为。
题目012:用Python代码实现Python内置函数max。
点评:这个题目看似简单,但实际上还是比较考察面试者的功底。因为Python内置的
max
函数既可以传入可迭代对象找出最大,又可以传入两个或多个参数找出最大;最为关键的是还可以通过命名关键字参数key
来指定一个用于元素比较的函数,还可以通过default
命名关键字参数来指定当可迭代对象为空时返回的默认值。
下面的代码仅供参考:
def my_max(*args, key=None, default=None):"""获取可迭代对象中最大的元素或两个及以上实参中最大的元素:param args: 一个可迭代对象或多个元素:param key: 提取用于元素比较的特征值的函数,默认为None:param default: 如果可迭代对象为空则返回该默认值,如果没有给默认值则引发ValueError异常:return: 返回可迭代对象或多个元素中的最大元素"""if len(args) == 1 and len(args[0]) == 0:if default:return defaultelse:raise ValueError('max() arg is an empty sequence')items = args[0] if len(args) == 1 else argsmax_elem, max_value = items[0], items[0]if key:max_value = key(max_value)for item in items:value = itemif key:value = key(item)if value > max_value:max_elem, max_value = item, valuereturn max_elem
题目013:写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。
点评:送人头的题目,不解释。
def count_letters(items):result = {}for item in items:if isinstance(item, (int, float)):result[item] = result.get(item, 0) + 1return result
也可以直接使用Python标准库中collections
模块的Counter
类来解决这个问题,Counter
是dict
的子类,它会将传入的序列中的每个元素作为键,元素出现的次数作为值来构造字典。
from collections import Counterdef count_letters(items):counter = Counter(items)return {key: value for key, value in counter.items() \if isinstance(key, (int, float))}
题目014:使用Python代码实现遍历一个文件夹的操作。
点评:基本也是送人头的题目,只要用过
os
模块就应该知道怎么做。
Python标准库os
模块的walk
函数提供了遍历一个文件夹的功能,它返回一个生成器。
import osg = os.walk('/Users/Hao/Downloads/')
for path, dir_list, file_list in g:for dir_name in dir_list:print(os.path.join(path, dir_name))for file_name in file_list:print(os.path.join(path, file_name))
说明:
os.path
模块提供了很多进行路径操作的工具函数,在项目开发中也是经常会用到的。如果题目明确要求不能使用os.walk
函数,那么可以使用os.listdir
函数来获取指定目录下的文件和文件夹,然后再通过循环遍历用os.isdir
函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。
题目015:现有2元、3元、5元共三种面额的货币,如果需要找零99元,一共有多少种找零的方式?
点评:还有一个非常类似的题目:“一个小朋友走楼梯,一次可以走1个台阶、2个台阶或3个台阶,问走完10个台阶一共有多少种走法?”,这两个题目的思路是一样,如果用递归函数来写的话非常简单。
from functools import lru_cache@lru_cache()
def change_money(total):if total == 0:return 1if total < 0:return 0return change_money(total - 2) + change_money(total - 3) + \change_money(total - 5)
说明:在上面的代码中,我们用
lru_cache
装饰器装饰了递归函数change_money
,如果不做这个优化,上面代码的渐近时间复杂度将会是[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlttBR2N-1647763303244)(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==)],而如果参数total
的值是99
,这个运算量是非常巨大的。lru_cache
装饰器会缓存函数的执行结果,这样就可以减少重复运算所造成的开销,这是空间换时间的策略,也是动态规划的编程思想。
题目016:写一个函数,给定矩阵的阶数n
,输出一个螺旋式数字矩阵。
例如:n = 2,返回:
1 2 4 3
例如:n = 3,返回:
1 2 3 8 9 4 7 6 5
这个题目本身并不复杂,下面的代码仅供参考。
def show_spiral_matrix(n):matrix = [[0] * n for _ in range(n)]row, col = 0, 0num, direction = 1, 0while num <= n ** 2:if matrix[row][col] == 0:matrix[row][col] = numnum += 1if direction == 0:if col < n - 1 and matrix[row][col + 1] == 0:col += 1else:direction += 1elif direction == 1:if row < n - 1 and matrix[row + 1][col] == 0:row += 1else:direction += 1elif direction == 2:if col > 0 and matrix[row][col - 1] == 0:col -= 1else:direction += 1else:if row > 0 and matrix[row - 1][col] == 0:row -= 1else:direction += 1direction %= 4for x in matrix:for y in x:print(y, end='\t')print()
题目017:阅读下面的代码,写出程序的运行结果。
items = [1, 2, 3, 4]
print([i for i in items if i > 2])
print([i for i in items if i % 2])
print([(x, y) for x, y in zip('abcd', (1, 2, 3, 4, 5))])
print({x: f'item{x ** 2}' for x in (2, 4, 6)})
print(len({x for x in 'hello world' if x not in 'abcdefg'}))
点评:生成式(推导式)属于Python的特色语法之一,几乎是面试必考内容。Python中通过生成式字面量语法,可以创建出列表、集合、字典。
[3, 4]
[1, 3]
[('a', 1), ('b', 2), ('c', 3), ('d', 4)]
{2: 'item4', 4: 'item16', 6: 'item36'}
6
题目018:说出下面代码的运行结果。
class Parent:x = 1class Child1(Parent):passclass Child2(Parent):passprint(Parent.x, Child1.x, Child2.x)
Child1.x = 2
print(Parent.x, Child1.x, Child2.x)
Parent.x = 3
print(Parent.x, Child1.x, Child2.x)
点评:运行上面的代码首先输出
1 1 1
,这一点大家应该没有什么疑问。接下来,通过Child1.x = 2
给类Child1
重新绑定了属性x
并赋值为2
,所以Child1.x
会输出2
,而Parent
和Child2
并不受影响。执行Parent.x = 3
会重新给Parent
类的x
属性赋值为3
,由于Child2
的x
属性继承自Parent
,所以Child2.x
的值也是3
;而之前我们为Child1
重新绑定了x
属性,那么它的x
属性值不会受到Parent.x = 3
的影响,还是之前的值2
。
1 1 1
1 2 1
3 2 3
题目19:说说你用过Python标准库中的哪些模块。
点评:Python标准库中的模块非常多,建议大家根据自己过往的项目经历来介绍你用过的标准库和三方库,因为这些是你最为熟悉的,经得起面试官深挖的。
模块名 | 介绍 |
---|---|
sys |
跟Python解释器相关的变量和函数,例如:sys.version 、sys.exit()
|
os |
和操作系统相关的功能,例如:os.listdir() 、os.remove()
|
re |
和正则表达式相关的功能,例如:re.compile() 、re.search()
|
math |
和数学运算相关的功能,例如:math.pi 、math.e 、math.cos
|
logging |
和日志系统相关的类和函数,例如:logging.Logger 、logging.Handler
|
json / pickle |
实现对象序列化和反序列的模块,例如:json.loads 、json.dumps
|
hashlib |
封装了多种哈希摘要算法的模块,例如:hashlib.md5 、hashlib.sha1
|
urllib |
包含了和URL相关的子模块,例如:urllib.request 、urllib.parse
|
itertools |
提供各种迭代器的模块,例如:itertools.cycle 、itertools.product
|
functools |
函数相关工具模块,例如:functools.partial 、functools.lru_cache
|
collections / heapq |
封装了常用数据结构和算法的模块,例如:collections.deque
|
threading / multiprocessing |
多线程/多进程相关类和函数的模块,例如:threading.Thread
|
concurrent.futures / asyncio |
并发编程/异步编程相关的类和函数的模块,例如:ThreadPoolExecutor
|
base64 |
提供BASE-64编码相关函数的模块,例如:bas64.encode
|
csv |
和读写CSV文件相关的模块,例如:csv.reader 、csv.writer
|
profile / cProfile / pstats |
和代码性能剖析相关的模块,例如:cProfile.run 、pstats.Stats
|
unittest |
和单元测试相关的模块,例如:unittest.TestCase
|
题目20:**init__
和__new**
方法有什么区别?
Python中调用构造器创建对象属于两阶段构造过程,首先执行__new__
方法获得保存对象所需的内存空间,再通过__init__
执行对内存空间数据的填充(对象属性的初始化)。__new__
方法的返回值是创建好的Python对象(的引用),而__init__
方法的第一个参数就是这个对象(的引用),所以在__init__
中可以完成对对象的初始化操作。__new__
是类方法,它的第一个参数是类,__init__
是对象方法,它的第一个参数是对象。
题目21:输入年月日,判断这个日期是这一年的第几天。
方法一:不使用标准库中的模块和函数。
def is_leap_year(year):"""判断指定的年份是不是闰年,平年返回False,闰年返回True"""return year % 4 == 0 and year % 100 != 0 or year % 400 == 0def which_day(year, month, date):"""计算传入的日期是这一年的第几天"""# 用嵌套的列表保存平年和闰年每个月的天数days_of_month = [[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]]days = days_of_month[is_leap_year(year)][:month - 1]return sum(days) + date
方法二:使用标准库中的datetime
模块。
import datetimedef which_day(year, month, date):end = datetime.date(year, month, date)start = datetime.date(year, 1, 1)return (end - start).days + 1
题目22:平常工作中用什么工具进行静态代码分析。
点评:静态代码分析工具可以从代码中提炼出各种静态属性,这使得开发者可以对代码的复杂性、可维护性和可读性有更好的了解,这里所说的静态属性包括:
- 代码是否符合编码规范,例如:PEP-8。
- 代码中潜在的问题,包括:语法错误、缩进问题、导入缺失、变量覆盖等。
- 代码中的坏味道。
- 代码的复杂度。
- 代码的逻辑问题。
工作中静态代码分析主要用到的是Pylint和Flake8。Pylint可以检查出代码错误、坏味道、不规范的代码等问题,较新的版本中还提供了代码复杂度统计数据,可以生成检查报告。Flake8封装了Pyflakes(检查代码逻辑错误)、McCabe(检查代码复杂性)和Pycodestyle(检查代码是否符合PEP-8规范)工具,它可以执行这三个工具提供的检查。
题目23:说一下你知道的Python中的魔术方法。
点评:魔术方法也称为魔法方法,是Python中的特色语法,也是面试中的高频问题。
魔术方法 | 作用 |
---|---|
__new__ 、__init__ 、__del__
|
创建和销毁对象相关 |
__add__ 、__sub__ 、__mul__ 、__div__ 、__floordiv__ 、__mod__
|
算术运算符相关 |
__eq__ 、__ne__ 、__lt__ 、__gt__ 、__le__ 、__ge__
|
关系运算符相关 |
__pos__ 、__neg__ 、__invert__
|
一元运算符相关 |
__lshift__ 、__rshift__ 、__and__ 、__or__ 、__xor__
|
位运算相关 |
__enter__ 、__exit__
|
上下文管理器协议 |
__iter__ 、__next__ 、__reversed__
|
迭代器协议 |
__int__ 、__long__ 、__float__ 、__oct__ 、__hex__
|
类型/进制转换相关 |
__str__ 、__repr__ 、__hash__ 、__dir__
|
对象表述相关 |
__len__ 、__getitem__ 、__setitem__ 、__contains__ 、__missing__
|
序列相关 |
__copy__ 、__deepcopy__
|
对象拷贝相关 |
__call__ 、__setattr__ 、__getattr__ 、__delattr__
|
其他魔术方法 |
题目24:函数参数*arg
和**kwargs
分别代表什么?
Python中,函数的参数分为位置参数、可变参数、关键字参数、命名关键字参数。*args
代表可变参数,可以接收0
个或任意多个参数,当不确定调用者会传入多少个位置参数时,就可以使用可变参数,它会将传入的参数打包成一个元组。**kwargs
代表关键字参数,可以接收用参数名=参数值
的方式传入的参数,传入的参数的会打包成一个字典。定义函数时如果同时使用*args
和**kwargs
,那么函数可以接收任意参数。
题目25:写一个记录函数执行时间的装饰器。
点评:高频面试题,也是最简单的装饰器,面试者必须要掌握的内容。
方法一:用函数实现装饰器。
from functools import wraps
from time import timedef record_time(func):@wraps(func)def wrapper(*args, **kwargs):start = time()result = func(*args, **kwargs)print(f'{func.__name__}执行时间: {time() - start}秒')return resultreturn wrapper
方法二:用类实现装饰器。类有__call__
魔术方法,该类对象就是可调用对象,可以当做装饰器来使用。
from functools import wraps
from time import timeclass Record:def __call__(self, func):@wraps(func)def wrapper(*args, **kwargs):start = time()result = func(*args, **kwargs)print(f'{func.__name__}执行时间: {time() - start}秒')return resultreturn wrapper
说明:装饰器可以用来装饰类或函数,为其提供额外的能力,属于设计模式中的代理模式。
扩展:装饰器本身也可以参数化,例如上面的例子中,如果不希望在终端中显示函数的执行时间而是希望由调用者来决定如何输出函数的执行时间,可以通过参数化装饰器的方式来做到,代码如下所示。
from functools import wraps
from time import timedef record_time(output):"""可以参数化的装饰器"""def decorate(func):@wraps(func)def wrapper(*args, **kwargs):start = time()result = func(*args, **kwargs)output(func.__name__, time() - start)return resultreturn wrapperreturn decorate
题目26:什么是鸭子类型(duck typing)?
鸭子类型是动态类型语言判断一个对象是不是某种类型时使用的方法,也叫做鸭子判定法。简单的说,鸭子类型是指判断一只鸟是不是鸭子,我们只关心它游泳像不像鸭子、叫起来像不像鸭子、走路像不像鸭子就足够了。换言之,如果对象的行为跟我们的预期是一致的(能够接受某些消息),我们就认定它是某种类型的对象。
在Python语言中,有很多bytes-like对象(如:bytes
、bytearray
、array.array
、memoryview
)、file-like对象(如:StringIO
、BytesIO
、GzipFile
、socket
)、path-like对象(如:str
、bytes
),其中file-like对象都能支持read
和write
操作,可以像文件一样读写,这就是所谓的对象有鸭子的行为就可以判定为鸭子的判定方法。再比如Python中列表的extend
方法,它需要的参数并不一定要是列表,只要是可迭代对象就没有问题。
说明:动态语言的鸭子类型使得设计模式的应用被大大简化。
题目27:说一下Python中变量的作用域。
Python中有四种作用域,分别是局部作用域(Local)、嵌套作用域(Embedded)、全局作用域(Global)、内置作用域(Built-in),搜索一个标识符时,会按照LEGB的顺序进行搜索,如果所有的作用域中都没有找到这个标识符,就会引发NameError
异常。
题目28:说一下你对闭包的理解。
闭包是支持一等函数的编程语言(Python、JavaScript等)中实现词法绑定的一种技术。当捕捉闭包的时候,它的自由变量(在函数外部定义但在函数内部使用的变量)会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。简单的说,可以将闭包理解为能够读取其他函数内部变量的函数。正在情况下,函数的局部变量在函数调用结束之后就结束了生命周期,但是闭包使得局部变量的生命周期得到了延展。使用闭包的时候需要注意,闭包会使得函数中创建的对象不会被垃圾回收,可能会导致很大的内存开销,所以闭包一定不能滥用。
题目29:说一下Python中的多线程和多进程的应用场景和优缺点。
线程是操作系统分配CPU的基本单位,进程是操作系统分配内存的基本单位。通常我们运行的程序会包含一个或多个进程,而每个进程中又包含一个或多个线程。多线程的优点在于多个线程可以共享进程的内存空间,所以进程间的通信非常容易实现;但是如果使用官方的CPython解释器,多线程受制于GIL(全局解释器锁),并不能利用CPU的多核特性,这是一个很大的问题。使用多进程可以充分利用CPU的多核特性,但是进程间通信相对比较麻烦,需要使用IPC机制(管道、套接字等)。
多线程适合那些会花费大量时间在I/O操作上,但没有太多并行计算需求且不需占用太多内存的I/O密集型应用。多进程适合执行计算密集型任务(如:视频编码解码、数据处理、科学计算等)、可以分解为多个并行子任务并能合并子任务执行结果的任务以及在内存使用方面没有任何限制且不强依赖于I/O操作的任务。
扩展:Python中实现并发编程通常有多线程、多进程和异步编程三种选择。异步编程实现了协作式并发,通过多个相互协作的子程序的用户态切换,实现对CPU的高效利用,这种方式也是非常适合I/O密集型应用的。
题目30:说一下Python 2和Python 3的区别。
点评:这种问题千万不要背所谓的参考答案,说一些自己最熟悉的就足够了。
- Python 2中的
print
和exec
都是关键字,在Python 3中变成了函数。 - Python 3中没有
long
类型,整数都是int
类型。 - Python 2中的不等号
<>
在Python 3中被废弃,统一使用!=
。 - Python 2中的
xrange
函数在Python 3中被range
函数取代。 - Python 3对Python 2中不安全的
input
函数做出了改进,废弃了raw_input
函数。 - Python 2中的
file
函数被Python 3中的open
函数取代。 - Python 2中的
/
运算对于int
类型是整除,在Python 3中要用//
来做整除除法。 - Python 3中改进了Python 2捕获异常的代码,很明显Python 3的写法更合理。
- Python 3生成式中循环变量的作用域得到了更好的控制,不会影响到生成式之外的同名变量。
- Python 3中的
round
函数可以返回int
或float
类型,Python 2中的round
函数返回float
类型。 - Python 3的
str
类型是Unicode字符串,Python 2的str
类型是字节串,相当于Python 3中的bytes
。 - Python 3中的比较运算符必须比较同类对象。
- Python 3中定义类的都是新式类,Python 2中定义的类有新式类(显式继承自
object
的类)和旧式类(经典类)之分,新式类和旧式类在MRO问题上有非常显著的区别,新式类可以使用**class__
属性获取自身类型,新式类可以使用__slots**
魔法。 - Python 3对代码缩进的要求更加严格,如果混用空格和制表键会引发
TabError
。 - Python 3中字典的
keys
、values
、items
方法都不再返回list
对象,而是返回view object
,内置的map
、filter
等函数也不再返回list
对象,而是返回迭代器对象。 - Python 3标准库中某些模块的名字跟Python 2是有区别的;而在三方库方面,有些三方库只支持Python 2,有些只能支持Python 3。
题目31:谈谈你对“猴子补丁”(monkey patching)的理解。
“猴子补丁”是动态类型语言的一个特性,代码运行时在不修改源代码的前提下改变代码中的方法、属性、函数等以达到热补丁(hot patch)的效果。很多系统的安全补丁也是通过猴子补丁的方式来实现的,但实际开发中应该避免对猴子补丁的使用,以免造成代码行为不一致的问题。
在使用gevent
库的时候,我们会在代码开头的地方执行gevent.monkey.patch_all()
,这行代码的作用是把标准库中的socket
模块给替换掉,这样我们在使用socket
的时候,不用修改任何代码就可以实现对代码的协程化,达到提升性能的目的,这就是对猴子补丁的应用。
另外,如果希望用ujson
三方库替换掉标准库中的json
,也可以使用猴子补丁的方式,代码如下所示。
import json, ujsonjson.__name__ = 'ujson'
json.dumps = ujson.dumps
json.loads = ujson.loads
单元测试中的Mock
技术也是对猴子补丁的应用,Python中的unittest.mock
模块就是解决单元测试中用Mock
对象替代被测对象所依赖的对象的模块。
题目32:阅读下面的代码说出运行结果。
class A:def who(self):print('A', end='')class B(A):def who(self):super(B, self).who()print('B', end='')class C(A):def who(self):super(C, self).who()print('C', end='')class D(B, C):def who(self):super(D, self).who()print('D', end='')item = D()
item.who()
点评:这道题考查到了两个知识点:
- Python中的MRO(方法解析顺序)。在没有多重继承的情况下,向对象发出一个消息,如果对象没有对应的方法,那么向上(父类)搜索的顺序是非常清晰的。如果向上追溯到
object
类(所有类的父类)都没有找到对应的方法,那么将会引发AttributeError
异常。但是有多重继承尤其是出现菱形继承(钻石继承)的时候,向上追溯到底应该找到那个方法就得确定MRO。Python 3中的类以及Python 2中的新式类使用C3算法来确定MRO,它是一种类似于广度优先搜索的方法;Python 2中的旧式类(经典类)使用深度优先搜索来确定MRO。在搞不清楚MRO的情况下,可以使用类的mro
方法或**mro**
属性来获得类的MRO列表。super()
函数的使用。在使用super
函数时,可以通过super(类型, 对象)
来指定对哪个对象以哪个类为起点向上搜索父类方法。所以上面B
类代码中的super(B, self).who()
表示以B类为起点,向上搜索self
(D类对象)的who
方法,所以会找到C
类中的who
方法,因为D
类对象的MRO列表是D --> B --> C --> A --> object
。
ACBD
题目33:编写一个函数实现对逆波兰表达式求值,不能使用Python的内置函数。
点评:逆波兰表达式也称为“后缀表达式”,相较于平常我们使用的“中缀表达式”,逆波兰表达式不需要括号来确定运算的优先级,例如
5 * (2 + 3)
对应的逆波兰表达式是5 2 3 + *
。逆波兰表达式求值需要借助栈结构,扫描表达式遇到运算数就入栈,遇到运算符就出栈两个元素做运算,将运算结果入栈。表达式扫描结束后,栈中只有一个数,这个数就是最终的运算结果,直接出栈即可。
import operatorclass Stack:"""栈(FILO)"""def __init__(self):self.elems = []def push(self, elem):"""入栈"""self.elems.append(elem)def pop(self):"""出栈"""return self.elems.pop()@propertydef is_empty(self):"""检查栈是否为空"""return len(self.elems) == 0def eval_suffix(expr):"""逆波兰表达式求值"""operators = {'+': operator.add,'-': operator.sub,'*': operator.mul,'/': operator.truediv}stack = Stack()for item in expr.split():if item.isdigit():stack.push(float(item))else: num2 = stack.pop()num1 = stack.pop()stack.push(operators[item](num1, num2))return stack.pop()
题目34:Python中如何实现字符串替换操作?
Python中实现字符串替换大致有两类方法:字符串的replace
方法和正则表达式的sub
方法。
方法一:使用字符串的replace
方法。
message = 'hello, world!'
print(message.replace('o', 'O').replace('l', 'L').replace('he', 'HE'))
方法二:使用正则表达式的sub
方法。
import remessage = 'hello, world!'
pattern = re.compile('[aeiou]')
print(pattern.sub('#', message))
扩展:还有一个相关的面试题,对保存文件名的列表排序,要求文件名按照字母表和数字大小进行排序,例如对于列表
filenames = ['a12.txt', 'a8.txt', 'b10.txt', 'b2.txt', 'b19.txt', 'a3.txt']
,排序的结果是['a3.txt', 'a8.txt', 'a12.txt', 'b2.txt', 'b10.txt', 'b19.txt']
。提示一下,可以通过字符串替换的方式为文件名补位,根据补位后的文件名用sorted
函数来排序,大家可以思考下这个问题如何解决。
题目35:如何剖析Python代码的执行性能?
剖析代码性能可以使用Python标准库中的cProfile
和pstats
模块,cProfile
的run
函数可以执行代码并收集统计信息,创建出Stats
对象并打印简单的剖析报告。Stats
是pstats
模块中的类,它是一个统计对象。当然,也可以使用三方工具line_profiler
和memory_profiler
来剖析每一行代码耗费的时间和内存,这两个三方工具都会用非常友好的方式输出剖析结构。如果使用PyCharm,可以利用“Run”菜单的“Profile”菜单项对代码进行性能分析,PyCharm中可以用表格或者调用图(Call Graph)的方式来显示性能剖析的结果。
下面是使用cProfile
剖析代码性能的例子。
example.py
import cProfiledef is_prime(num):for factor in range(2, int(num ** 0.5) + 1):if num % factor == 0:return Falsereturn Trueclass PrimeIter:def __init__(self, total):self.counter = 0self.current = 1self.total = totaldef __iter__(self):return selfdef __next__(self):if self.counter < self.total:self.current += 1while not is_prime(self.current):self.current += 1self.counter += 1return self.currentraise StopIteration()cProfile.run('list(PrimeIter(10000))')
如果使用line_profiler
三方工具,可以直接剖析is_prime
函数每行代码的性能,需要给is_prime
函数添加一个profiler
装饰器,代码如下所示。
@profiler
def is_prime(num):for factor in range(2, int(num ** 0.5) + 1):if num % factor == 0:return Falsereturn True
安装line_profiler
。
pip install line_profiler
使用line_profiler
。
kernprof -lv example.py
运行结果如下所示。
Line # Hits Time Per Hit % Time Line Contents
==============================================================1 @profile2 def is_prime(num):3 86624 48420.0 0.6 50.5 for factor in range(2, int(num ** 0.5) + 1):4 85624 44000.0 0.5 45.9 if num % factor == 0:5 6918 3080.0 0.4 3.2 return False6 1000 430.0 0.4 0.4 return True
题目36:如何使用random
模块生成随机数、实现随机乱序和随机抽样?
点评:送人头的题目,因为Python标准库中的常用模块应该是Python开发者都比较熟悉的内容,这个问题回如果答不上来,整个面试基本也就砸锅了。
random.random()
函数可以生成[0.0, 1.0)
之间的随机浮点数。random.uniform(a, b)
函数可以生成[a, b]
或[b, a]
之间的随机浮点数。random.randint(a, b)
函数可以生成[a, b]
或[b, a]
之间的随机整数。random.shuffle(x)
函数可以实现对序列x
的原地随机乱序。random.choice(seq)
函数可以从非空序列中取出一个随机元素。random.choices(population, weights=None, *, cum_weights=None, k=1)
函数可以从总体中随机抽取(有放回抽样)出容量为k
的样本并返回样本的列表,可以通过参数指定个体的权重,如果没有指定权重,个体被选中的概率均等。random.sample(population, k)
函数可以从总体中随机抽取(无放回抽样)出容量为k
的样本并返回样本的列表。
扩展:
random
模块提供的函数除了生成均匀分布的随机数外,还可以生成其他分布的随机数,例如random.gauss(mu, sigma)
函数可以生成高斯分布(正态分布)的随机数;random.paretovariate(alpha)
函数会生成帕累托分布的随机数;random.gammavariate(alpha, beta)
函数会生成伽马分布的随机数。
题目37:解释一下线程池的工作原理。
点评:池化技术就是一种典型空间换时间的策略,我们使用的数据库连接池、线程池等都是池化技术的应用,Python标准库
currrent.futures
模块的ThreadPoolExecutor
就是线程池的实现,如果要弄清楚它的工作原理,可以参考下面的内容。
线程池是一种用于减少线程本身创建和销毁造成的开销的技术,属于典型的空间换时间操作。如果应用程序需要频繁的将任务派发到线程中执行,线程池就是必选项,因为创建和释放线程涉及到大量的系统底层操作,开销较大,如果能够在应用程序工作期间,将创建和释放线程的操作变成预创建和借还操作,将大大减少底层开销。线程池在应用程序启动后,立即创建一定数量的线程,放入空闲队列中。这些线程最开始都处于阻塞状态,不会消耗CPU资源,但会占用少量的内存空间。当任务到来后,从队列中取出一个空闲线程,把任务派发到这个线程中运行,并将该线程标记为已占用。当线程池中所有的线程都被占用后,可以选择自动创建一定数量的新线程,用于处理更多的任务,也可以选择让任务排队等待直到有空闲的线程可用。在任务执行完毕后,线程并不退出结束,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程长时间处于闲置状态时,线程池可以自动销毁一部分线程,回收系统资源。基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小。
一般线程池都必须具备下面几个组成部分:
- 线程池管理器:用于创建并管理线程池。
- 工作线程和线程队列:线程池中实际执行的线程以及保存这些线程的容器。
- 任务接口:将线程执行的任务抽象出来,形成任务接口,确保线程池与具体的任务无关。
- 任务队列:线程池中保存等待被执行的任务的容器。
题目38:举例说明什么情况下会出现KeyError
、TypeError
、ValueError
。
举一个简单的例子,变量a
是一个字典,执行int(a['x'])
这个操作就有可能引发上述三种类型的异常。如果字典中没有键x
,会引发KeyError
;如果键x
对应的值不是str
、float
、int
、bool
以及bytes-like
类型,在调用int
函数构造int
类型的对象时,会引发TypeError
;如果a[x]
是一个字符串或者字节串,而对应的内容又无法处理成int
时,将引发ValueError
。
题目39:说出下面代码的运行结果。
def extend_list(val, items=[]):items.append(val)return itemslist1 = extend_list(10)
list2 = extend_list(123, [])
list3 = extend_list('a')
print(list1)
print(list2)
print(list3)
点评:Python函数在定义的时候,默认参数
items
的值就被计算出来了,即[]
。因为默认参数items
引用了对象[]
,每次调用该函数,如果对items
引用的列表进行了操作,下次调用时,默认参数还是引用之前的那个列表而不是重新赋值为[]
,所以列表中会有之前添加的元素。如果通过传参的方式为items
重新赋值,那么items
将引用到新的列表对象,而不再引用默认的那个列表对象。这个题在面试中经常被问到,通常不建议使用容器类型的默认参数,像PyLint这样的代码检查工具也会对这种代码提出质疑和警告。
[10, 'a']
[123]
[10, 'a']
题目40:如何读取大文件,例如内存只有4G,如何读取一个大小为8G的文件?
很显然4G内存要一次性的加载大小为8G的文件是不现实的,遇到这种情况必须要考虑多次读取和分批次处理。在Python中读取文件可以先通过open
函数获取文件对象,在读取文件时,可以通过read
方法的size
参数指定读取的大小,也可以通过seek
方法的offset
参数指定读取的位置,这样就可以控制单次读取数据的字节数和总字节数。除此之外,可以使用内置函数iter
将文件对象处理成迭代器对象,每次只读取少量的数据进行处理,代码大致写法如下所示。
with open('...', 'rb') as file:for data in iter(lambda: file.read(2097152), b''):pass
在Linux系统上,可以通过split
命令将大文件切割为小片,然后通过读取切割后的小文件对数据进行处理。例如下面的命令将名为filename
的大文件切割为大小为512M的多个文件。
split -b 512m filename
如果愿意, 也可以将名为filename
的文件切割为10个文件,命令如下所示。
split -n 10 filename
扩展:外部排序跟上述的情况非常类似,由于处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。“排序-归并算法”就是一种常用的外部排序策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件,然后在归并阶段将这些临时文件组合为一个大的有序文件,这个大的有序文件就是排序的结果。
题目41:说一下你对Python中模块和包的理解。
每个Python文件就是一个模块,而保存这些文件的文件夹就是一个包,但是这个作为Python包的文件夹必须要有一个名为__init__.py
的文件,否则无法导入这个包。通常一个文件夹下还可以有子文件夹,这也就意味着一个包下还可以有子包,子包中的__init__.py
并不是必须的。模块和包解决了Python中命名冲突的问题,不同的包下可以有同名的模块,不同的模块下可以有同名的变量、函数或类。在Python中可以使用import
或from ... import ...
来导入包和模块,在导入的时候还可以使用as
关键字对包、模块、类、函数、变量等进行别名,从而彻底解决编程中尤其是多人协作团队开发时的命名冲突问题。
题目42:说一下你知道的Python编码规范。
点评:企业的Python编码规范基本上是参照PEP-8或谷歌开源项目风格指南来制定的,后者还提到了可以使用Lint工具来检查代码的规范程度,面试的时候遇到这类问题,可以先说下这两个参照标准,然后挑重点说一下Python编码的注意事项。
- 空格的使用
- 使用空格来表示缩进而不要用制表符(Tab)。
- 和语法相关的每一层缩进都用4个空格来表示。
- 每行的字符数不要超过79个字符,如果表达式因太长而占据了多行,除了首行之外的其余各行都应该在正常的缩进宽度上再加上4个空格。
- 函数和类的定义,代码前后都要用两个空行进行分隔。
- 在同一个类中,各个方法之间应该用一个空行进行分隔。
- 二元运算符的左右两侧应该保留一个空格,而且只要一个空格就好。
- 标识符命名
- 变量、函数和属性应该使用小写字母来拼写,如果有多个单词就使用下划线进行连接。
- 类中受保护的实例属性,应该以一个下划线开头。
- 类中私有的实例属性,应该以两个下划线开头。
- 类和异常的命名,应该每个单词首字母大写。
- 模块级别的常量,应该采用全大写字母,如果有多个单词就用下划线进行连接。
- 类的实例方法,应该把第一个参数命名为
self
以表示对象自身。 - 类的类方法,应该把第一个参数命名为
cls
以表示该类自身。
- 表达式和语句
- 采用内联形式的否定词,而不要把否定词放在整个表达式的前面。例如:
if a is not b
就比if not a is b
更容易让人理解。 - 不要用检查长度的方式来判断字符串、列表等是否为
None
或者没有元素,应该用if not x
这样的写法来检查它。 - 就算
if
分支、for
循环、except
异常捕获等中只有一行代码,也不要将代码和if
、for
、except
等写在一起,分开写才会让代码更清晰。 import
语句总是放在文件开头的地方。- 引入模块的时候,
from math import sqrt
比import math
更好。 - 如果有多个
import
语句,应该将其分为三部分,从上到下分别是Python标准模块、第三方模块和自定义模块,每个部分内部应该按照模块名称的字母表顺序来排列。
题目43:运行下面的代码是否会报错,如果报错请说明哪里有什么样的错,如果不报错请说出代码的执行结果。
class A: def __init__(self, value):self.__value = value@propertydef value(self):return self.__valueobj = A(1)
obj.__value = 2
print(obj.value)
print(obj.__value)
点评:这道题有两个考察点,一个考察点是对
_
和__
开头的对象属性访问权限以及@property
装饰器的了解,另外一个考察的点是对动态语言的理解,不需要过多的解释。
1
2
扩展:如果不希望代码运行时动态的给对象添加新属性,可以在定义类时使用
__slots__
魔法。例如,我们可以在上面的A
中添加一行__slots__ = ('__value', )
,再次运行上面的代码,将会在原来的第10行处产生AttributeError
错误。
题目44:对下面给出的字典按值从大到小对键进行排序。
prices = {'AAPL': 191.88,'GOOG': 1186.96,'IBM': 149.24,'ORCL': 48.44,'ACN': 166.89,'FB': 208.09,'SYMC': 21.29
}
点评:
sorted
函数的高阶用法在面试的时候经常出现,key
参数可以传入一个函数名或一个Lambda函数,该函数的返回值代表了在排序时比较元素的依据。
sorted(prices, key=lambda x: prices[x], reverse=True)
题目45:说一下namedtuple
的用法和作用。
点评:Python标准库的
collections
模块提供了很多有用的数据结构,这些内容并不是每个开发者都清楚,就比如题目问到的namedtuple
,在我参加过的面试中,90%的面试者都不能准确的说出它的作用和应用场景。此外,deque
也是一个非常有用但又经常被忽视的类,还有Counter
、OrderedDict
、defaultdict
、UserDict
等类,大家清楚它们的用法吗?
在使用面向对象编程语言的时候,定义类是最常见的一件事情,有的时候,我们会用到只有属性没有方法的类,这种类的对象通常只用于组织数据,并不能接收消息,所以我们把这种类称为数据类或者退化的类,就像C语言中的结构体那样。我们并不建议使用这种退化的类,在Python中可以用namedtuple
(命名元组)来替代这种类。
from collections import namedtupleCard = namedtuple('Card', ('suite', 'face'))
card1 = Card('红桃', 13)
card2 = Card('草花', 5)
print(f'{card1.suite}{card1.face}')
print(f'{card2.suite}{card2.face}')
命名元组与普通元组一样是不可变容器,一旦将数据存储在namedtuple
的顶层属性中,数据就不能再修改了,也就意味着对象上的所有属性都遵循“一次写入,多次读取”的原则。和普通元组不同的是,命名元组中的数据有访问名称,可以通过名称而不是索引来获取保存的数据,不仅在操作上更加简单,代码的可读性也会更好。
命名元组的本质就是一个类,所以它还可以作为父类创建子类。除此之外,命名元组内置了一系列的方法,例如,可以通过_asdict
方法将命名元组处理成字典,也可以通过_replace
方法创建命名元组对象的浅拷贝。
class MyCard(Card):def show(self):faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']return f'{self.suite}{faces[self.face]}'print(Card) # <class '__main__.Card'>
card3 = MyCard('方块', 12)
print(card3.show()) # 方块Q
print(dict(card1._asdict())) # {'suite': '红桃', 'face': 13}
print(card2._replace(suite='方块')) # Card(suite='方块', face=5)
总而言之,命名元组能更好的组织数据结构,让代码更加清晰和可读,在很多场景下是元组、字典和数据类的替代品。在需要创建占用空间更少的不可变类时,命名元组就是很好的选择。
题目46:按照题目要求写出对应的函数。
要求:写一个函数,传入一个有若干个整数的列表,该列表中某个元素出现的次数超过了50%,返回这个元素。
def more_than_half(items):temp, times = None, 0for item in items:if times == 0:temp = itemtimes += 1else:if item == temp:times += 1else:times -= 1return temp
点评:LeetCode上的题目,在Python面试中出现过,利用元素出现次数超过了50%这一特征,出现和
temp
相同的元素就将计数值加1,出现和temp
不同的元素就将计数值减1。如果计数值为0
,说明之前出现的元素已经对最终的结果没有影响,用temp
记下当前元素并将计数值置为1
。最终,出现次数超过了50%的这个元素一定会被赋值给变量temp
。
题目47:按照题目要求写出对应的函数。
要求:写一个函数,传入的参数是一个列表(列表中的元素可能也是一个列表),返回该列表最大的嵌套深度。例如:列表
[1, 2, 3]
的嵌套深度为1
,列表[[1], [2, [3]]]
的嵌套深度为3
。
def list_depth(items):if isinstance(items, list):max_depth = 1for item in items:max_depth = max(list_depth(item) + 1, max_depth)return max_depthreturn 0
点评:看到题目应该能够比较自然的想到使用递归的方式检查列表中的每个元素。
题目48:按照题目要求写出对应的装饰器。
要求:有一个通过网络获取数据的函数(可能会因为网络原因出现异常),写一个装饰器让这个函数在出现指定异常时可以重试指定的次数,并在每次重试之前随机延迟一段时间,最长延迟时间可以通过参数进行控制。
方法一:
from functools import wraps
from random import random
from time import sleepdef retry(*, retry_times=3, max_wait_secs=5, errors=(Exception, )):def decorate(func):@wraps(func)def wrapper(*args, **kwargs):for _ in range(retry_times):try:return func(*args, **kwargs)except errors:sleep(random() * max_wait_secs)return Nonereturn wrapperreturn decorate
方法二:
from functools import wraps
from random import random
from time import sleepclass Retry(object):def __init__(self, *, retry_times=3, max_wait_secs=5, errors=(Exception, )):self.retry_times = retry_timesself.max_wait_secs = max_wait_secsself.errors = errorsdef __call__(self, func):@wraps(func)def wrapper(*args, **kwargs):for _ in range(self.retry_times):try:return func(*args, **kwargs)except self.errors:sleep(random() * self.max_wait_secs)return Nonereturn wrapper
点评:我们不止一次强调过,装饰器几乎是Python面试必问内容,这个题目比之前的题目稍微复杂一些,它需要的是一个参数化的装饰器。
题目49:写一个函数实现字符串反转,尽可能写出你知道的所有方法。
点评:烂大街的题目,基本上算是送人头的题目。
方法一:反向切片
def reverse_string(content):return content[::-1]
方法二:反转拼接
def reverse_string(content):return ''.join(reversed(content))
方法三:递归调用
def reverse_string(content):if len(content) <= 1:return contentreturn reverse_string(content[1:]) + content[0]
方法四:双端队列
from collections import dequedef reverse_string(content):q = deque()q.extendleft(content)return ''.join(q)
方法五:反向组装
from io import StringIOdef reverse_string(content):buffer = StringIO()for i in range(len(content) - 1, -1, -1):buffer.write(content[i])return buffer.getvalue()
方法六:反转拼接
def reverse_string(content):return ''.join([content[i] for i in range(len(content) - 1, -1, -1)])
方法七:半截交换
def reverse_string(content):length, content= len(content), list(content)for i in range(length // 2):content[i], content[length - 1 - i] = content[length - 1 - i], content[i]return ''.join(content)
方法八:对位交换
def reverse_string(content):length, content= len(content), list(content)for i, j in zip(range(length // 2), range(length - 1, length // 2 - 1, -1)):content[i], content[j] = content[j], content[i]return ''.join(content)
扩展:这些方法其实都是大同小异的,面试的时候能够给出几种有代表性的就足够了。给大家留一个思考题,上面这些方法,哪些做法的性能较好呢?我们之前提到过剖析代码性能的方法,大家可以用这些方法来检验下你给出的答案是否正确。
题目50:按照题目要求写出对应的函数。
要求:列表中有
1000000
个元素,取值范围是[1000, 10000)
,设计一个函数找出列表中的重复元素。
def find_dup(items: list):dups = [0] * 9000for item in items:dups[item - 1000] += 1for idx, val in enumerate(dups):if val > 1:yield idx + 1000
点评:这道题的解法和计数排序的原理一致,虽然元素的数量非常多,但是取值范围
[1000, 10000)
并不是很大,只有9000个可能的取值,所以可以用一个能够保存9000个元素的dups
列表来记录每个元素出现的次数,dups
列表所有元素的初始值都是0
,通过对items
列表中元素的遍历,当出现某个元素时,将dups
列表对应位置的值加1,最后dups
列表中值大于1的元素对应的就是items
列表中重复出现过的元素。
sleep(random() * max_wait_secs)return Nonereturn wrapperreturn decorate
方法二:```python
from functools import wraps
from random import random
from time import sleepclass Retry(object):def __init__(self, *, retry_times=3, max_wait_secs=5, errors=(Exception, )):self.retry_times = retry_timesself.max_wait_secs = max_wait_secsself.errors = errorsdef __call__(self, func):@wraps(func)def wrapper(*args, **kwargs):for _ in range(self.retry_times):try:return func(*args, **kwargs)except self.errors:sleep(random() * self.max_wait_secs)return Nonereturn wrapper
点评:我们不止一次强调过,装饰器几乎是Python面试必问内容,这个题目比之前的题目稍微复杂一些,它需要的是一个参数化的装饰器。
题目49:写一个函数实现字符串反转,尽可能写出你知道的所有方法。
点评:烂大街的题目,基本上算是送人头的题目。
方法一:反向切片
def reverse_string(content):return content[::-1]
方法二:反转拼接
def reverse_string(content):return ''.join(reversed(content))
方法三:递归调用
def reverse_string(content):if len(content) <= 1:return contentreturn reverse_string(content[1:]) + content[0]
方法四:双端队列
from collections import dequedef reverse_string(content):q = deque()q.extendleft(content)return ''.join(q)
方法五:反向组装
from io import StringIOdef reverse_string(content):buffer = StringIO()for i in range(len(content) - 1, -1, -1):buffer.write(content[i])return buffer.getvalue()
方法六:反转拼接
def reverse_string(content):return ''.join([content[i] for i in range(len(content) - 1, -1, -1)])
方法七:半截交换
def reverse_string(content):length, content= len(content), list(content)for i in range(length // 2):content[i], content[length - 1 - i] = content[length - 1 - i], content[i]return ''.join(content)
方法八:对位交换
def reverse_string(content):length, content= len(content), list(content)for i, j in zip(range(length // 2), range(length - 1, length // 2 - 1, -1)):content[i], content[j] = content[j], content[i]return ''.join(content)
扩展:这些方法其实都是大同小异的,面试的时候能够给出几种有代表性的就足够了。给大家留一个思考题,上面这些方法,哪些做法的性能较好呢?我们之前提到过剖析代码性能的方法,大家可以用这些方法来检验下你给出的答案是否正确。
题目50:按照题目要求写出对应的函数。
要求:列表中有
1000000
个元素,取值范围是[1000, 10000)
,设计一个函数找出列表中的重复元素。
def find_dup(items: list):dups = [0] * 9000for item in items:dups[item - 1000] += 1for idx, val in enumerate(dups):if val > 1:yield idx + 1000
点评:这道题的解法和计数排序的原理一致,虽然元素的数量非常多,但是取值范围
[1000, 10000)
并不是很大,只有9000个可能的取值,所以可以用一个能够保存9000个元素的dups
列表来记录每个元素出现的次数,dups
列表所有元素的初始值都是0
,通过对items
列表中元素的遍历,当出现某个元素时,将dups
列表对应位置的值加1,最后dups
列表中值大于1的元素对应的就是items
列表中重复出现过的元素。
Python入门到放弃相关推荐
- python入门到放弃篇46绘制几何图形
昨天,因为有时间.所以,久违地python编程一波.突然有一个新奇的想法,然后去实践了一下,又烧死了不少脑细胞,不过已经习惯了. 废话不多说,今天的代码有点小多,希望各位客官能够满意. 代码1: #随 ...
- python入门到放弃恶搞图-《Python3从入门到放弃》视频教程
<Python3从入门到放弃>视频教程2016年 001.01.python编程环境配置 001.02.IDE的又一选择PyCharm 002.01.name 002.02.namespa ...
- python入门到放弃恶搞图-学Python方法用错,直接从入门到放弃!
原标题:学Python方法用错,直接从入门到放弃! 从你开始学习编程的那一刻起,就注定了以后所要走的路-从编程学习者开始,依次经历实习生.程序员.软件工程师.架构师.CTO等职位的磨砺:当你站在职位顶 ...
- Python入门到放弃系列一
目录 初衷 基本概念 前言 第一部分:基础知识 第一章 起步 安装&配置部分 第二章 变量和简单数据类型 注释学习 代码自动补全 初衷 考虑到很多的地方都说要会python,shell,之类的 ...
- 从python入门到放弃_《Python3从入门到放弃》视频教程
" A! l% N+ q8 t% F& B9 u 001.01.python编程环境配置 001.02.IDE的又一选择PyCharm 002.01.name 002.02.name ...
- 从python入门到放弃_Python从入门到放弃?方法不对一切白费
从你开始学习编程的那一刻起,就注定了以后所要走的路 -从编程学习者开始,依次经历实习生.程序员.软件工程师.架构师. CT O 等职位的磨砺:当你站在职位顶 峰的位置蓦然回首时,会发现自己的成功并不是 ...
- Python入门到放弃(一)
介绍python和库文件管理 python是解释型语言 Python的特点:简单.易学.速度快.免费开源.高层语言.可移植性.解释性.面向对象(也支持面向过程).可扩展性.可嵌入性.丰富的库.规范的代 ...
- 多看一眼多进步,python入门到放弃
python相关工具都安装完成后,就可以开始学习了,以下在pycharm中,以下学习内容来自b站边学习边整理的笔记,好记性不如赖笔头,多总结多记录,总是不错的 print()函数的使用 print函数 ...
- python ** 运算符_Python从入门到放弃运算符(2)
摘要:上一篇Python从入门到放弃-运算符(1),讲了Python的运算符中的算术运算符.赋值运算符.比较(关系)运算符,这篇继续讲Python的运算符. 逻辑运算符 逻辑运算符是对真和假两种布尔值 ...
- python从入门到放弃系列恶搞短片-太惨!学Python方法用错,直接从入门到放弃!...
原标题:太惨!学Python方法用错,直接从入门到放弃! 从你开始学习编程的那一刻起,就注定了以后所要走的路-从编程学习者开始,依次经历实习生.程序员.软件工程师.架构师.CTO等职位的磨砺:当你站在 ...
最新文章
- java 网络实验_Java实验五网络编程与安全
- webrender 查看是否开启_想要体验极致顺滑的网页加载体验?手动开启Firefox WebRender渲染引擎...
- Django(五)中间件
- JavaScript算法(实例三)数组排序--冒泡排序 / sort() / reverse() / 比较函数
- android网络请求线程池,利用线程池实现Android客户端的http网络数据请求工具类
- datatable???
- 升级AndroidStudio3.4问题汇总
- 第一次作业--四则运算
- 为什么 Kubernetes 是微服务发展的必然产物?
- appinventor如何做个游戏_单亮:游戏的重要性
- python ffmpeg直播_python+ffmpeg视频并发直播压力测试
- php动态网站实训心得体会,十天学会写PHP动态网站
- 大话设计模式之爱你一万年:第一章 设计模式基本概念:1.软件设计模式概述
- spring手动控制事务开启_手动控制spring事务
- 火灾检测参考资料与数据集
- JPA实体中数据库生成ID的最终指南1
- AI新动向:多模态+自监督!Meta AI一次搞定语音、视觉和文本三个SOTA
- 如何下载建外街道卫星地图高清版大图
- FreeEIM 全排列算法原理和实现
- ElementUI 的组件 Switch(开关)如何让文字显示在按钮上