第三章 函数基础及进阶

3.1 上章补充内容

3.1.1 Bytes类型

计算机的数据需要存到硬盘上,但是硬盘只能存储二进制的数据。

我们知道将计算机里的数据转换成我们能看懂的数据是将二进制 -> 十进制(ASCII、gbk、utf-8等),那存储就是反过来的过程。

文字 -> utf-8、GBK -> 2进制

图片 -> png、jpg -> 2进制

音乐 -> mp3、wav -> 2进制

视频 -> mp4、mov -> 2进制

Python中encode()可以用来编码(解码是decode()),比如我们将'朱明'通过utf-8编码('朱明'.encode('utf-8')),得到一串16进制的数据(b'\xe6\x9c\xb1\xe6\x98\x8e'),那这里的\xe5就是一个字节(前面的b就表示是bytes类型),总共6个字节,我们前面说过utf-8中一个中文字符占三个字节,这里也符合这一规定。

那我们就可以来定义bytes类型了。bytes类型,以16进制形式表示,2个16进制数构成一个byte,以b''来表示。

那么问题来了,之前我们open()操作文件的时候都没有涉及到二进制,这不是相互矛盾么?

其实open()函数是默认帮我们进制转换,我们可以通过encoding='..'来设置编码的方式,Python3默认的编码是encoding='utf-8'(pycharm加载文件的编码也是utf-8,有时候查看文件也可能会产生乱码),我们有时产生文件乱码的情况,很大可能就是编码混乱的问题。

问题又来了,我们如果进行两次编码,会产生什么结果?

f = open('test.txt', 'w', encoding='utf-8')
f.write('朱明'.encode('utf-8'))
f.close()
# 报错TypeError: write()参数类型不支持bytes,必须是str

那么Python的文件打开形式又给我们提供了三个文件打开方法:wbrbab。都是以二进制来写、读、追加。注意,这时open()encoding=''就禁止传入参数了(会抛出ValueError),后面写入文件的数据必须是bytes类型的数据了,可以同时使用不同的编码写入,python不会报错,但是你查看文件的时候,解释器会显示乱码的。

f = open('test.txt', 'wb')
f.write('朱明'.encode('utf-8'))
# f.write('朱明'.encode('gbk'))
f.close()

bytes有两个场景:字符、图片、视频等数据存硬盘的时候,数据需要变成bytes(上面说的就是这个场景);网络传输数据时,数据也需要变成bytes类型,才可以进行传输,这点在讲网络编程时我们再说。

3.1.2 深浅copy

列表、字典、集合三个数据类型都有copy()的方法,我们以字典举例

# 首先我们先定义一个数据集
data = {
    "name": "zm",
    "age": 18,
    "scores":{
        "语文": 110,
        "高数": 130,
        "英语": 98,
    },
}

我们复制一个新的a = data(按照前面介绍的字符数字等数据类型复制的原理,我们类比理解一下),adataid()都一样,那我们修改一下原来的data数据a应该不会改变。我们data['name'] = 'wzw',输出a和data,我们发现==两个值都发生了变化==。

那我们来剖析一下原理,字典、列表、集合这些数据类型是一个容器(容器有一个地址),里面的数据也分别有自己的地址(和容器地址没关联),所以我们修改容器里面的值,adata两个指针指向都没变,还是指向容器地址,共享同一份数据。二字符串那些不可变数据类型修改一下值就会创建一个新的数据,所以有指针的指向会发生变化。

那特定情况下,比如数据备份,我们不想让adata同时改变,那就用到了copy()方法。我们b = data.copy(),这时候b就是新的数据容器了,具体可以自己修改值试试。

But,当我们修改第二层以及更深层数据时,我们发现copy()出来的数据和原来的数据还是会一起被修改。原因和上面类似,我们copy()出来的新字典中嵌套的子字典存放的又是容器地址,这样我们改容器里面的容器里面的值就还是会和原来的值同步修改。

如果我们直接修改容器地址那就不会出现这样的情况:

b['scores'] = 22
print(b['scores'])	# 22
print(data['scores'])	# {'语文': 110, '高数': 130, '英语': 98, '数学': 130}

同理,我们可以推导理解三层嵌套结构,四层嵌套结构…等等等等。

那我们就知道了,copy()只能真正copy()一层,我们称为浅copy()

如果我们想完完全全的copy()嵌套数据,我们就需要使用copy模块deepcopy()

import copy
c = data.deepcopy()		# 完全深copy

PS:深copy比较占内存,不常用,但是在特定的场景下需要用到,所以必须得知道这个东西和怎么深copy

3.1.3 编码转换

编码与解码

其实前面多多少少也提到了,我们这里就再简单总结说下。

Python中编码的方法就是encode(),解码的方法是decode(),编码出来的东西是给机器看的,返回的是bytes类型;而解码出来的是我们人能看懂的类型,象数字、字符、列表等等数据类型。

print('朱明'.encode('utf-8'))		# 相应的16进制码
print('朱明'.encode('utf-8').decode('gbk'))	# 乱码
print('朱明'.encode('utf-8').decode('utf-8'))		# 朱明

decode()参数不写时,默认使用Python3默认的编码格式(utf-8),建议还是写一下,不然有时候会不好进行后期的debug

编码转换

那问题来了,我们想把utf-8编码格式的数据转换成gbk编码格式的数据,比如我们要把Windows文件发到Linux上或者Mac上,这时候就会产生乱码,因为Windows的文件解码方式默认是gbk,Linux和Mac的文件解码方式默认是utf-8,当然word那些应用会自动帮我们解决这个问题,如果用记事本这些软件,那就会产生乱码。那我们就需要进行编码的转换(或者利用有的软件有修改编码的接口来解决这一问题)。

那我们就必须要有utf-8和gbk的映射关系,那就想到了我们Unicode万国码。我们就可以通过utf-8 -> unicode -> gbk来实现编码的转换。那我们底层程序来实现就要通过先二进制读取文件数据,然后解码,再用另一种格式编码,最后将存入新文件。

f = open('test_utf8.txt', 'rb', encoding='utf-8')
data = f.read()
print(type(data))
f.close()
# 先将utf-8编码解码成Unicode,再编码成gbk
data_gbk = data.decode('utf-8').encode('gbk')

f_new = open('test_gbk.txt', 'wb', encoding='gbk')
f_new.write(data_gbk)
f_new.close()

3.2 函数基础

3.2.1 函数定义及特性

函数的作用

函数最大的作用就是减少重复的代码,将需要重复的代码封装成函数,供其他不同代码调用;而且如果实现功能后期需要修改的话,只要修改函数就可以了,不需要修改每个模块的代码,减少了后期维护的麻烦(易维护),增加了程序的可扩展性。

函数的定义语法

