Python高阶学习记录

文章导读

        阅读本文需要一定的python基础,部分知识点是对python入门篇学习记录python并发编程学习记录的深入探究,本文记录的Python知识点包括函数式编程,装饰器,生成器,迭代器,正则表达式,内存管理和Python新特性。

1、函数式编程

1.1、高阶函数

概述

①、函数式编程最鲜明的特点就是:函数是一等公民(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

②、一个函数可以接收另一个函数作为参数,这种函数就称之为高阶函数。

③、Python内置的高阶函数有map,reduce,filter,sorted。

实操:定义三个函数,第一个函数不接收参数直接打印信息,第二个参数接收两个任意类型的参数并打印信息,第三个参数打印信息后调用前面两个函数中任意一个函数。

# 不接收参数的函数
def fun1() :
    print("fun1函数正在运行")

# 接收多个参数的函数
def fun2(a,b):
    print("fun2函数正在运行")
    print(f"接收到的参数是:{a},{b}")

# 当不确定参数个数时可以采用下面的方式动态地接收参数
def fun3(fun,*args,**kwargs):
    print("fun3函数正在运行")
    # 调用传入的函数
    fun(*args,**kwargs)

if __name__ == '__main__':
    # 函数没有带小括号表示该函数在内存中的地址,函数后带小括号表示直接调用对应的函数
    f1 = fun1
    f2 = fun2
    # 将fun1传入fun3中
    fun3(f1)
    # 将fun2传入fun3中
    fun3(f2,1,{"name":"muxikeqi"})

运行结果如下:

1.2、lambda表达式和匿名函数

概述:lambda表达式可以用来声明匿名函数,并且lambda函数是一种简单的、在同一行中定义函数的方法,它实际生成了一个函数对象。

注意:表达式只允许包含一个表达式,不能包含复杂语句,该表达式的计算结果就是函数的返回值。

语法

lambda 参数1,参数2,... : <表达式>

实操1:掌握lambda函数的基本写法。

f = lambda a,b,c : a+b+c
print(f"1+2+3={f(1,2,3)}")

运行结果如下:

实操2:创建一个包含三个元素的列表,列表的第一位结果为输入值*2,列表的第二位结果为输入值*3,列表的第三位结果为输入值*4,为该列表传入任意三个值,打印这个列表的最终数据。

f1 = [lambda a:a*2,lambda b:b*3,lambda c:c*4]
print(f"列表的最终结果为:[{f1[0](2)},{f1[1](3)},{f1[2](4)}]")

运行结果如下:

1.3、偏函数

概述:Python的 functools 模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。

作用:把一个函数某些参数固定住(也就是设置默认值),返回一个新的函数,调用这个新的函数会更简单。

知识点补充:int()函数除了能把包含整数的字符串转换为整数外,还提供了base参数,默认值为10,表示传入的参数类型为十进制,如果传入base参数,就可以标记传入数据的进制。

实操1:(在没有学习偏函数之前的解决思路)现有一个列表中包含多个2进制数据,要求通过定义函数的方式简化操作,将列表中的数据转换成二进制并打印。

# 待转换的数据源
data = ['1000000','1010101','10101011','10001111','11101101']

def int2(num, base=2):
  return int(num,base)

for i in data:
    print(int2(i),end='\t')

运行结果如下:

实操2:利用Python提供的偏函数实现实操1中同样的效果。

import functools
# 待转换的数据源
data = ['1000000','1010101','10101011','10001111','11101101']
# 偏函数说白了就是为定义含有默认值的函数给出了另一个创建方案
int2 = functools.partial(int,base=2)
for i in data:
    print(int2(i),end="\t")

运行结果如下:

1.4、闭包(closure)

概述:闭包是一个函数,它可以访问到父级函数的作用域,是函数和自由变量的总和。

知识点补充

①、局部变量:如果变量名称绑定在一个代码块中,则为该代码块的局部变量,除非声明为nonlocal或global;

②、全局变量:如果变量名称绑定在模块层级,则为全局变量;

③、自由变量:如果变量在一个代码块中被使用但不是在这个代码块中定义的,则为自由变量。

特点

①、闭包是一个函数,而且存在于另一个函数当中;

②、闭包可以访问到父级函数的变量,且该变量不会销毁。

实操1:通过下面的例子理解什么是闭包:

"""
闭包的特点:
1、存在外层函数嵌套外层函数的情况
2、内层函数引用了外层函数的变量或参数(即在前面提到过的自由变量)
3、外层函数将内层函数的地址当作函数的返回值
"""
# 定义一个名为outer的函数
def outer():
    # 定义一个变量
    num = 1
    print("outer函数已执行")
    # 在outer函数内又定义了一个名为inner的函数,但我们不调用它
    def inner():
        print(f"outer函数中的num:{num}")
        print("inner函数已执行")
    # 在outer函数的最后返回inner函数的内存地址
    return inner

if __name__ == '__main__':
    # 调用outer函数并把运行结果返回给result变量
    result = outer()
    # 这里打印分隔符用于前后区分调用的结果
    print("---------------------------------")
    # 将outer函数的返回结果运行
    result()

运行结果如下:

总结:闭包是由于函数内部使用了函数外部的变量,只要闭包这个函数对象不销毁,则外部函数的局部变量也不会销毁。

作用

①、隐藏变量,避免全局污染;

②、可以读取函数内部的变量。

闭包使用不当的缺点

①、导致变量不会被垃圾回收机制回收,造成内存消耗(因为自由变量不会被回收,能一直存在);

②、不恰当的使用闭包可能会造成内存泄漏的问题。

实操2:通过下面的例子理解什么是全局变量的污染,add函数用于实现变量a自增 ,print_ten函数用于判断全局变量的值是否为10,如果为10就打印"ten!",否则打印"全局变量a,不等于10"。

#通过全局变量,可以实现,但会污染其他程序
a = 10
def add():
  global a
  a+=1
  print("a:",a)

def print_ten():
  if a==10:
    print("ten!")
  else:
    print("全局变量a,不等于10")

add()
print_ten()

运行结果如下:

实操3:通过闭包实现实操2中的要求并避免全局变量的污染问题。

分析:实操2中造成全局变量污染的原有在于add函数永久修改了全局变量,因此将add函数中添加闭包即可。

知识点补充:global关键字修饰变量后标识该变量是全局变量,对该变量进行修改就是修改全局变量,而nonlocal关键字修饰变量后标识该变量是上一级函数中的局部变量,如果上一级函数中不存在该局部变量,nonlocal位置会发生错误

# 定义一个全局变量a
a = 10
# 定义一个名为add的函数,将全局变量a传进来用于让闭包函数add_one将变量a保存为自由变量
def add(num):
    # 声明为全局变量
    a = num
    def add_one():
        nonlocal a
        a+=1
        print("a:",a)
    return add_one

def print_ten():
    if a==10:
        print("ten!")
    else:
        print("全局变量a,不等于10")

# 将add函数返回的add_one函数的地址保存到变量add_one_again中
add_one_again = add(a)
# 利用add_one_again变量中保存的add_one函数的地址直接调用add_one函数
add_one_again()
# 再次调用该函数
add_one_again()
# 直接调用print_ten函数
print_ten()

运行结果如下:

实操4:(用闭包实现不修改源码添加功能),为fun1函数添加记录日志的功能,用打印语句模拟对应的功能。

# 源程序
def fun1 ():
    print("正在执行fun1函数")
    
fun1()
def fun2(fun):
    def inner_fun(*args,**kwargs):
        # 添加新的功能
        print("记录日志中")
        # 获取传入的函数并执行(为了避免调用的函数会传入参数,这里用*args和**kwargs接收参数)
        fun(*args,**kwargs)
    # 将inner_fun函数的内存地址返回
    return  inner_fun
def fun1 ():
    print("正在执行fun1函数")

new_fun = fun2(fun1)
new_fun()

运行结果如下:

程序分析:首先执行倒数第三行的fun2(fun1),将fun1函数的内存地址传入fun2函数中,然后进入fun2函数,fun2函数将fun2函数里inner_fun函数的地址作为返回结果返回给倒数第3行的new_fun变量,所以此时new_fun变量保存的就是闭包函数inner_fun函数的内存地址,然后最后一行通过new_fun()的意思就是通过inner_fun的地址直接去调用inner_fun函数(函数地址后面加小括号就是调用这个函数地址里的函数)。

1.5、map函数

概述:map函数是一个内置函数,函数接收两个参数,第一个是函数,第二个是序列(可以传入多个序列),map函数将传入的序列中每个元素依次作用到传入的函数中,并返回一个map类型的对象;

实操1:通过map函数求1~10每个数的平方,返回结果用列表(list)展示。

# 定义了一个函数,用于计算数值的平方
def fun(i):
    return i**2
# 定义一个空列表,用于存储用于计算的数据
lis = []
# 通过for循环遍历range里的数并添加到lis列表中
for i in range(1,11):
    # 注意:append会修改lis本身,并且返回None。不能把返回值再赋值给lis
    lis.append(i)
# map函数的返回结果是map类型的对象,如果需要返回lies类型的数据要通过list做强制类型转换
result = list(map(fun,lis))
# 打印结果
print(result)

运行结果如下:

实操2:结合map函数和lambda表达式实现求1~10每个数的平方,返回结果用列表(list)展示。

lis = map(lambda n:n*n,[1,2,3,4,5,6,7,8,9,10])
print(list(lis))

运行结果如下:

实操3:结合map函数和lambda表达式实现任意两个数相加。

# 注意:lis只会计算最短参数个数的值进行运算
lis = map(lambda a,b:a+b, [1,2,3],[4,5,6,7])
print(list(lis))

运行结果如下:

总结:如果结合map函数和lambda表达式,lambda表达式可以创建任意个数的变量用于计算,但后面的参数一定要对应上前面的变量,否则会报错(报错信息:<lambda>() missing 1 required positional argument: 'c')

1.6、reduce函数

概述:reduce位于functools模块,reduce把一个函数作用在一个序列[x1, x2, x3...]上,这个函数必须接收两个参数,第一个参数是调用的函数,第二个参数是序列,reduce把前两个的计算结果继续和序列的下一个元素进行计算,依次类推,直到计算完序列中所有元素。

注意:如果要结合lambda表达式和reduce函数,lambda表达式只能接收两个参数,因为reduce函数的第一个参数是一个二元函数,也就是说它接受两个参数并返回一个值,如果lambda表达式定义了三个及以上个参数就会报错(报错信息:<lambda>() missing 1 required positional argument: 'z')。

reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

实操1:利用reduce函数计算1~10之间所有奇数的和。

# 从functools模块中导入reduce函数
from functools import reduce
# 定义一个空列表
lis = []
# 利用循环为lis列表中添加数据
for i in range(1,10,2):
    lis.append(i)
# 定义一个函数用于计算两数之和
def add(a,b):
    return a+b
# 利用reduce函数开始计算
result = reduce(add,lis)
# 打印结果
print(f"1~10之间所有奇数之和为:{result}")

运行结果如下:

实操2:结合reduce函数和lambda表达式计算1~10之间所有奇数的和。

from functools import reduce

result = reduce(lambda x,y:x+y, [1,3,5,7,9])
print(f"1~10之间所有奇数之和为:{result}")

1.7、filter函数

概述:filter函数是一个内置函数,该函数用于过滤序列。filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False, 决定保留还是丢弃该元素。

实操1:利用filter函数过滤出列表中所有能被3整除的数。

# 定义一个函数用于判断某个值是否是3的倍数并返回布尔值
def is_three(num):
    return num % 3 == 0
# 通过filter函数过滤出所有3的倍数的数值
result = filter(is_three,[11,12,13,14,15,16,17,18,19,20])
# 由于返回结果是filter类型的对象,要用list做强制类型转换
print(list(result))

运行结果如下:

实操2:利用filter函数和lambda表达式实现过滤出列表中所有能被3整除的数。

result = filter(lambda num:num%3==0,[11,12,13,14,15,16,17,18,19,20])
print(f"列表中能被3整除的数包括:{list(result)}")

运行结果如下:

实操3:利用filter函数和lambda表达式实现空字符串的删除。

result=filter(lambda s:(s and s.strip()), ['A', '', 'B', None, 'C', '  '])
print(list(result))

运行结果如下:

1.8、sorted函数

概述:排序算法,排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小,sorted函数默认做升序排序,如果需要反序排序,添加 reverse=True 即可。

排序规则

①、如果是对数字排序,我们可以直接比较;

②、如果是自定义对象进行排序,直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。通常规定,对于两个元素x和y,如果认为x < y,则返回-1,如果认为x == y,则返回0,如果认为x > y,则返回1,这样,排序算法就不用关心具体的比较过程,而是根据比较结果直接排序。

实操1:通过sorted函数对列表中的数字进行排序。

lis = [14,63,32,74,15,84,25,84,36,41,73,11,90,52]
print(f"排序结果为{sorted(lis)}")

运行结果如下:

知识点补充:sorted函数是一个高阶函数,它还可以接收一个key函数来实现自定义的排序。

实操2:对列表中数据的绝对值做降序排序。

lis = [-10,-28,-50,-84,-63,-25,-96,-36,9,14,64]
print(f"排序结果为{sorted(lis,key=abs,reverse=True)}")

运行结果如下:

实操3:对自定义对象中的age属性进行排序。

# 创建一个名为Student的类
class Student:
    def __init__(self,name,age):
        self.name = name
        self.age = age

# 创建Student类的实例对象
s1 = Student("张三",18)
s2 = Student("李四",22)
s3 = Student("王五",15)

# 通过sorted对对象的年龄进行排序
result = sorted([s1,s2,s3] ,key=lambda s:s.age)
# 对排序后的对象遍历打印
for i in result:
    print(f"{i.name}:{i.age}")

运行结果如下:

知识点补充:可以通过functools模块中的cmp_to_key来对自定义的cmp函数进行包装,然后就能赋值给sorted函数的关键字参数key,间接实现Python2中cmp函数用于排序的效果。

实操4(了解):利用functools模块中的cmp_to_key调用自己定义的排序方法实现基于对象中某个属性的排序。

# 自定义一个排序方法
def custom_sorted(stu1,stu2):
  if stu1.age < stu2.age:
    return -1
  if stu1.age > stu2.age:
    return 1
  return 0

# 创建Student类的实例对象
s1 = Student("张三",18)
s2 = Student("李四",22)
s3 = Student("王五",15)

# 通过sorted对对象的年龄进行排序
result = sorted([s1,s2,s3] ,key=cmp_to_key(custom_sorted))

# 对排序后的对象遍历打印
for i in result:
    print(f"{i.name}:{i.age}")

运行结果如下:

2、装饰器(Decorator)

2.1、初识装饰器

概述:python 中的装饰器用于提供了一些额外的功能。装饰器本质上是一个Python函数(其实就是闭包),它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。

装饰器的应用场景:插入日志、性能测试、事务处理、缓存、权限校验等。

实操1:复习闭包中是如何为原有程序插入日志记录功能的,分别为两个函数插入日志记录功能。

# 需要插入日志记录的函数
def source_code1():
    print("执行了程序1")

def source_code2(a,b):
    print("执行了程序2,接收的参数为",a,b)

# 外部函数传递函数地址并返回内部函数的地址(闭包)
def outer(fun):
    #内部函数用于执行传入的函数并记录日志。*args和**kwargs用于接收任意数量的参数,参考python入门篇
    def log(*args,**kwargs):
        fun(*args,**kwargs)
        print("日志记录中...")
    # 返回内部函数地址
    return log
# 获取内部函数地址(获取地址时不要传入参数,参数要在调用函数时传入)
new_source_code1 = outer(source_code1)
new_source_code2 = outer(source_code2)
# 在内部函数地址后加小括号表示调用内部函数
new_source_code1()
print("----------------------------------")
# 在调用函数时,如果函数需要参数就要给出对应数量的参数
new_source_code2("keep","study")

运行结果如下:

总结:装饰器其实是简化了获取闭包函数地址和调用闭包函数的步骤,通过@闭包的父级函数名获取闭包的地址,再通过被指定装饰器的类名直接调用闭包函数中的内容。

实操2:通过装饰器实现实操1中相同的功能。

# 外部函数传递函数地址并返回内部函数的地址
def outer(fun):
    #内部函数用于执行传入的函数并记录日志。*args和**kwargs用于接收任意数量的参数,参考python入门篇
    def log(*args,**kwargs):
        fun(*args,**kwargs)
        print("日志记录中...")
    # 返回内部函数地址
    return log

# 需要插入日志记录的函数
@outer # 该装饰器的作用等同于outer(source_code1),获取了闭包函数的地址
def source_code1():
    print("执行了程序1")

@outer # 该装饰器的作用等同于outer(source_code2),获取了闭包函数的地址
def source_code2(a,b):
    print("执行了程序2,接收的参数为",a,b)

# 通过函数名直接调用闭包函数
source_code1()
print("----------------------------------")
# 在调用函数时,如果函数需要参数就要给出对应数量的参数
source_code2("keep","study")

运行结果如下:

2.2、多个装饰器

概述:装饰器函数的执行顺序是分为(被装饰函数)定义阶段和(被装饰函数)执行阶段的,装饰器函数在被装饰函数定义好后立即执行。

函数定义阶段:执行顺序是从最靠近函数的装饰器开始,自内而外的执行;

函数执行阶段:执行顺序由外而内,一层层执行;

示例

# 函数定义阶段:
# 定义两个装饰器
@mylog
@cost_time
# 上面的两个装饰器相当于:
# fun2 = cost_time(fun2)
# fun2 = mylog(fun2)
# 也相当于:
# fun2 = mylog(cost_time(fun2))
# 定义阶段:先执行cost_time函数,再执行mylog函数
def fun2():
    pass
​
#调用执行阶段
#先执行mylog的内部函数,再执行cost_time的内部函数
fun2()

实操1:为函数添加两个装饰器,查看不同装饰器在定义阶段和执行阶段的先后执行顺序。

import time
# 创建了一个统计耗时的闭包
def cost_Time(fun):
    print("costTime函数 start")
    def inCosstTime(*args,**kwargs):
        print("costTime函数 start")
        start_time = time.time()
        fun(*args,**kwargs)
        end_time = time.time()
        print("总耗时:",end_time-start_time)
        print("costTime函数 end")
    print("costTime函数 end")
    return inCosstTime

# 创建了一个增加日志的闭包
def mylog(fun):
    print("mylog函数 start")
    def inMylog(*args,**kwargs):
        print("mylog函数 start")
        fun(*args,**kwargs)
        print("日志记录中...")
        print("mylog函数 end")
    print("mylog函数 end")
    return inMylog

print("定义阶段:")
# 通过装饰器获取闭包函数的内存地址
@mylog        # 等同于mylog(myfun)
@cost_Time    # 等同于cost_Time(myfun)
def myfun():
    print("主函数 start")
    time.sleep(1)
    print("主程序 end")

print("-------------------------------------")
print("执行阶段:")
myfun()

运行结果如下:

2.3、带参数的装饰器

概述:当通过闭包实现新增功能时,不可避免的有传入参数的需求,装饰器也给出了对应的解决方案。

实操:通过以下案例了解如何让装饰器传递参数,让闭包函数使用传入的参数(就是在闭包的父级目录外层再定义一个函数专门接收参数)。

# 在外层定义一个函数用于接收参数
def get_value(myvalue):
    def mylog(fun):
        def inlog(*args,**kwargs):
            # 执行添加的功能
            print("正在记录日志:")
            print(f"接收到的参数是:{myvalue}")
            # 执行原有程序
            fun(*args,**kwargs)
        # 返回闭包函数的地址
        return inlog
    # 返回闭包函数的父层函数地址
    return mylog

# 在装饰器的后面加一个小括号放参数,和调用函数时传递参数是一个道理
@get_value("牛逼")
def fun():
    print(f"主程序正在执行...")

if __name__ == '__main__':
    fun()

运行结果如下:

2.4、wraps装饰器(了解)

概述:一个函数不止有他的执行语句,还有着 __name__(函数名)和 __doc__(说明文档)等属性,前面学习的案例都会改变这些属性;functool.wraps 可以将原函数对象的指定属性赋值给包装函数对象,默认有module、name、doc,或者通过参数选择。

实操1:(通过实操1和实操2的对比了解wraps装饰器的作用及其用法)。

def mylog(func):
    def infunc(*args, **kwargs):
        """
        添加了日志记录的功能
        """
        print("日志记录...")
        print(f"函数名称:{func.__name__},函数文档:{func.__doc__}" )
        return func(*args, **kwargs)
    return infunc

@mylog  # fun2 = mylog(fun2)
def fun2():
    """
    执行了fun2函数的功能
    """
    print("使用功能2")

if __name__ == '__main__':
    fun2()
    print(f"函数名称:{fun2.__name__},函数文档:{fun2.__doc__}" )

运行结果如下:

实操2:通过wraps装饰器保持闭包函数和增加程序的函数拥有相同的name、doc等参数。

from functools import wraps
def mylog(func):

    @wraps(func)
    def infunc(*args, **kwargs):
        """
        添加了日志记录的功能
        """
        print("日志记录...")
        print(f"函数名称:{func.__name__},函数文档:{func.__doc__}" )
        return func(*args, **kwargs)
    return infunc

@mylog  # fun2 = mylog(fun2)
def fun2():
    """
    执行了fun2函数的功能
    """
    print("使用功能2")

if __name__ == '__main__':
    fun2()
    print(f"函数名称:{fun2.__name__},函数文档:{fun2.__doc__}" )

运行结果如下:

2.5、回顾学习过的装饰器

2.5.1、property装饰器

概述:property装饰器用于类中的函数,使得可以像访问属性一样来获取一个函数的返回值。

class User:
  def __init__(self,name,month_salary):
    self.name = name
    self.month_salary = month_salary

  @property
  def year_salary(self):
    return int(self.month_salary)*12

if __name__ == '__main__':
  u1 = User("muxikeqi","3000")
  # 由于year_salary方法被@property装饰器所修饰,所以可以像调用属性一样调用方法
  print(f"{u1.name}的年薪为{u1.year_salary}")

运行结果如下:

2.5.2、staticmethod装饰器

概述:staticmethod装饰器同样是用于类中的方法,这表示这个方法将会是一个静态方法,意味着该方法可以直接被调用无需实例化,但同样意味着它没有 self 参数,也无法访问实例化后的对象。

class Person:
  @staticmethod
  def say_hello():
    print("hello everyone!")

if __name__ == '__main__':
  Person.say_hello()

运行结果如下:

2.5.3、classmethod装饰器

概述:classmethod装饰器 是一个类方法。该方法无需实例化,没有 self 参数。相对于 staticmethod 的区别在于它会接收一个指向类本身的 cls 参数。

class Person:
  @classmethod
  def say_hello(cls):
    print(f"hello {cls.__name__}")

if __name__ == '__main__':
  Person.say_hello()

运行结果如下:

2.6、类装饰器

概述:前面学习的装饰器都是函数来完成的,其实用类也可以实现装饰器。类能实现装饰器的功能, 是由于调用一个对象时,实际上调用的是它的 __call__ 方法。

实操1:定义任意一个类,重写该类的__call__方法,然后创建该类的一个实例对象,尝试直接调用创建的实例对象并查看结果(本例用于理解调用对象就是调用该类的__call__方法)

# 创建一个类
class Student():
    def __call__(self, *args, **kwargs):
        print("__call__方法被调用")

# 创建一个实例对象
s = Student()
# 调用该实例对象
s()

运行结果如下:

实操2:通过类装饰器为fun1函数添加一个日志记录的功能。

class myLog:
    def __init__(self,fun):
        self.fun = fun

    def __call__(self, *args, **kwargs):
        print("日志记录中...")
        return self.fun(*args,**kwargs)
@myLog # fun1 = myLog(fun1)
def fun1():
    print("功能1被调用。")

fun1()

运行结果如下:

2.7、装饰器练习

实操:通过类装饰器和函数装饰器为某一个耗时且返回固定值的函数添加缓存功能和计时功能,调用两次该函数,打印运行时间和返回值。

import time
# 缓存类
class CacheDecorator:
    # 定义一个私有类属性,代表缓存(字典类型)
    __cache = {}
    def __init__(self,fun):
        self.fun = fun

    def __call__(self, *args, **kwargs):
        # 如果某个函数名在缓存中有记录就直接使用缓存中函数名对应的返回值
        if self.fun.__name__ in CacheDecorator.__cache:
            return  CacheDecorator.__cache[self.fun.__name__]
        # 如果缓存中没有某个函数的函数名,就运行该函数并将函数名和返回结果保存到缓存中
        else:
           # 运行传入的函数
           result = self.fun()
           # 将函数名和返回值保存到缓存中
           CacheDecorator.__cache[self.fun.__name__] = result
           # 返回返回值
           return result

# 计时函数
def Cost_time(fun):
    def infun(*args,**kwargs):
        start_time = time.time()
        # 运行传入的函数并将返回值保存
        result = fun()
        end_time = time.time()
        print(f"总耗时:{end_time-start_time}")
        # 最后将函数返回值返回
        return result
    # 闭包要返回内部函数的地址方便调用
    return infun

# 通过函数装饰器为函数添加计时功能
@Cost_time # Cost_time(fun_long_time)
# 通过类装饰器为函数添加了缓存功能
@CacheDecorator # CacheDecorator(fun_long_time)
def fun_long_time():
    """
    该函数用于模拟每次耗时都较长,并且每次都返回同一个结果的情况
    """
    print("fun_long_time start")
    # 休眠5秒用于模拟程序执行时间
    time.sleep(5)
    print("fun_long_time end")
    # 返回任意一个固定值(这个值是模拟的,所以可以随机定)
    return 1

if __name__ == '__main__':
    # 第一次调用
    result1 = fun_long_time()
    # 第二次调用
    result2 = fun_long_time()
    # 打印两次返回的返回值
    print(f"第一次执行的返回值为:{result1}")
    print(f"第二次执行的返回值为:{result2}")

运行结果如下:

3、生成器,迭代器,动态性

3.1、生成器(Generator)

3.1.1、初识生成器

概述:Python中一边循环一边计算的机制,称为生成器(Generator)。

产生的原因:列表所有数据都在内存中,如果有海量数据的话将会非常耗内存。如果仅需要访问前面几个元素,那后面绝大多数元素占用的空间都浪费了。如果列表元素按照某种算法推算出来,那我们就可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的列表,从而节省大量的空间。

特点

①、时间换空间,想要得到庞大的数据,又想让它占用空间少,那就用生成器。

②、采用延迟计算,需要的时候,再计算出数据。

创建方式

①、把一个列表推导式的 [ ] 改成 ( ) ,就创建了一个生成器。

②、在函数内部添加一个yield关键字,调用该函数就是创建一个生成器。

注意:直接打印生成器的结果是一个对象,如果需要生成器的数据可以通过循环用到一个取一个。

实操:同时创建一个列表推导式和一个生成器,要求采用创建方式①创建生成器,用于计算1~9的平方。

# 通过列表推导式创建
lis = [x**2 for x in range(1,10)]
print(lis)
# 通过生成器创建
gener = (y**2 for y in range(1,10))
print(f"直接打印生成器的结果为:{gener}")
print("通过循环打印生成器的值:",end="")
for i in gener:
    print(i,end="   ")

运行结果如下:

3.1.2、通过yield创建生成器

概述:①、如果一个函数中包含 yield 关键字,那么这个函数就不再是一个普通函数,调用函数就是创建了一个生成器(generator)对象。

②、生成器函数其实就是利用关键字 yield 一次性返回一个结果,阻塞,重新开始.

原理:①、生成器函数返回一个迭代器,for循环对这个迭代器不断调用 __next__() 函数,不断运行到下一个yield语句,一次一次取得每一个返回值,直到没有yield语句为止,最终引发Stoplteration异常。

②、yield相当于return语句返回一个值,并且记住这个返回的位置,下次迭代时,代码从yield的下一条语句(不是下一行)开始执行。

③、send()和next()一样,都能让生成器继续往下走一步(下次遇到yield语句停),但send()能传一个值,这个值作为yield表达式整体的结果。

注意:生成器推导式底层原理也是如此。

实操:通过yield语句创建一个生成器,用于打印0~2的值,每挂起一次再打印对应的信息并自增,然后通过__next__()方法去唤醒程序继续打印,程序结束后抛出程序运行结束的信息。

"""
1、调用存在yield语句的函数就是创建一个生成器
2、yield语句用于挂起程序并返回相应的值
3、生成器通过__next__()方法运行到下一个yield语句处时,是从上一个yield的下一句开始执行,而不是从上一个yield的下一行开始执行
4、return在生成器中代表终止,直接报错StopIteration并抛出return的返回值
5、__next__()方法用于唤醒生成器并执行到下一个yield语句
"""
def t1():
    print("function start")
    i = 0
    while i<3:
        # 通过yield返回i并挂起(暂停)
        yield i
        # 当调用__next__方法后从这里开始执行
        print(f"i{i+1}:{i}")
        # 对i进行自增(注意:python不支持java的++)
        i+=1
    print("function end")
    # 生成器碰到return语句就结束程序并抛出返回的异常
    return "程序运行结束"

if __name__ == '__main__':
    # 创建生成器对象(函数中包含yield关键字)
    a = t1()
    # 打印生成器对象
    print(a)
    # 迭代器通过__next__()方法执行到下一个yield处
    a.__next__()
    a.__next__()
    a.__next__()
    # 当循环结束时就意味着没有yield语句了,导致报错StopIteration
    a.__next__()

运行结果如下:

3.1.3、生成器中send的用法

实操:通过yield创建一个生成器,并通过next()方法,send()方法和__next__()方法使程序运行到下一个yield语句处,理解三个不同方法的异同。

def t2():
    print("function start")
    i = 0
    while i<3:
        # 第一次next()方法到yield i时结束,第二次send()方法从为tem赋值开始,所以第一次打印的tem的值为666
        # 由于yield将i值返回给了调用函数,并且__next__方法不能传入任何值,导致tem赋值为None
        tem = yield i
        print(f"tem:{tem}")
        i+=1
    print("function end")
    return "程序运行结束"

if __name__ == '__main__':
    # 创建生成器对象
    res = t2()
    # 利用next方法运行到下一个yield处
    next(res)
    # 利用send方法传入一个值赋值给tem并运行到下一个yield
    res.send(666)
    # 利用__next__方法运行到下一个yield处
    res.__next__()
    # 再次调用,循环结束,没有下一个yield导致报错并抛出return语句的信息
    # res.__next__()

    # 调用for循环发现它的作用和next()或__next__()方法一样但不会报错
    for r in res:
        print(r,end="  ")

运行结果如下:

总结:生成器仅仅保存了一套生成数值的算法,并且没有让这个算法现在就开始执行,而是我什么时候调它,它什么时候开始计算一个新的值,并给你返回。

特点:①、普通函数生成一个结果,而生成器函数生成一系列结果,通过yield关键字返回一个值后,还能从其退出的地方继续运行,可以随时间产生一系列的值。

②、生成器和迭代是密切相关的,迭代器也有一个__next__成员方法,这个方法要么返回迭代的下一项,要么引起异常结束迭代。

③、生成器是一个特殊的程序,可以被用作控制循环的迭代行为,python中生成器是迭代器的一种,使用yield返回值函数,每次调用yield会暂停,而可以使用next()函数和send()函数恢复生成器。

④、生成器看起来像是一个函数,但表现得却像是迭代器。

3.2、迭代器(Ieratable)

3.2.1、初识迭代器

概述:①、迭代器是Python最强大的功能之一,是访问集合元素的一种方式。

②、迭代器是一个可以记住遍历的位置的对象。

③、迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。

④、迭代器只能往前不会后退。

⑤、迭代器有两个基本的方法:iter() 和 next() 。

可迭代对象和迭代器的区别

①、实现了iter方法的对象称为可迭代对象。

②、实现了next方法的可迭代对象称为迭代器。即同时实现iter方法和next方法的对象称为迭代器。

注意:生成器是一种迭代器,而列表,字典和字符串是一个可迭代对象。

实操:通过实操判断列表是一个迭代器还是一个可迭代对象。

# python3.6之前不加.abc,之后的加
from collections.abc import Iterator
from  collections.abc import Iterable

# 判断列表是否是一个迭代器
a = isinstance([],Iterator)
# 判断列表是否是一个可迭代对象
b = isinstance([],Iterable)
print(f"列表是一个迭代器:{a}")
print(f"列表是一个可迭代对象:{b}")

运行结果如下:

知识点补充:当把可迭代对象传入iter方法后就是一个迭代器,例如列表是一个可迭代对象,但iter(列表)就是一个迭代器了。

for循环的本质

for i in [1,2,3,4,5,6]
    pass

上面的程序等同于下面的程序:

# 首先获取迭代器对象(传入iter方法的可迭代对象就转换成了迭代器)
ite = iter([1,2,3,4,5,6])
# 进入循环遍历每一个元素
while True:
    try:
        i = next(ite)
    except StopIteration:
        # 遇到StopIteration就退出整个循环
        break

3.2.2、手搓一个迭代器

概述:将一个类作为迭代器需要实现__next__()方法和__iter__()方法,__iter__()方法返回一个特殊的迭代器对象, 这个迭代器对象实现了__next__()方法并通过 StopIteration 异常标识迭代的完成,而__next__()方法会返回下一个迭代器对象。

实操:创建一个依次返回1,3,5,7,9的迭代器。

# 创建一个名为Numbers的类
class Numbers:
    def __iter__(self):
        self.number = 1
        return self

    def __next__(self):
        if self.number<10:
            i = self.number
            self.number += 2
            return i
        else:
            raise StopIteration

# 创建Numbers类的实例
cla = Numbers()
# 使用iter()函数创建一个可迭代对象,并将迭代器对象应用于名为cla的实例
ite = iter(cla)
# 通过next()方法从迭代器中获取下一个元素并打印
print(next(ite))
print(next(ite))
print(next(ite))
print(next(ite))
print(next(ite))

运行结果如下:

3.3、Python动态语言特性

概述:Python是一个动态语言,运行时可以改变其结构。

3.3.1、动态添加属性和方法

概述:动态编程语言是高级程序设计语言的一个类别,在计算机科学领域已被广泛应用。它是指在运行时可以改变其结构的语言 :例如新的函数、 对象、甚至代码可以被引进, 已有的函数可以被删除或是其他结构上的变化。

实操1:运行过程中为对象添加属性和实例方法。

import types
class Person():
    def __init__(self,name,age):
        self.name = name
        self.age = age

# 创建两个实例对象
p1 = Person("muxikeqi",20)
p2 = Person("niko",21)

# 动态的为对象添加属性
p1.money = 128
print(f"{p1.name}的余额为{p1.money}")

# 动态地为对象添加方法
# 定义要添加的方法
def relax(person):
    print(f"{person.name}放松中...")
# 为对象添加方法需要通过如下方法绑定要添加的函数和已有对象
p1.relax = types.MethodType(relax,p1)
# 调用为对象添加到方法验证是否添加成功
p1.relax()

运行结果如下:

实操2:为类添加静态方法和类方法

# 定义一个类
class Person():
    def __init__(self,name,age):
        self.name = name
        self. Age = age

# 创建实例对象
p1 = Person("muxikeqi",18)

# 定义一个静态方法
@staticmethod
def staticfun():
    print("静态方法运行中...")

# 定义一个类方法
@classmethod
def classfun(cls):
    print("类方法运行中...")

# 为类添加静态方法(左边的函数名可以随意)
Person.staticfunc = staticfun
# 通过类调用添加的静态方法
Person.staticfunc()

# 为类添加类方法(左边的函数名可以随意)
Person.cls = classfun
# 通过类名调用类方法
Person.cls()

# 通过Person类的实例对象去调用为Person类动态添加的方法
p1.staticfunc()
p1.cls()

运行结果如下:

3.3.2、slots限制成员属性和方法

概述:①、__slots__对动态添加对象的成员变量、成员方法有限制。对动态添加类属性、类方法没有限制。

②、__slots__只对本类有限制,不限制子类。

实操:通过实例了解__slots__的用法。

class Person():
    __slots__ = {"name","age"}
    def __init__(self,name,age):
        self.name = name
        self.age = age

if __name__ == '__main__':
    # 创建实例对象
    p1 = Person("muxikeqi",20)
    # 动态的为p1对象添加属性
    p1.money = 123

运行结果如下:

此结果说明我们通过__solots__成功限制了动态地为对象添加属性。

4、正则表达式

4.1、初识正则表达式

概述:正则表达式是一种文本模式,正则表达式使用单个字符串来描述、匹配某个或一系列句法规则的字符。

作用:①、匹配和替换文本字符串;②、验证表单输入;③、提取字符串中的信息。

参考网站

特点:①、可读性较差。②、通用性强,适用于很多语言(Python,Java,JavaScript...)。

4.2、re模块

概述:Python通过re模块实现对正则表达式的支持,re模块提供了常见的正则匹配操作,如:匹配、搜索、替换等功能,本节练习都通过精准匹配(写什么内容就匹配一摸一样的内容)的方式进行匹配,主要是为了熟悉re提供的方法。

4.2.1、match实现匹配操作

概述:re.match() 从字符串的开始进行匹配,如果开始部分匹配成功就返回匹配对象,否则返回None。

参数说明:re.match()方法接收两个参数,第一个参数为匹配的规则(要找以什么字符串开始的内容),第二个参数为用于匹配的字符串。

实操:通过re.match()查找搜索内容是否在目标字符串的开头。

import re

m1 = re.match("keep","muxikeepstudy")
m2 = re.match("muxi","muxikeepstudy")
print(m1)
print(m2)

运行结果如下:

4.2.2、search实现搜索操作

概述:re.search() 从字符串的开始进行匹配,如果找到匹配就返回匹配对象,否则返回None。

参数说明:re.search()方法接收两个参数,第一个参数为匹配的规则(正则表达式),第二个参数为用于匹配的字符串。

实操:通过re.search()搜索字符串中任意匹配的字符串内容。

import re

m1 = re.search("keep","muxikeepstudy")
print(m1)

运行结果如下:

4.2.3、findall实现提取操作

概述:re.findall() 查找所有匹配项,并且将所有的查找结果以列表的形式返回,如果没有查找到匹配项,就返回一个空列表。

参数说明:re.findall()方法接收两个参数,第一个参数为匹配的规则(正则表达式),第二个参数为用于匹配的字符串。

实操:通过re.findall()方法提取字符串中符合匹配项的所有内容。

import re 
# 模拟有匹配项的情况
m1 = re.findall("muxi","muxikeepstudymuxikkk")
print(m1)
# 模拟没有匹配项的情况
m2 = re.findall("abc","abdefghijklmn")
print(m2)

运行结果如下:

4.2.4、finditer实现提取操作

概述:re.finditer() 查找所有匹配的字符串,返回所有匹配结果的一个迭代器。

参数说明:re.finditer()方法接收两个参数,第一个参数为匹配的规则(正则表达式),第二个参数为用于匹配的字符串。

实操:通过re.finditer()方法提取字符串中符合匹配项的所有内容并打印其报错的内容。

import re
m1 = re.finditer("muxi","muxikeepstudymuxikkk")
print(m1)
# 通过遍历打印迭代器中的内容
for i in m1:
    print(i)

运行结果如下:

4.2.5、sub实现替换操作

概述:re.sub() 用于替换匹配的字符,返回替换后的字符串。

参数说明:re.finditer()方法接收三个参数,第一个个参数为匹配的规则(正则表达式),第二个参数为要替换的字符串,第三个参数为要匹配的字符串。

实操:通过re.sub()方法将字符串中所有 " muxi " 替换成 " MUXI "。

import re
m1 = re.sub("muxi","MUXI","muxikeepstudymuxikkk")
print(m1)

运行结果如下:

4.3、匹配单个字符

概述:大多数字、字母和符号都可以匹配自身,但部分字符在正则表达式中有特殊含义,如果想匹配这些字符,只需要在符号前上一个 \ 即可。

# 正则表达式中拥有特殊含义的字符
. ^ $ * + ? { } [ ] \ | ( )

正则表达式的匹配字符

代码功能
.匹配任意1个字符(除了\n)
[ ]匹配[ ]中列举的字符
\d匹配数字,即0-9
\D匹配非数字,即不是数字
\s匹配空白,即 空格,tab键
\S匹配非空白
\w匹配非特殊字符,即a-z、A-Z、0-9、_、汉字
\W匹配特殊字符,即非字母、非数字、非汉字

实操1:通过精准匹配的匹配方式,匹配以$test开头的内容(匹配特殊字符)。

import re
# $具有特殊含义,需要在$前加\
m1 = re.match("\$test","$teststring")
print(m1)

运行结果如下:

实操2:通过 . 匹配任意字符。

import re
m1 = re.match("m.","muxikeqi keep!")
print(m1)

运行结果如下:

实操3:通过 [ ] 匹配字符串中以m或M开头的字符。

import re
m1 = re.match("[mM]","Muxikeqi")
print(m1)

运行结果如下:

实操4:通过 [ ] 匹配字符串中是否以字母或数字开头,如果符合就打印这个匹配的字符。

import re
m1 = re.match("[0-9a-zA-Z]","muxikeqi996")
print(m1)

运行结果如下:

实操5:通过 \d 获取字符串中是数字的匹配字符。

import re
m1 = re.findall("san\d","zhangsan996")
print(m1)

运行结果如下:

实操6:通过 \s 获取字符串中san后带有空格的字符串。

import re
m1 = re.findall("san\s","zhangsan san1san   sansansan\t")
print(m1)

运行结果如下:

实操7:通过 \S 匹配字符串中的特殊字符。

import re
m1 = re.findall("san\S","zhangsan$996")
print(m1)

运行结果如下:

实操8:通过 \w 匹配以中英文字母 或 数字 或 _ 中任意字符开头的字符,并打印这个字符。

import re
m1 = re.match("\w","muxikeqi")
print(m1)

运行结果如下:

实操9:通过正则表达式匹配字符串开头的任意字符。

import re
# 技巧:通过[\d\D] [\s\S] [\w\W]都可以匹配任意字符
print(re.match("[\d\D]","&python"))
print(re.match("[\s\S]","!python"))
print(re.match("[\w\W]","python"))

运行结果如下:

4.4、匹配多个字符

概述:正则表达式的强大之处在于可以指定正则的某部分必须重复一定的次数。

代码功能
*匹配前一个字符出现0次或者无限次,即可有可无
+匹配前一个字符出现1次或者无限次,即至少有1次
?匹配前一个字符出现1次或者0次,即要么有1次,要么没有
{m}匹配前一个字符出现m次
{m,n}匹配前一个字符出现从m到n次

实操1:匹配首字母为大写,后面的字母为小写的字符串,并将匹配的字符串打印出来。

import re
print(re.match("[A-Z][a-z]*","MuxikeQi").group())

运行结果如下:

程序说明:由于[A-Z]没有指定次数,对应前面匹配单个字符的学习经验,这是一个精准匹配,第一个字母一定是A-Z这个范围中的字母,然后[a-z]*指定了*,表示0次或无数次,即从第二个字符开始,a-z的字母出现0次或无数次,当取到字母Q时,程序发现不满足正则表达式的要求,终止程序,最终取出 " Muxike "。

实操2:匹配一个字符串,要求首字母为M,最后一个字母为q,中间至少有任意一个字符,打印匹配到的字符串。

import re
print(re.match("M.+q","Muxike123$qi").group())

运行结果如下:

实操3:匹配一个网址,已知网址开头可以为http,也可以为https,打印匹配到的网址。

import re
print(re.match("https?://.*","https://muxikeqi").group())

运行结果如下:

实操4:现有一串乱码 " muxi1233#$^&*() ",要求获取乱码中从开头到结尾为字母,数字或下划线的字符,最少要8位字符,最多要15位字符。

import re
print(re.match("\w{8,20}","muxi1233#$^&*()").group())

运行结果如下:

知识点补充:在正则表达式中,{n,}表示匹配n次到无数次;{,n}表示匹配0次到n次。

4.5、贪婪模式与非贪婪模式

概述:在正则表达式中,贪婪模式和非贪婪模式描述的是量词的匹配方式,贪婪模式指量词会尽可能多的匹配字符,非贪婪模式指量词会尽可能少的匹配字符。

知识点补充:在正则表达式中,贪婪模式是默认的,可以通过在量词后面加 ?将其转换为非贪婪模式。

实操:通过正则表达式的贪婪模式和非贪婪模式匹配如下字符串: "<h1>hello</h1><h1>world</h1>"。

import re
# 用于匹配的字符串
s = "<h1>hello</h1><h1>world</h1>"
# 贪婪模式
print("贪婪模式:",re.match("<h1>.+</h1>",s).group())
# 非贪婪模式
print("非贪婪模式:",re.match("<h1>.+?</h1>",s).group())

运行结果如下:

使用建议

①、如果需要匹配尽可能长的字符,则使用贪婪模式;

②、如果需要匹配尽可能少的字符,则使用非贪婪模式;

③、在某些情况下,非贪婪模式可以避免正则表达式匹配超出我们预期的字符,产生错误。

4.6、匹配开头与结尾

代码功能
^匹配字符串开头
$匹配字符串结尾

实操1:利用正则表达式判断字符串是否以数字开头,如果以数字开头就打印匹配到的字符串,如果不以数字开头就打印匹配失败。

import re
result = re.match("^\d.*","666donk")
if result:
    print(result.group())
else:
    print("匹配失败")

运行结果如下:

实操2:利用正则表达式判断字符串是否以数字结尾,如果以数字结尾就打印匹配到的字符串,如果不以数字结尾就打印匹配失败。

import re
r = re.match(".*\d$","donk666")
if r:
    print(r.group())
else:
    print("匹配失败")

运行结果如下:

注意:^除了有指定字符开头的功能外,[^指定字符] 表示除了指定字符都匹配。

实操3:匹配所有除a,b,c开头的字符串。

import re
print(re.match("[^abc].*","muxikeqi253*&^").group())

运行结果如下:

4.7、匹配分组

概述:当我们的需求是提取字符串中某一部分的内容时,就可以考虑使用分组。

代码功能
|匹配左右任意一个表达式
(ab)将括号中字符作为一个分组
\num引用分组num匹配到的字符串
(?P<name>)分组起别名为name
(?P=name)引用别名为name分组匹配到的字符串

实操1:定义一个列表代表学习计划的科目,然后通过 | 来指定科目,如果指定的科目在学习计划中就打印该科目在最近的学习计划中,如果没有通过 | 指定就打印该科目目前不在学习计划内。

import re
study_lis = ["C++","JAVAEE","Redis","Python"]
for lis in study_lis:
    r = re.match("Redis|Python",lis)
    if r:
        print(f"{lis}在最近的学习计划中")
    else:
        print(f"{lis}不在最近的学习计划中")

运行结果如下:

实操2:现有一个列表存放了多个邮箱,请利用分组过滤出163,qq和gmail的邮箱。

import re
mailbox_lis = ["hello@qq.com","donk@gmail.com","muxi@193.com"]
for l in mailbox_lis:
    # 1、利用()进行分组;2、.有特殊含义需要用\转换为普通字符。
    r = re.match(".{1,10}@(qq|gmail|163)\.com",l)
    if r:
        print(l)
    else:
        print(f"暂不支持该格式的邮箱({l})")

运行结果如下:

实操3:现有一个列表中存储了电话信息,存储格式为电话:电话号码,请对电话和电话号码进行分组,分组完成后如果匹配就打印匹配信息,第一个分组和第二个分组信息,如果不匹配就打印电话格式有误。

import re
tel_lis = ["电话:10086","电话:19389890909","电话:17509090909","电话:19935781896"]
for l in tel_lis:
    r = re.match("(.{2}):(1\d{4,10})",l)
    if r:
        # 获取匹配的所有字符信息
        print(r.group())
        # 获取第一个分组(分组序号从1开始)
        print(r.group(1))
        # 获取第二个分组
        print(r.group(2))
    else:
        print("电话格式有误")

运行结果如下:

实操4:利用分组对前端任意标签进行匹配(如:<title>donk666</title>),要求第二个标签使用前面分组的标签,达到简化代码的效果,最后打印匹配到的信息以及各个分组的信息。

import re
# 由于尖括号里的内容几乎一样,所以这里对第一个尖括号分组,后面的尖括号直接拿第一个分组的内容加一修改即可
# 1-6是因为标签中用到的数字最多为6,如h6
# \在程序中有特殊的含义,常用来作为转义字符,所以这里要在\前加上\表示这是一个普通字符
r = re.match("<([a-zA-Z1-6]+)>(.+)</\\1>","<title>donk666</title>")
if r:
    print(r.group())
else:
    print("匹配失败")

运行结果如下:

实操5:在实际开发过程中,对前端代码进行匹配时可能有多个标签,此时可以考虑对分组取别名,这样就不用一个一个数分组的序号,提高编码效率及其清晰度,要求对实操4进行取别名操作并通过别名应用分组。

import re
# 通过?P<别名>的方式在分组中取别名,通过(?P=别名)的方式调用别名的分组内容
# 1-6是因为标签中用到的数字最多为6,如h6
# \在程序中有特殊的含义,常用来作为转义字符,所以这里要在\前加上\表示这是一个普通字符
r = re.match("<(?P<my_tag>[a-zA-Z1-6]+)>(.+)</(?P=my_tag)>","<title>donk666</title>")
if r:
    # 获取匹配的所有字符信息
    print(r.group())
    # 获取第一个分组(分组序号从1开始)
    print(r.group(1))
    # 获取第二个分组
    print(r.group(2))
else:
    print("匹配失败")

运行结果如下:

4.8、标志的使用

概述:正则表达式标志用于修改正则表达式的匹配方式,以及匹配结果。

Python的re模块支持以下5种标志:

代码功能
re.I(或IGNORECASE)忽略大小写,使匹配对大小写不敏感
re.M(或MULTILINE)多行匹配,修改'^'和'$'的行为
re.S(或DOTALL).可以匹配所有字符,包含换行符
re.U(或UNICODE)使用Unicode匹配
re.X(或VERBOSE)忽略空格和注释,使正则表达式更具有可读性

实操1:正则表达式匹配数据时利用标志忽略大小写。

import re
# 利用re.I或re.IGNORECASE效果一样,都可以实现忽略大小写
r = re.match("[a-z]*","Muxikeqi",re.I)
if r:
    print(r.group())
else:
    print("匹配失败")

运行结果如下:

实操2:通过标志实现正则表达式忽略换行。

import re
# 利用re.S或re.DOTALL效果一样,都可以实现匹配所有字符,包括换行
r = re.match(".*","hello\nMuxikeqi",re.S)
if r:
    print(r.group())
else:
    print("匹配失败")

运行结果如下:

实操3:当正则表达式较复杂时难以阅读可以考虑为代码添加注释,然后通re.VERBOSE或re.X忽略注释即可(下面的例子是故意写得比较复杂,了解re.X的使用场景及其方法)。

import re
r = re.match("""
    # 分组1开始
    (
    # 以小写字母开头,大写字母结尾的情况
    [a-z]+[A-Z]+ 
    # 或者
    | 
    # 以大写字母开头,小写字母结尾的情况
    [A-Z]+[a-z]+
    # 分组1结束
    )
    # 后面紧跟着一个空格
    \s 
    # 对后面跟的内容再做一个分组
    (Muxikeqi)
    # re.VERBOSE与re.X的效果一样
    ""","Hello Muxikeqi",re.X)
if r:
    print(r.group())
else:
    print("匹配失败")

运行结果如下:

5、Python新特性

5.1、字符串格式化输出

概述:formatted字符串是带有'f'字符前缀的字符串,可以很方便的格式化字符串。

实操1:通过该实操了解Pythonh中不同格式控制输出的方法。

name="muxikeqi"
age = 88
print("-"*10,"旧版本用法","-"*10)
# 旧版本用法
# 1、使用%实现格式控制输出
print("我的用户名是%s,年龄%d"%(name,age))
# 2、使用format实现格式控制输出
print("我的用户名是{},年龄{}".format(name,age))

# 新版本用法
print("-"*10,"新版本用法","-"*10)
# 使用formatted实现格式控制输出
print(f"我的用户名是{name},年龄{age}")

运行结果如下:

实操2:通过formatted灵活控制输出变量的值。

a = 1
b = 2
print("-"*10,"旧的用法","-"*10)
# 首先写我们常用的写法
print(f"a={a},b={b}")
print("-"*10,"新的用法","-"*10)
# formatted其实支持特殊的格式控制输出
print(f"{a=},{b=}")
print("-"*10,"新用法再升级","-"*10)
# 在新用法的基础上加入运算
print(f"{a+1=}")

运行结果如下:

实操3:通过该实操对比了解新旧版本对格式控制输出时的对齐操作。

name = "muxikeqi"
# 旧版本使用format进行居中,共对20个字符进行居中,用*号填充缺失的字符
print("{:*^20}".format(name))
# 旧版本使用format进行左对齐操作
print("{:*<20}".format(name))
# 旧版本使用format进行右对齐操作
print("{:*>20}".format(name))
# 新版本用formatted进行居中
print(f"{name:*^20}")
# 新版本用formatted进行左对齐操作
print(f"{name:*<20}")
# 新版本用formatted进行右对齐操作
print(f"{name:*>20}")

运行结果如下:

实操4:通过该实操了解新旧版本对浮点数做格式化控制(保留n位小数)。

score = 98.76543
# 利用format进行格式控制,保留两位小数
print("{:.2f}".format(score))
# 利用formatted进行格式控制,保留三位小数
print(f"{score:.3f}")

运行结果如下:

5.2、字符串新方法

方法名功能描述
str.removeprefix()如果str以它开头的话,将会返回一个修改过前缀的新字符串,否则它将返回原始字符串。
str.removesuffix()如果str以其结尾,则返回带有修改过后缀的新字符串,否则它将返回原始字符串。

实操:通过该实操了解新增的两个字符串用法。

name = "donk666"
# 删除以do开头的前缀
print("删除do前缀后的name为:",name.removeprefix("do"))
# 删除以666结尾的后缀
print("删除666后缀后的name为:",name.removesuffix("666"))
# 验证两个方法并不会修改原字符串
print(name)

运行结果如下:

5.3、变量类型标注

概述:变量类型注解是用来对变量和函数的参数返回值类型做注解(暗示),帮助开发者写出更加严谨的代码,让调用方减少类型方面的错误,也可以提高代码的可读性和易用性。但是,变量类型注解语法传入的类型表述能力有限,不能说明复杂的类型组成情况,因此引入了 typing 模块,来实现复杂的类型表达。

常用数据类型

TypeDescription
int整型 integer
float浮点数字
bool布尔(int 的子类)
str字符 (unicode)
bytes8 位字符
object任意对象(公共基类)
List[str]字符组成的列表
Tuple[int, int]两个int对象的元组
Tuple[int, ...]任意数量的 int 对象的元组
Dict[str, int]键是 str 值是 int 的字典
Iterable[int]包含 int 的可迭代对象
Sequence[bool]布尔值序列(只读)
Mapping[str, int]从 str 键到 int 值的映射(只读)
Any具有任意类型的动态类型值
Union联合类型
Optional参数可以为空或已经声明的类型
Mapping映射,是 collections.abc.Mapping 的泛型
MutableMappingMapping 对象的子类,可变
Generator生成器类型, Generator[YieldType、SendType、ReturnType]
NoReturn函数没有返回结果
Set集合 set 的泛型, 推荐用于注解返回类型
AbstractSetcollections.abc.Set 的泛型,推荐用于注解参数
Sequencecollections.abc.Sequence 的泛型,list、tuple 等的泛化类型
TypeVar自定义兼容特定类型的变量
Generic自定义泛型类型
NewType声明一些具有特殊含义的类型
Callable可调用类型, Callable[[参数类型], 返回类型]
NoReturn没法返回值

实操前说明:下面的程序都写在C:\Users\muxishiye\Desktop\python\函数式编程目录下的test20.py文件中。

实操1:对简单类型变量做标注。

a:int = 200
b:str = 100

对简单类型变量进行标注时,我们发现程序并不会自动检查输入的类型,即使输入的类型与我们标注的类型不一致也不会提醒,此时我们可以考虑导入mypy模块辅助代码检查。

首先,打开控制台(ALT+F12),输入如下命令导入mypy模块:

pip install mypy

在终端中通过cd命令进入当前文件所在目录,然后通过mypy 当前文件名的语法检查当前文件:

此时发现报错,意思就是我们给变量赋值的时int类型,但我们给这个变量已经标识为str类型了。

接下来修改错误并完善程序:

a:int = 200
b:str = "muxikeqi"
c:float = 1.23456
d:bool = True
e:bytes = b"abc"

再次在控制台用mypy检查错误:

实操2:对复杂类型变量做类型标注。

# python3.10及3.10之后的版本可以直接使用,3.10版本之前需要从typing模块中导入
# 下面演示python3.10之前版本操作的方法(3.10及3.10版本后可以省略导包这一步)
from typing import List,Set,Dict,Tuple
# 将a指定为列表,并且列表的元素指定为int类型
a:List[int] = [123,456,789]
# 将b指定为集合,并且集合的元素指定为float类型
b:Set[float] = {1.2,3.4,5.6,7.8}
# 将c指定为字典,并且字典的键指定为int类型,值指定为str类型
c:Dict[int,str] = {1:"muxi",2:"keqi",3:"donk"}

# 将d指定为元组,并且将元组的元素依次指定为int,str和float类型
d:Tuple[int,str,float] = (1,"hello",2.34)
# 不可变元组的元素类型不能指定为object:error: Incompatible types in assignment (expression has type "tuple[int, int, int]", variable has type "tuple[object]")
# d:Tuple[object] = (1,2,3)

# 利用省略号定义一个可变大小的元组
e:Tuple[int,...] = (1,2,3,4,5)

通过控制台利用mypy检查代码是否存在错误:

总结:根据实操2可知,除了不可变的普通元组外,其他复杂类型的变量都可以指定为object,包括可变元组,因为object是一切类型的父类,而元组不能指定为object的原有是因为元组本身的特性。

5.4、函数参数添加类型标注

实操1:通过该案例了解如何为函数指定参数类型并尝试使用typing模块中的Callable为函数指定参数类型并为函数设置别名方便直接调用。

# 该函数带有两个参数,参数类型分别为int和float类型,函数最后的返回结果为float类型
def addFunc(num1:int,num2:float) -> float:
    return num1+num2

# 模拟上面的程序带默认值的情况
def addFunc2(num1:int,num2:float=123.4) -> float:
    return num1+num2

# 需要从typing导入
from typing import Callable
# 通过 别名:callable() 方法设置对应函数的参数类型和返回值类型 注意:后面跟的是函数地址,带括号是直接调用该函数
add:Callable[[int,float],float] = addFunc
# 通过别名直接调用该函数
print(add(1,2.3))

运行结果如下:

实操2:通过函数定义一个能产生整数的生成器,生成1-10的数据,最后通过循环遍历所有的数据。

from typing import Iterable
def num_lis(num:int) ->Iterable[int]:
    i = 1
    while i<num:
        yield i
        i+=1
lis = num_lis(11)
for l in lis:
    print(l,end="\t")

运行结果如下:

5.5、混合类型检查

概述:混合类型就是指函数的参数可能为多个类型,此时就可以用python提供的方法进行多个类型的参数传递,旧版本使用typing包下的Union[ ]方法实现,新版本中的联合运算符使用“|”线来代替旧版本中Union[ ]方法,使得程序更加简洁。

实操1:使用旧版本的混合类型检查方式。

from typing import Union
def oldFunc(num:Union[int,float]) ->Union[int,float]:
    return num
# 传入int类型的值
print(oldFunc(1))
# 传入float类型的值
print(oldFunc(1.23))

运行结果如下:

实操2:使用新版本的混合类型检查方式。

def newFunc(num:int|float) ->int|float:
    return num
# 传入int类型的值
print(newFunc(1))
# 传入float类型的值
print(newFunc(1.23))

运行结果如下:

5.6、类型别名更改

概述:之前是直接通过不同类型的赋值操作来赋予类型新的名字,在新版本中通过TypeAlias来规定了类型名字的替换。这样操作的优势在于能够让程序开发人员和Python编辑器更加清楚的知道newname是一个变量名还是一个类型的别名,提升程序开发的可靠性。

实操1:通过该案例了解老版本中如何更改类型别名。

oldtype = str
s : oldtype = "hellow"
print("字符串信息:",s)

运行结果如下:

实操2:通过该案例了解如何新版本如何更改类型别名。

from typing import TypeAlias
# 为类型更改别名
newstr:TypeAlias = str
newint:TypeAlias = int
def func(s:newstr,num:newint)->newstr:
    return s+newstr(num)
print(func("donk",666))

运行结果如下:

5.7、二进制数据中1的个数统计

概述:Python新版本中通过调用bit_count函数来统计二进制中数字“1”的个数。

实操:通过Python的新旧版本不同的方法对二进制数据中1的出现个数进行统计。

val = 83
print(bin(val))

# 旧版本方法
print("1出现的次数为:",bin(val).count("1"))

# 新版本方法
print("1出现的次数为:",val.bit_count())

运行结果如下:

5.8、字典新特性

概述1:在Python3.10中,针对于字典的items,keys,values方法都增加了mapping属性,对三个方法调用mapping属性后都会返回原字典数据。

实操1:复习老版本中字典的items,keys和values方法。

# 定义一个字典
dic = {"name":"muxikeqi","age":"1024","score":88}
print("字典数据:",dic)
print("通过items()方法获取所有键值对数据:",dic.items())
print("通过keys()方法获取所有的键值对中的键:",dic.keys())
print("通过values()方法获取所有键值对中的值:",dic.values())

运行结果如下:

实操2:实操新版本中字典的mapping属性。

# 定义一个字典
dic = {"name":"muxikeqi","age":"1024","score":88}
print("字典数据:",dic)
print("调用字典keys().mapping属性:",dic.keys().mapping)
print("调用字典items().mapping属性:",dic.items().mapping)
print("调用字典values().mapping属性:",dic.values().mapping)

运行结果如下:

概述2:新版本中对于zip函数加了strict参数,strict参数就是要严格的通过参数长度的匹配原则,旧版本的zip函数会根据长度最短的参数创建字典。新版本的zip函数中,当设定strict参数为True,则要求zip的输入数必须要长度一致,否则报错。

实操3:练习Python旧版本中zip创建字典对象的方法。

keys = ["name","age","score","sex"]
values = ["donk","17","666"]
dic = dict(zip(keys,values))
print("旧版本中通过zip创建的字典对象:",dic)

运行结果如下:

实操4:通过Python新特性中的strict参数对zip创建字典对象时严格按照参数长度进行匹配。

keys = ["name","age","score","sex"]
values = ["donk","17","666"]
dic = dict(zip(keys,values,strict=True))
print("旧版本中通过zip创建的字典对象:",dic)

运行结果如下(报错说明对两个列表的长度进行了严格对照,由于两个列表的长度不一样所以报错):

5.9、dataclass装饰器

5.9.1、初识dataclass装饰器

概述:dataclass 可以认为是提供了一个简写__init__方法的语法糖,类型注释是必填项 (不限制数据类型时, 添加typing.Any为类型注释), 默认值的传递方式和__init__方法的参数格式一致。

实操1:通过实操了解dataclass装饰器的用法。

from dataclasses import dataclass
@dataclass
class Student():
    name:str
    age:int
    score:float
s = Student("donk",17,98.9)
print(s)

运行结果如下:

dataclass装饰器的参数

dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
key含义
init指定是否自动生成__init__,如果已经有定义同名方法则忽略这个值,也就是指定为True也不会自动生成
repr同init,指定是否自动生成__repr__;自动生成的打印格式为class_name(arrt1:value1, attr2:value2, ...)
eq同init,指定是否生成__eq__;自动生成的方法将按属性在类内定义时的顺序逐个比较,全部的值相同才会返回True
order自动生成__lt____le____gt____ge__,比较方式与eq相同;如果order指定为True而eq指定为False,将引发ValueError;如果已经定义同名函数,将引发TypeError
unsafehash如果是False,将根据eq和frozen参数来生成__hash__:
1. eq和frozen都为True,__hash__将会生成
2. eq为True而frozen为False,__hash__被设为None
3. eq为False,frozen为True,__hash__将使用超类(object)的同名属性(通常就是基于对象id的hash) 当设置为True时将会根据类属性自动生成__hash__,然而这是不安全的,因为这些属性是默认可变的,这会导致hash的不一致,所以除非能保证对象属性不可随意改变,否则应该谨慎地设置该参数为True
frozen设为True时对field赋值将会引发错误,对象将是不可变的,如果已经定义了__setattr____delattr__将会引发TypeError

5.9.2、dataclass成员变量的额外设置

概述

①、可通过dataclass.field方法设置变量的额外功能;

②、field方法中的参数default用于设置默认值,值为指定的具体值;

③、field方法中的参数default_factory用于设置默认值,值为类型名,程序会根据类型名创建一个空的对象,若使用defualt设置需要手动创建空对象。

④、field方法中的参数repr用于设置生成的__repr__方法中是否加入此属性,默认值为True。

实操:通过该实例了解datacalss成员变量的额外设置方法。

from dataclasses import dataclass, field
@dataclass
class Student():
    name:str
    age:int
    score:float
    work1:str=field(default="design")
    work2:str=field(default="run",repr=False)
    language1:str=field(default="")
    language2:str=field(default_factory=str)

s = Student("donk",17,98.9)
print(s)

运行结果如下:

5.9.3、建立类变量

概述:在类中建立变量,默认是成员变量。若需要设置类变量,需要设置类型为ClassVar。

实操:为Student类添加一个类属性,并为类属性添加一个默认值。

from typing import ClassVar
from dataclasses import dataclass, field
@dataclass
class Student():
    name:str
    age:int
    score:float
    # 将country设置为类变量
    city:ClassVar=field(default="长沙")

s = Student("donk",17,98.9)
print(s)
print("类变量city:",Student.city)

运行结果如下:

5.10、字典合并

概述:Python新特性为字典添加两个新的运算符:| 和 |= 。| 运算符用于合并字典(不更改原有字典)。|= 用于更新字典(修改原有字典)。

实操:通过练习了解旧版本中字典合并及新版本中合并字典和更新字典的方法。

# 准备数据
dic1 = {"name":"donk"}
dic2 = {"age":17}
# 旧版本合并字典
dic1.update(dic2)
print(dic1)

# 新版本合并字典(不修改原字典)
dic3 = dic1 | dic2
print(dic3)

# 新版本更新字典(修改原字典)
dic1 |= dic2 # 等价于 dic1 = dic1 | dic2
print(dic1)

运行结果如下:

5.11、match语法

概述:match...case结构化模式匹配,可以匹配字典、类以及其他更复杂的结构。match...case的匹配模式类似于Java或C++中switch的使用。

语法

match 变量:
   case <pattern_1>:
     <action_1>
   case <pattern_2>:
     <action_2>
   case <pattern_3>:
     <action_3>
   case _:
     <action_wildcard>

实操1:通过案例了解match...case的用法。

num = 600
match num:
    case 200:
        print("访问成功")
    case 404:
        print("访问资源不存在")
    case 500:
        print("服务器错误")
    case _:
        print("未知状态")

运行结果如下:

实操2:通过该实例了解match...case的匹配方式。

# 准备3个元组(列表,字典都可以实现)
student1 = ("donk",17,"boy")
student2 = ("magixx",18,"boy")
student3 = ("emma",19,"girl")
def fun(student):
    match student:
        case (name,_,"girl"):
            print(f"{name}是女孩")
        case (name,_,"boy"):
            print(f"{name}是男孩")
        case (name,age,_):
            print(f"{name}现在{age}岁")
# 函数调用(match...case结构类似于if...elif语句,匹配到前面的内容执行玩对应语句块就会结束,而不会继续向下匹配)
fun(student1)
fun(student2)
fun(student3)

运行结果如下:

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沐曦可期

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值