Python中称函数为function,Java中为method

def 函数名(各种参数):
    # 函数的功能
    pass
	# 函数可以有返回值,也可以没有返回值
    # return ...
    # return不但可以用来返回结果,也代表函数结束的语句,类似循环的break
注意的点
  • 函数名我们建议小写
  • _来分开几个单词,比如学生人数计数:count_stu()
  • 函数分有返回值(return)的和无返回值的两种。函数外部的代码想获取函数的执行结果,就可以用return语句来把函数结果返回。
  • 函数执行过程中,只要执行到return语句,就会停止执行,并返回结果。
  • 无返回值的函数,默认return None

3.2.2 函数的参数类型

形参和实参
# 这里的x和y就属于形参
def calc(x, y):
    res = x**y
    return res

# 这里的c就属于实参
c = calc(a, b)
print(c)

形参只有**在被调用时才分配内存单元,调用结束之后就释放。可以说,形参只在函数内部有效**,函数调用结束返回后,就不能再使用该形参变量。

实参就可以是常量、变量、表达式等等形式的参数,他们必须要有确定的值,以便调用函数时,把值传给形参

位置参数

拿上面的calc(x, y)函数为例,其中的x和y就是calc(x, y)的两个位置参数,如果我们calc(1),少传入一个参数,python就会报错少一个positional argument。

函数的开发规范

每个函数下面都需要加上函数每个参数的意思和需要传入的参数类型等信息,Pycharm中,我们可以打三个双引号,然后回车就会自动生成,然后我们就可以在每行冒号后面写上参数的信息,生成类似如下代码的样子。

def register(name, age, major, country):
    """
    :param name:str     用户姓名
    :param age:int      用户年龄
    :param major:str    用户学科
    :param country:str  用户国籍
    :return:
    """
    info = f"""
    --------------- 你的注册信息 --------------
    name:{name}
    age:{age}
    major:{major}
    country:{country}
    """
    print(info)


register('zm', 18, 'math', 'China')
默认参数

那我们设想一个情况,该程序大多数都是中国国内的人使用,所以country这个位置参数就大概率是China,我们就可以采用默认参数,然后设置成China,然后你不传入这个位置的参数就是默认country是China,传入就用你传入的信息。

def register(name, age, major, country='China'):
    """
    :param name:str     用户姓名
    :param age:int      用户年龄
    :param major:str    用户学科
    :param country:str  用户国籍
    :return:
    """
    info = f"""
    --------------- 你的注册信息 --------------
    name:{name}
    age:{age}
    major:{major}
    country:{country}
    """
    print(info)


register('zm', 18, 'math')	# country值为China
register('wzw', 18, 'math', 'US')	# country值为US

不过,需要注意的是:默认参数必须写在函数参数的最末端,多个默认参数也是一样,都放在后面就好,比如calc(x, y, z='1', b='2')。不然就会报错SyntaxError: non-default argument follows default argument

关键参数

通过上面的函数参数的传入,我们可以知道,函数的传参必须按顺序传入,不然本来赋给name的参数赋给了age,那就会导致函数后续的功能产生一些错误,导致最后的结果错误。

那我们可以使用关键参数来乱序传参。

def register(name, age, major, country='China'):
    """
    :param name:str     用户姓名
    :param age:int      用户年龄
    :param major:str    用户学科
    :param country:str  用户国籍
    :return:
    """
    info = f"""
    --------------- 你的注册信息 --------------
    name:{name}
    age:{age}
    major:{major}
    country:{country}
    """
    print(info)


register(major='math', age=18, name='wzw')	# 没有出现乱序

那我们可以关键参数和位置参数混合传入参数么?

答案是可以,不过有两个需要注意的点:

  1. 调用函数传参时,位置参数必须在关键参数的前面,比如register(major='math', 18, name='wzw')就·会报错SyntaxError: positional argument follows keyword argument
  2. 很容易产生参数传入错误。比如register('zm', major='math', age=18, name='wzw'),运行时,Python就会报错TypeError: register() got multiple values for argument 'name',意思就是有多个参数,传入了name这个位置。

所以我们建议尽量用一种,不要混在一起使用,既对自己写代码造成一些困扰,又给你后期维护带来很多不便。

非固定参数

现在我们需要写一个累加的函数,简单一点,我们规定传入的参数必须是整形数字,返回所以传入参数的累加值。

def num_sum(a, b):
    return a+b

print(num_sum(1, 2))	# 3

这个函数只能支持两个参数,不能灵活的接收多个不固定的参数,那我们就可以这样写:

def num_sum(*args):
    return sum(args)


print(num_sum(1, 1, 1, 1, 1, 1, 1, 1))	# 8

我们在函数最后面(存在默认参数的话,就写在默认参数的后面),添加*args或者**kwargsargs就是arguments的简写)

*args***kwargs*的区别是什么呢?

我们可以输出一下

def name(*args, **kwargs):
    print(args, kwargs)
    
name()		# 输出结果:() {}

我们可以看出,一个是元组形式,一个是字典形式。元组形式的用来接受多个位置参数的传入;字典形式的用来接受多个关键参数的传入。

那有人可能会问,我把上面非固定参数、位置参数、关键参数可以一起用吗?

答案是可以,不过还是一样的问题,那就是参数传入的混乱,所以实际开发需要尽量避免混合使用,而且出现函数必须使用混合使用不同类型参数的情况几乎没有,有的话,也是简单的混合。

3.2.3 函数引出的变量问题

name = 'zm'
def change():
    name = 'wzw'
    print(name)
    
def change_2():
    print(name)
    
change()	# 输出wzw
print(name)	# 输出zm -> change()没有改变第一个name的值
change_2()	# 输出zm

# 为什么change()没有改变name一开始的值(name)呢?
# 因为第一个name和change()内部自己定义的name不是一个变量。一个是全部代码都可以调用的全局变量,另一个是函数内被的局部变量。Python规定函数是不能直接改变全局变量的。
全部变量

能够被全部的代码调用的变量就是全局变量。上面代码中的第一个name就是全局变量。

局部变量

函数中定义的变量就是局部变量,只能供所在函数内部调用。函数内部变量的查找顺序是**局部变量>全局变量**,所以change()输出自己的局部变量namechange_2()因为找不到自己有name这个局部变量,才输出全局变量name的。

在函数中修改全局变量

global声明就好了

name = 'zm'
def change():
    global name	# 声明函数内部的name是全局变量name
    name = 'wzw'
    print(name)

print(name)	# 输出wzw,而不是zm了

但不建议使用,因为后期维护的话,不好找全局变量,不符合开发规范。

查看两种变量的方法

python有两个内置方法:locals()globals(),分别可以查看局部变量和全局变量。局部变量返回的是调用位置的局部变量,而不是全部函数的局部变量。全局变量不管在哪调用都是返回全部的全局变量。

3.2.4 函数是否能修改可变变量

names = ['zm', 'wzw']
def change():
    names.append('ghd')
    # names = ['zm']	重新定义names则会报错,比不可变的全局变量更激进(不可变变量可以同时存在局部变量和全局变量(比如外面的name和函数内部的name);而可变变量不能同时存在局部变量和全局变量(外面有了names,函数里面再定义一个names就报错))
print(names)

通过上面的代码,我们可以看出,函数是可以改变可变变量(列表、字典、集合等等)的。

原理就是第一章我们讲到的变量的原理,只是我们从字符串和数字的不可变的变量延伸到我们这里的可变的变量(listdictset等数据类型)。函数获取到的names只是容器的地址,函数不可以修改这个容器(比如names = ['zm']就会报错);但是函数是可以修改容器里面的东西(包括元素、键值对在容器内的东西)的,比如函数里面的names.append('ghd')就可以在全局变量names里面增加元素。

3.2.5 嵌套&匿名&高阶函数

嵌套函数
name = 'ghd'
def change():
    name = 'zm'
    
    def change2():
        name = 'wzw'
        print('第三层打印'+name)
    change2()
    print('第二层打印'+name)
print('最外层(第一层)'+name)

# 输出:
# 第三层打印wzw
# 第二层打印zm
# 最外层(第一层)ghd

等到后面的装饰器就会用到嵌套函数的知识。

匿名函数

匿名函数就是不起名字的函数,像上面的change和change2都是函数的名称。

匿名函数的语法是lambda,例子如下:

def calc(x, y):
    return x**y

print(calc(2, 5))
# 为了好理解,我们给匿名函数起个名字c,方便后面调用
c = lambda x, y: x**y
print(calc(2, 5))

匿名函数可以缩短代码量,但是也比较令人不好理解。lambda的匿名函数通常搭配其他函数使用,比如map()

# map()可以放两个参数,一个是函数,一个是列表、字典等可迭代的数据
res = map(lambda x: x**2, [1, 5, 7, 4, 8])
for i in res:
    print(i)
# 输出结果:
# 1\n25\n49\n16\n64\n

上面的代码也可以写成一个函数,我们假设是calc(x),然后map(calc, [1, 5, 7, 4, 8]),匿名函数这样就显得很简便,同时代码的逼格也就提高了。

不过,lambda最多也就可以写一个三元运算(加一个简单的if语句,详看Python基础语法的笔记2.3),一些带for等等较复杂的函数,就不好转换成lambda写。

# lambda的三元运算
res = map(lambda x: x**2 if x > 5 else x**3, [1, 5, 7, 4, 8])
for i in res:
    print(i)

上面写的匿名函数的意思是,如果x大于5就平方,否则就三次方。输出的结果是

1	# 1^3
125	# 5^3
49	# 7^2
64	# 4^3
64	# 8^2
高阶函数

我们现在知道,可以传入的参数有字符、数字、list、dict、set等等,那我们是否可以使函数作为参数传入呢?

答案是可以的,而且上面的map()也用到了(传入的第一个参数是函数)。那我们也来写一个函数作为参数传入的函数。

# 绝对值
def get_abs(n):
    return int(str(n).strip('-'))

# 两个数的绝对值的和
def sum_abs(x, y, f):
    return f(x) + f(y)

print(sum_abs(-1, 3, get_abs))

那这个平常比较少用到,都是为了后面学习装饰器做铺垫。

3.2.6 递归函数

我们现在有一个奇怪的需求,一个数,然后不断除2(出现小数就向下取整),然后输出每次运算出的整型。

n = 100
while n>0:
    n = int(n/2)
    print(n)

那我们现在需要把这个改成函数,我们可以这样写:

def calc(n):
    n = int(n/2)
    print(n)
    if n > 0:
    	calc(n)
    # print(n)	下面解释
calc(100)

# 输出:
# 50
# 25
# 12
# 6
# 3
# 1
# 0

这样的函数就叫做递归函数,可以近似理解为函数循环。那大家也注意到了,代码中有一个注释的语句,我们把它解除注释,然后执行一次,我们发现输出的东西是从100到0又到100(50\n25\n12\n6\n3\n1\n0\n0\n1\n3\n6\n12\n25\n50),我们可以来解释一下这个现象。

首先,第一次递归得到的结果是50,我们第一次输出过后,进入判断n>0,然后进入了第二次递归,第一次递归的第二次输出就暂缓执行,等待第二次递归结束再执行。那么,以此类推,当我们执行第n次递归的时候,前面有n-1个递归在等待结果,只有最后if条件不满足,最后以此递归结束,前面的递归才倒序依次结束。

那我们就可以看出,递归有什么缺点了,那就是占内存,效率不高,还可能会导致出现栈溢出的现象,我们接下来总结一下递归的一些注意点。

递归的特点:

  • 必须有一个明确的结束条件。(不然递归就会一直进行下去)
  • 每次进入更深一层递归时,问题规模应当比上次递归小
  • 递归的效率不高,递归层次过多会导致栈溢出。(计算机中,函数调用是通过栈(stack)这个数据结构来实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就减少一层。由于栈的大小不是无限的,所以递归调用的次数过多就会导致栈溢出的现象)

Python限制递归次数最多994次,是可以改的,有特殊需求可以自行百度修改。栈很重要,是一个先进后出的数据结构,想进一步理解可以百度或者学习数据结构。

当然,递归在特定情况也是有很多应用的,比如以后学的一些算法,比如堆排、快排,这些比较复杂,我们以后再说。

递归练习——2分查找

用递归实现2分查找的算法,以列表a = [1, 3, 4, 6, 7, 8, 9, 11, 15, 17, 19, 21, 22, 25, 29, 33, 38, 69, 107]为例,查找指定的值。

# 这里是我实现的方案
a = [1, 3, 4, 6, 7, 8, 9, 11, 15, 17, 19, 21, 22, 25, 29, 33, 38, 69, 107]
temp = 0

def half_find(list_test, n):
    global temp
    mid = int(len(list_test)/2)

    if n < list_test[mid]:
        half_find(list_test[:mid+1], n)
    elif n == list_test[mid]:
        temp = mid
    else:
        half_find(list_test[mid:], n)

    return temp

print(half_find(a, 4))
print(a.index(4))

3.2.7 Python的内置函数

所谓内置函数,就是Python自己自带的函数,比如print()id()map()input()等等,我们这里就集中来说说Python的常用内置函数。

Python的所有内置函数如下图所示:

Python中bk python中bk函数_数据


下面我们就挑几个常用的做一些讲解,有个印象就行,用到就再找就好。

  • abs():求绝对值
  • ascii():返回传入参数的ascii码,比如ascii('中国'),返回是\\u4e2d\\u56fd
  • bin():二进制,传入数字,将数字转换成二进制返回
  • bool():空列表、空字典、空集合、0都代表False,其他都代表True的,可以传入参数,看看他们的返回值
  • all():传入的参数必须可以遍历,我们称为可迭代对象,all()会判断这个可迭代对象内的所有元素套一个bool(),看它们是否都为True。一些情况下,我们还是需要这个函数的。
  • any():和all()类似的原理,all()是全部是True才会返回True,any()是只要有一个是True就返回True。
  • bytearray():将二进制转换成二进制列表。字符串是不可变的,我们将其变为bytes类型,bytes类型也不可修改,然后我们在将bytes类型改为bytearray类型,然后我们就可以按照索引修改对应的bytes类型数据,然后bytearray()可以直接decode(),就变成了新的字符。
  • bytes():比如bytes('中国', 'utf-8'),和'中国'.encode('utf-8')一样的结果。
  • callable():判断一个对象是否可调用,学完Python面向对象编程就可以理解了。
  • chr():返回一个数字对应的ascii字符,比如chr(90)就返回ascii码表里90号对应的字符'Z'
  • classmethod():也是面向对象编程使用,后面用到再说。
  • complie():Python解释器自己用的,忽略
  • complex():求复数,比如complex(10, 9)返回的是10+9j,做工程的可能会用到,我们普通程序员一般不会用到。
  • copyright():没用
  • credits():Python感谢开发者的。。。没用
  • delattr():面向对象用,后面再说
  • dict():生成一个空字典
  • dir():返回对象的可调用属性,比如dir([]),他就会返回list的一些可调用的方法,啥append()sort()等等
  • divmod():返回出发的商和余数,比如divmod(4, 2)返回(2, 0),以元组形式返回
  • enumerate():返回列表的索引和元素,常用,不过也可以通过循环和index()的搭配来实现。
a = ['zm', 'ghd']
for i in enumerate(a):
    print(f'enumerate 0:{i[0]} | enumerate 1:{i[1]}')
# 输出:
# enumerate 0:0 | enumerate 1:zm
# enumerate 0:1 | enumerate 1:ghd
  • eval():可以把字符串形式的list、dict、set、tuple在转换成其原有的数据类型。这个其实是Python解释器用的,只是我们也用的到。
  • exec():把字符串格式的代码,进行解义并执行,比如exec("print('hello world')"),代码就会输出hello world
  • exit():退出程序
  • filter():对list、dict、set、tuple等可迭代对象进行过滤,可以搭配匿名函数使用,比如过滤出所有大于10的值:filter(lambda x: x>10, [1, 2, 24, 4, 11, 67, 7]),然后for循环输出即可。
  • float():转成浮点数
  • format():格式化输出,很少用,用个人喜欢用f''
  • frozenset():把一个集合变成不可修改的集合。
  • getattr():面向对象时使用,后面再说。
  • globals():返回所有的全局变量。
  • hasattr():面向对象使用,后面再说。
  • hash():传入任意参数,生成并返回哈希值。
  • help():帮助,进去之后可以查看各种东西。
  • hex():返回10进制数的16进制表示格式。
  • id():返回传入数据的内存地址。
  • input():获取输入的数据。
  • int():转成整型。
  • isinstance():判断数据是什么类型,比如isinstance([1, 2], list)就会返回True,isinstance({}, set)就是False。
  • issubclass():面向对象时用到,后面再说。
  • iter():转成可迭代对象。
  • len():返回传入参数的长度。
  • list():转成列表。
  • locals():返回当前位置的局部变量。
  • map():前面用过,和filter()原理相似。
  • max():返回传入参数中的最大值,字典、列表等都可以,不过必须全是数字或者全是字符串(一般是)。
  • memoryview():一般人不用,大神用的,忽略。
  • min():返回传入参数中的最小值。
  • next():生成器会用到,下面马上会说。
  • object():面向对象时用,后面再说。
  • oct():将10进制数返回8进制。
  • open():文件操作,前面细说过,不讲了。
  • ord():返回ascii字符对应的10进制数和chr()正好相反。
  • print():输出。
  • property():面向对象时用,后面再说。
  • quit():退出。
  • range():数字列表,前面说过,不再赘述。
  • repr():没什么用,忽略
  • reversed():反转列表,和列表的反转方法一样。
  • round():把小数四舍五入成整数,注意4.5会变成4,只有4.51才是5,要比5大一丁点才会进位,单纯的5不行,和日常生活中不一样,需要注意一下。round(10.15, 1)返回10.2,保留一位小数,不填第二个参数的话,就默认是整数比如round(10.15)返回10。
  • set():转成集合。
  • setattr():面向对象时用,后面再说。
  • slice():没用。
  • sorted():列表的sort()一样效果
  • staticmethod():面向对象时用,后面再说。
  • str():转成字符。
  • sum():求和,必须都是数字
  • super():面向对象时用,后面再说。
  • tuple():转成元组
  • type():返回数据的类型
  • vars():面向对象时用,后面再说。
  • zip():把多个列表拼成一个,和传统的合并不太一样。。。看看下面的例子就懂了
a = [1, 4, 9, 10, 11]
b = ['a', 'b', 'c']
print(list(zip(a, b)))

# 输出:[(1, 'a'), (4, 'b'), (9, 'c')]

有些东西会丢弃,以最短的为标准,新列表里面的元素是以元组为单位的,两个列表拼一起就是二元组,多个就是多元组。

3.2.8 NameSpace和闭包现象

名称空间NameSpace

python里我们写x=1,1我们知道存在哪,x就是一个指针,指向1的地址,这个我们前面都解释过,但是x和1的对应关系存储在哪呢?也就是x存在哪呢?那就是存在名称空间里面。

python有很多个名称空间,这些名称空间互不干扰,不同空间内相同名字的变量也没有任何联系。

名称空间有4类:LEGB

  • locals:函数内部的名字空间,一般有函数的局部变量以及形式参数。
  • encolsing function:在嵌套函数中的外部函数的名字空间。若fun2嵌套在fun1里面,fun1的名字空间就是enclosing function
  • globals:当前的模块空间,模块就是一些py文件,可以说全局变量。
  • __Builtins__:内置模块空间,放内置变量或内置函数的名字空间,print(dir(__builtins__))可以查看包含的值,我们Python抛出的一些错误,比如ValueError等也放在这里。

这个就可以解释,为什么可以同时存在同名的全局变量和局部变量,因为四个名称空间互不干扰,localsglobals两个里面同名的变量不会产生错误。

不同变量的作用域也就是由这个变量所在的名称空间决定的。

作用域即范围:

  • 全局范围:全局有效,可以当是一直有的
  • 局部范围:局部有效,可以当是临时有的

作用域的查找顺序优先从自己的名称空间找,顺序是按L->E->G->B,还找不到,那就抛出NameError的异常,表示变量名称不存在。这里也可以解释前面函数修改变量产生的一些现象。注意哈,如果是多层嵌套的话,E还有好几层的,就由所在内部函数一层一层往上找就好。理解这些顺序就好了,这样之后遇到自己代码因为这个报错的话,就可以知道怎么找出错误了。

闭包现象
什么是闭包
def outer():
    name = 'I am zm'
    
    def inner():
        print(name)
    
    return inner

print(outer())
func = outer()
func()	# 输出I am zm

这里我们输出就会发现,outer()返回的是inner()的函数地址,所以我们就可以在函数外部调用outer()的内部函数inner()

不过,这里也就引出了一个问题。我们前面说过,函数执行结束过后他执行占用的内存就会被全部释放,但是这里的现象就是outer()的内部函数inner()还是可以继续用的,并没有被释放,这个现象就叫闭包现象,官方也有定义“这个黏黏糊糊的现象就是闭包”。

闭包的意义

**返回的函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域。**简单说,就是改变了该函数的作用域。

闭包在下面的装饰器就会用到,一些企业面试也可能会问这个问题。

3.3 函数进阶

3.3.1 装饰器

要说清楚这个装饰器,我们就先假设一个场景。现在你是一家视频网站的后端开发工程师,网站分别有一下几个板块(简单的模拟)。

def home():
    print('----首页----')
def beijing():
    print('----北京专区----')
def henan():
    print('----河南专区----')
def anhui():
    print('----安徽专区----')

视频上线初期,为了吸引用户,全部免费观看,之后因为宽带费用公司承受不了,要对比较受欢迎的几个板块收费,其中包括北京和河南。那我们得先让其用户认证,然后判断是否是VIP会员就可以了。然后我们写了相应的函数。

account = {
    "is_authenticated": False,	# 用户登录成功就变成True
    "username": "zm",	# 假装是DataBase的用户信息
    "password": "abc123",	# 假装是DataBase的密码信息
}

def login():
    if account["is_authenticated"]:
        username = input('user:')
        password = input('password:')
        if username == account["username"] and password == account["password"]:
            print('welcome login ...')
            account["is_authenticated"] = True
        else:
            print('wrong username or password!')
    else:
        print('已登录,通过验证...')

def home():
    print('----首页----')
def beijing():
    login()
    print('----北京专区----')
def henan():
    login()
    print('----河南专区----')
def anhui():
    login()
    print('----安徽专区----')

这样我们哪个界面需要登录认证我们加上login()就可以了。但是,这已经违反软件开发中的一个“开放-封闭”原则,简单来说,就是已经实现功能的代码不应该被修改(封闭),但可以被扩展(开放)。

那我们就想到了高阶函数,把函数当做参数传入。哪个专区需要认证,我们就把那个专区的函数名当做参数传入login(),大概代码如下。

account = {
    "is_authenticated": False,
    "username": "zm",
    "password": "abc123",
}

def login(func):
    if account["is_authenticated"]:
        username = input('user:')
        password = input('password:')
        if username == account["username"] and password == account["password"]:
            print('welcome login ...')
            account["is_authenticated"] = True
            func()		# 改了这里
        else:
            print('wrong username or password!')
    else:
        func()		# 改了这里

def home():
    print('----首页----')
def beijing():
    print('----北京专区----')
def henan():
    print('----河南专区----')
def anhui():
    print('----安徽专区----')
    
login(anhui)
login(henan)

那我们就完成了任务了。但是,这样的代码被老开发者来说,是要被唾弃的。因为你改变了调用方式,现在所有专区的模块必须调用login()才可以加上认证这个功能,而企业开发中其他模块的功能代码肯定对这些anhui()henan()这些函数有很多的调用,现在他们必须全加上login()才行,想想这个工程有多大,肯定会被骂的。

我们再看下匿名函数和普通函数:

def plus(n):
    return n+1
plus_2 = lambda x: x+1
calc = plus_2
print(calc(2))	# 输出3

我们发现我们可以给函数另起名字,而且同样可以调用函数功能那我们应用到我们的代码中,演变成anhui = login(anhui),然后我们下面继续调用anhui()

但是运行过后,我们又会发现anhui()报错,我们找了下原因,就是login(anhui)的时候已经调用了anhui(),然后anhui()返回的是None,所以最后anhui = login(anhui)时,anhui获得的是None。我们可以将最后的anhui()改成print(anhui)看看其输出。

那我们的问题就变成了怎么延缓login(func)的调用呢?

我们想起了前面所提到的一个黏黏糊糊的现象——闭包。我们可以将login()函数改一下,改成一个嵌套结构,然后return内部函数名,搞成闭包的现象。

account = {
    "is_authenticated": False,
    "username": "zm",
    "password": "abc123",
}

def login(func):
    def inner():
        if account["is_authenticated"]:
        username = input('user:')
        password = input('password:')
        if username == account["username"] and password == account["password"]:
            print('welcome login ...')
            account["is_authenticated"] = True
            func()
        else:
            print('wrong username or password!')
    else:
        func()
	return inner

def home():
    print('----首页----')
def beijing():
    print('----北京专区----')
def henan():
    print('----河南专区----')
def anhui():
    print('----安徽专区----')
    
anhui = login(anhui)	# 这里anhui拿到的就是inner函数
henan = login(henan)	# henan拿到的也是inner函数

print(anhui)	# 输出的是inner的函数地址

anhui()	# 成功进入认证
henan()	# 第二次无需认证,成功

这样我们其他模块调用anhui()时,不需要加任何东西,正常写anhui()调用,就可以实现验证的功能。

那我们现在因为anhui()的一些视频点击量很大,我们就要加上vip的等级,从原来的函数修改了一下,增加了一个参数。

def anhui(vip_level):
    if vip_level > 3:
        print('全部解锁!')
    else:
        print('----安徽专区----')

然后,如果我们继续用anhui = login(anhui(4))的话,我们会发现少参数,python会报错的,因为返回的inner()是不带参的,那anhui拿到的就是不带参的anhui()而不是一开始的anhui(vip_level)了。

那很简单,我们修改一下login()不就好了,inner()改成inner(arg)inner()返回的func()改成func(arg)即可。那我们再次运行就会发现,anhui()虽然不报错了,但是henan()报错了,因为它不传参,不需要判别vip等级。

那怎么办?其实也很简单,我们可以使用非固定参数,把上面说到的arg换成*args, **kwargs

def login(func):
    def inner(*args, **kwargs):	# 这里
        if account["is_authenticated"]:
            username = input('user:')
            password = input('password:')
            if username == account["username"] and password == account["password"]:
                print('welcome login ...')
                account["is_authenticated"] = True
                func(*args, **kwargs)	# 这里
            else:
                print('wrong username or password!')
        else:
            func(*args, **kwargs)	# 这里
	return inner

好了,这样我们anhui(4)就不会报错了,henan()也不会报错。

account = {
    "is_authenticated": False,
    "username": "zm",
    "password": "abc123",
}

def login(func):
    def inner(*args, **kwargs):
        if account["is_authenticated"]:
            username = input('user:')
            password = input('password:')
            if username == account["username"] and password == account["password"]:
                print('welcome login ...')
                account["is_authenticated"] = True
                func(*args, **kwargs)
            else:
                print('wrong username or password!')
        else:
            func(*args, **kwargs)
	return inner

def home():
    print('----首页----')

def beijing():
    print('----北京专区----')

@login	# 用到装饰器的函数就加一个@装饰器名,实现覆盖,方便后期维护
def henan():
    print('----河南专区----')

@login
def anhui(vip_level):
    if vip_level > 3:
        print('全部解锁!')
    else:
        print('----安徽专区----')

# 写了@login后,python自动就帮你执行了anhui = login(anhui),不用自己写了,直接调用即可
# anhui = login(anhui)	# 这里anhui拿到的就是inner函数
# henan = login(henan)	# henan拿到的也是inner函数

anhui(4)	# 没报错
henan()	# 没报错

那上面的代码就是最终的装饰器了,在日后的开发过程中,是一个很重要的知识点,会经常使用。装饰器也称作是语法糖,装饰器也可以嵌套,比如anhui()在被login()装饰的基础上,我们再写一个pay_money()再装饰一下,我们就要写成

@pay_money
@login
def anhui():
    .......

好了,至此装饰器就结束了。

3.3.2 列表生成式

先有个需求,给列表每个元素加一,a = [0, 1, 2, 3, 4]

# 生成一个新列表   浪费空间
a = [0, 1, 2, 3, 4]
b = []
for i in a:
	b.append(i+1)
a = b
# 通过下标,修改value		毫无新意
a = [0, 1, 2, 3, 4]
for i, v in enumerate(a):
    a[i] = v+1
print(a)
# 匿名函数
a = [0, 1, 2, 3, 4]
a = list(map(lambda x: x+1, a))
# 列表生成式
a = [0, 1, 2, 3, 4]
a = [i+1 for i in a]	# 这就是列表生成式,有时候会用
# 列表生成式也相当于创建了一个新列表,也占空间

3.3.3 生成器

生成器

生成器,又称generator

现在有如下代码:

for i in range(100000):
    print(i)
    if i > 100:
        break

我们可以看出上述代码非常浪费空间,i=101breakrange()生成的十万个元素的列表除了用了101个,其他全浪费了。(while循环实现即可,while循环原理类似生成器)(这里的range()在python3优化成了生成器,就不会浪费空间了)

那我们实现一个一边循环,一边计算下面一个元素,这种机制就叫生成器。

生成器优化循环效率

这个就是一个生成器g = (x*x for x in range(10)),它只会保存这个算法,就生成了一个生成器的对象。我们如果要取下一个元素,就调用Python的内置方法next()next(g)第一次返回0,然后我们可以一直next(),直到后面没有元素了,就会抛出StopIteration异常。这种计算我们叫做惰性运算。

斐波拉契数列
# 输出前100项Fibonacci数列 斐波拉契数列
a, b = 0, 1
n = 0
while n < 100:
    n = a + b
    a = b
    b = n
    print(n)
函数生成器

上面的斐波拉契数列我们应该可以了解,我们不能用一个普通的生成器来实现斐波拉契数列。那我们就引入一个函数生成器的概念。

我们先将上面的while循环改成一个函数:

def fib(n):
    a, b = 0, 1
    i = 0
    count = 0
    while i < n:
        i = a + b
        a = b
        b = i
        print(i)
        count += 1

然后我们将print(i)(或者return i)改成yield i,函数就变成了函数生成器。

def fib(n):
    a, b = 0, 1
    i = 0
    count = 0
    while i < n:
        i = a + b
        a = b
        b = i
        yield i		# 暂停,next()才会继续循环
        count += 1

我们可以输出查看fib()函数返回的数据类型,我们发现是generator。然后我们就可以next(fib)(或者fib.__next__(),一样的效果)

函数生成器我们可以实现先执行用几次函数,然后干其他的事,接着再执行函数,函数生成式可以使函数占用 的内存一直不释放。

生成器实现并发编程

好了,上面我们说了函数生成器如何向外传参数(yield n),那我们可不可以向里传入参数呢?

答案是可以的,

def g_test():
	while True:
        n = yield
        print('receive from outside:', n)

g = g_test()
g.__next__()	# 第一次next会调用生成器,同时会发送None到yield

for i in range(10):
    g.send(i)	# 调用生成器,同时发送i到yield

生成器应用非常广,Python中循环、文件操作等等底层都是用生成器来实现的。

那我们现在来用函数生成器实现单线程下的多并发效果(微观的串行,宏观的并行)。

# 吃包子
# 消费者吃包子:p1, p2, p3
# 生产者做包子:
def consumer(name):
    print(name + '准备吃包子啦')
    while True:
        baozi = yield 	# 接收外面的包子
        print(name+'收到包子, '+'包子编号为'baozi)

p1 = consumer("P1")
p2 = consumer("P2")
p3 = consumer("P3")

p1.__next__()
p2.__next__()
p3.__next__()

for i in range(10):
    print('生成了第%s屉包子-----------' % i)
    # 一屉包子三个
    p1.send(i)
    p2.send(i)
    p3.send(i)
    
# 我们就实现了单线程的并发效果,三个人同时吃包子

3.3.4 迭代器

for循环的数据类型有以下几种:

  1. 集合数据类型,eg:list、tuple、dict、set、str等
  2. generator,包括生成器和带yield的函数生成器

这些可以被for循环的,就叫可迭代对象。

可迭代对象我们可以用isinstance(X, Iterable)来判断(需要导入collections库)

迭代器我们可以用isinstance(X, Iterator)

迭代器可以next(),可迭代对象只是可以被循环。

iter()将参数转换成迭代器,使其可以被next()

迭代器近似就是生成器,可以这样理解,面试一般不会问,区别我们在练习题11中解释了。

3.4 练习题

参考http://book.luffycity.com/python-book/315-lian-xi-989826-zuo-ye.html

自己实现的代码:

# 第1题
def sum_num(*args):
    if args:
        return sum(args)
    else:
        return 0


print(sum_num(1, 2, 3.5))
print(sum_num())
print(sum_num(1.2, 2.1, 3.5))

# 第2题
import os

def replace_file(filename, old_str, new_str):
    with open(filename, 'r', encoding='utf-8') as f:
        with open('new.txt', 'w', encoding='utf-8') as new_f:
            for line in f.readlines():
                if old_str in line:
                    line = line.replace(old_str, new_str)
                    new_f.write(line)
                else:
                    new_f.write(line)

    os.replace('new.txt', filename)

replace_file('test.txt', 'wzw', 'py')
# 文件为同目录下的test.txt,内容见下面的附录

# 第3题
def all_true(data):
    if isinstance(data, list):
        if all(data):
            print('该列表中的元素全不为空')
        else:
            print('该列表中存在空元素')
    elif isinstance(data, tuple):
        if all(data):
            print('该元组中的元素全不为空')
        else:
            print('该元组中存在空元素')
    elif isinstance(data, str):
        if all(data):
            print('该字符串中的元素全不为空')
        else:
            print('该字符串中存在空元素')
    else:
        print('请传入列表、元组或字符串类型的数据')

all_true([1, 2, 0])
all_true([1, 2, 3])
all_true([1, [], 2])
all_true([1, {}, 2])
all_true((1, 2, 3))
all_true((1, 2, []))
all_true('012')
all_true('01 ')
all_true('')

# 第4题
def cut_item(data):
    """
    将列表、字符、字典所有元素长度强制小于等于2
    :param data: 要处理的数据
    :return: 处理完的数据
    """
    if isinstance(data, str):
        cut_two_str(data)
    elif isinstance(data, list):
        data = cut_two_list(data)
    elif isinstance(data, dict):
        data = cut_two_dict(data)

    return data

def cut_two_list(cut_list):
    """
    将列表的所有元素长度强制小于等于2,包括嵌套的字典和列表结构
    :param cut_list: 要处理的列表
    :return: 处理完的列表
    """
    for i, v in enumerate(cut_list):
        if isinstance(v, list):
            cut_two_list(v)
        elif isinstance(v, dict):
            cut_two_dict(v)
        elif isinstance(v, str):
            if len(v) > 2:
                cut_list[i] = v[:2]

    return cut_list

def cut_two_dict(cut_dict):
    """
    将字典的所有元素长度强制小于等于2,包括嵌套的列表和字典结构
    :param cut_dict: 要处理的字典
    :return: 处理完的字典
    """
    for i, v in cut_dict.items():
        if isinstance(v, list):
            cut_two_list(v)
        elif isinstance(v, dict):
            cut_two_dict(v)
        elif isinstance(v, str):
            if len(v) > 2:
                cut_dict[i] = v[:2]
    return cut_dict

def cut_two_str(cut_str):
    """
    将字符的长度强制小于等于2
    :param cut_str: 要处理的字符
    :return: 处理完的字符
    """
    if len(cut_str) > 2:
        cut_str = cut_str[:2]
    return cut_str

print(cut_item('zmw'))
print(cut_item('zm'))
print(cut_item(['wzw', 'zzw', 'zm', 'ghd', 'py']))
print(cut_item(['wzw', 'zzw', 'zm', ['ghd', 'py']]))
print(cut_item([{'1': 'wzw', '2': 'zzw', '3': 'zm'}, ['ghd', 'py']]))
print(cut_item({'1': 'wzw', '2': 'zzw', '3': 'zm', '4': 'ghd', '5': 'py'}))
print(cut_item({'1': 'wzw', '2': 'zzw', '3': 'zm', '4': ['ghd', 'py']}))

# 第5题
闭包就是一种保留内部函数的一种现象,可以使外部函数结束运行的情况下,还可以继续调用内部函数。官方文档称之为黏黏糊糊的现象。

# 第6题
def make_card():
    card = []
    before_name = ['♥', '♦', '♠', '♣']
    after_name = [i for i in range(1, 11)]
    special_after = ['J', 'Q', 'K', 'A']
    special_name = ['小王', '大王']

    for before in before_name:
        for after in after_name:
            card.append((before, str(after)))
        for after in special_after:
            card.append((before, after))
    card.append(special_name[0])
    card.append(special_name[1])
    return card

print(make_card())

# 第7题
def min_max(*args):
    return_dict = {'max': max(args), 'min': min(args)}
    return return_dict


print(min_max(1, 3, 9, 4, 5, 19.3, 13.1, 0, -3))

# 第8题
from math import pi

def area(*args):
    def calc_rectangle_area(length, width,):
        return length * width

    def calc_square_area(length,):
        return length ** 2

    def calc_round_area(radius,):
        return pi * (radius ** 2)

    if args[0] == '圆形':
        try:
            data = calc_round_area(args[1])
        except TypeError:
            print('请输入正确的格式,圆形计算格式为 ("圆形", 半径)')
    elif args[0] == '正方形':
        try:
            data = calc_square_area(args[1])
        except TypeError:
            print('请输入正确的格式,正方形计算格式为 ("正方形", 边长)')
    elif args[0] == '长方形':
        try:
            data = calc_rectangle_area(args[1], args[2])
        except TypeError:
            print('请输入正确的格式,长方形计算格式为 ("长方形", 边长)')
    return data

print(area('圆形', 1))
print(area('正方形', 1.5))
print(area('长方形', 2.3, 10))

# 第9题
def factorial(n):
    temp = 1
    for i in [x for x in range(1, n+1)]:
        temp = temp * i
    return temp

print(factorial(3))
print(factorial(4))
print(factorial(10))

# 第10题
# 本代码所需的test.txt位于同目录下,内容见下面附录
info = {
    'is_authentication': False,
    'user_info': [],
}

def data_init(filename):
    with open(filename, 'r', encoding='utf-8') as f:
        for line in f.readlines():
            info['user_info'].append(line.strip().split(','))

def login(func):

    def inner(*args, **kwargs):
        counts = [i[0] for i in info['user_info']]
        if not info['is_authentication']:
            user = input('please input your username: ')
            pwd = input('please input your password: ')
            if user in counts:
                if [user, pwd] in info['user_info']:
                    info['is_authentication'] = True
                    func(*args, **kwargs)
                else:
                    print('密码错误!')
            else:
                print('用户名不存在!')
        else:
            func()

    return inner

@login
def ghd():
    print('----- 欢迎进入该模块 ----')

@login
def zm():
    print('----- 欢迎使用该功能 ----')

data_init('test.txt')
ghd()
zm()

# 第11题
迭代器与生成器的区别:
(1)生成器:
1.生成器本质上就是一个函数,它记住了上一次返回时在函数体中的位置。
2.对生成器函数的第二次(或第n次)调用,跳转到函数上一次挂起的位置。
3.而且记录了程序执行的上下文。
4.生成器不仅“记住”了它的数据状态,生成还记住了程序执行的位置。
(2)迭代器
1.迭代器是一种支持next()操作的对象。它包含了一组元素,当执行next()操作时,返回其中一个元素;
2.当所有元素都被返回后,再执行next()报异常—StopIteration;
3.生成器一定是可迭代的,也一定是迭代器对象。
(3)区别:
1.生成器是生成元素的,迭代器是访问可迭代对象元素的一种方式;
2.迭代输出生成器的内容;
3.迭代器是一种支持next()操作的对象
4.迭代器(iterator):其中iterator对象表示的是一个数据流,可以把它看做一个有序序列,只是不知道序列的长度,只有通过next()函数获取需要计算的下一个数据。可以看做生成器的一个子集。

# 第12题
生成器获取value有两种方式,一个是next()计算获取下一个数据,一个是yield,获取外部传来的值或暂停并返回函数的值

# 第13题
# 该题不需要文件,会自动在程序目录下创建web.log
import time

def logger(filename, channel='file'):
    """
    日志方法
    :param filename: log filename
    :param channel: 输出的目的地,屏幕(terminal),文件(file),屏幕+文件(both)
    :return:
    """
    with open(filename, 'w', encoding='utf-8') as f:
        count = 1
        data_init = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + f' [{count}] test log db backup 3'
        f.write(data_init + '\n')

        while True:
            data = yield
            count += 1
            now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
            log = now + f' [{count}] ' + data

            if channel == 'both':
                f.write(log + '\n')
                print(log)
            elif channel == 'file':
                f.write(log + '\n')
            elif channel == 'terminal':
                print(log)
            else:
                print('请检查参数')

# 调用
log_obj = logger(filename='web.log', channel='both')
log_obj.__next__()
log_obj.send('user yz login success')
log_obj.send('user zm login success')
log_obj.send('user ghd login success')
log_obj.send('user wzw login success')     # 可以给多个send试试

# 第14题
name = ['alex', 'wupeiqi', 'yuanhao', 'nezha']
print(list(map(lambda x: x + '_sb', name)))

# 第15题
list_1 = [1, 3, 5, 6, 7, 8]
print(list(filter(lambda x: x % 2 == 0, list_1)))

# 第16题
1.我认为是map()来算比较好:
print(list(map(lambda x: x['shares'] * x['price'], portfolio)))
2.代码如下:
portfolio = [
    {'name': 'IBM', 'shares': 100, 'price': 91.1},
    {'name': 'AAPL', 'shares': 50, 'price': 543.22},
    {'name': 'FB', 'shares': 200, 'price': 21.09},
    {'name': 'HPQ', 'shares': 35, 'price': 31.75},
    {'name': 'YHOO', 'shares': 45, 'price': 16.35},
    {'name': 'ACME', 'shares': 75, 'price': 115.65},
]

result = (filter(lambda x: x['price'] > 100, portfolio))
print([i['name'] for i in result])

# 第17题
li = ['alex', 'egon', 'smith', 'pizza', 'alen']

def func(old_str):
    if old_str[0] == 'a':
        new_str = 'A' + old_str[1:]
    else:
        new_str = old_str
    return new_str

print(list(map(func, li)))

# 第18题
li = ['alex', 'egon', 'smith', 'pizza', 'alen']

def rule(data):
    return data[-2]

li.sort(key=rule, reverse=True)
print(li)

# 第19题
# poetry.txt文件位于同目录下
import os
count = 0

with open('poetry.txt', 'r', encoding='utf-8') as f:
    with open('new.txt', 'w', encoding='utf-8') as new_f:
        while True:
            line = f.readline()
            if not line:
                break
            count += 1
            if count != 3:
                new_f.write(line)
                
os.replace('new.txt', 'poetry.txt')

# 第20题
# username.txt文件位于同目录下
with open('username.txt', 'r+', encoding='utf-8') as f:
    data = f.readlines()
    for line in data:
        if 'alex' in line:
            print('用户该用户已存在')
            exit('程序结果')
    f.write('\nalex')

# 第21题
# user_info.txt文件位于同目录下
import os
with open('user_info.txt', 'r', encoding='utf-8') as f:
    with open('new.txt', 'w', encoding='utf-8') as new_f:
        for line in f.readlines():
            if '100003' not in line:
                new_f.write(line)

os.replace('new.txt', 'user_info.txt')

# 第22题
# user_info.txt文件位于同目录下
import os
with open('user_info.txt', 'r', encoding='utf-8') as f:
    with open('new.txt', 'w', encoding='utf-8') as new_f:
        for line in f.readlines():
            if '100002' in line:
                line = 'alex li,100002'
            new_f.write(line)
os.replace('new.txt', 'user_info.txt')

# 第23题
import time

def time_calc(func):
    def inner(*args, **kwargs):
        begin = time.time()
        func(*args, **kwargs)
        end = time.time()
        return end-begin
    return inner

@time_calc
def calc(n):
    for i in range(n):
        pass	# 占位符

print('time:' + str(calc(1000000)))
print('time:' + str(calc(10000000)))

# 第24题
lambda就是匿名函数,自己经常在map()、filter()中使用,实现一些简单的函数和三元运算等。

# 第25题
import random

def dice(n):
    temp_list = []
    for i in range(1, n+1):
        temp_list.append(random.randint(1, 7))

    return temp_list

choice = input('三个骰子压大小(输入大或小):').strip()
if choice in ['大', '小']:
    result = dice(3)
    if sum(result) > 9:
        print(f'结果是:{result}={sum(result)},[大]!')
        if choice == '大':
            print('您压中了!')
        else:
            print('您没压中!')
    elif sum(result) < 9:
        print(f'结果是:{result}={sum(result)},[小]!')
        if choice == '小':
            print('您压中了!')
        else:
            print('您没压中!')
    else:
        print('不大不小。。。请再来一遍!')

附录:

# 第2题的test.txt内容如下:
py, 18, 177333112233
py, 24, 18845516854
zm, 18, 177333112233
py, 24, 18845516854
zm, 18, 177333112233
zm, 24, 18845516854
py, 18, 177333112233
zm, 24, 18845516854

# 第10题的test.txt内容如下:
zm,123
ghd,123
wzw,123
py,123
yz,123

# 第19题的poetry.txt内容如下:
昔人已乘黄鹤去,此地空余黄鹤楼。
黄鹤一去不复返,白云千载空悠悠。
晴川历历汉阳树,芳草萋萋鹦鹉洲。
日暮乡关何处是?烟波江上使人愁。

# 第20题的username.txt内容如下:
pizza
alex
egon

# 第21和22题的user_info.txt内容如下:
pizza,100001
alex,100002
egon,100003