Python学习3(函数装饰器)

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python学习3(函数装饰器)相关的知识,希望对你有一定的参考价值。

千峰教育b站的课程

函数

函数定义

参数可有可无

def 函数名([参数...]):
    代码

直接打印函数名,会看到函数所在的内存地址,函数名相当于一个变量,指向了这个内存地址

def gen():
    print(1)
print(gen) # <function gen at 0x000002B1092DC268>

调用,函数名()

调用函数时参数的顺序

>>> def test(a,b):
...     print(a,b)
... 
>>> test(1,2)  # 位置参数
1 2
>>> test(b=1,a=2)  # 关键字参数
2 1
>>> 
>>> test(b=1,2)  # 关键字参数写在位置参数之前会导致出错
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument

怎么保证传入的参数符合类型要求
isinstance(变量, 类型)

def get_sum(a,b):
    if isinstance(a, int) and isinstance(b, int):
        print(a + b)
    else:
        print('type error')
get_sum(1.2,2)

参数默认值

在定义参数的时候可以给参数赋默认值,这样在调用的时候可以不传递这个参数
且默认值参数必须在普通参数的后面(其实也很好理解,因为调用函数传递的时候是按位置传递的)

# def get_sum(a, c = 100,b): # 报错
def get_sum(a,b, c = 100): # c给定了默认值100
    if isinstance(a, int) and isinstance(b, int):
        print(a + b + c)
    else:
        print('type error')
get_sum(1,2) #103

关键字参数

调用的时候明确使用参数名赋值,因为默认是按位置顺序赋值的,为了防止因为位置传递发生错误,可以使用关键字参数赋值
例如下面例子中,如果c想用默认值,而d要传递参数,就需要用到关键字参数

def get_sum(a,b, c = 100,d=200):
    if isinstance(a, int) and isinstance(b, int):
        print(a + b + c + d)
    else:
        print('type error')
get_sum(1,2, d = 5)

可变参数

*args 接收单独的变量
**kwargs

*args

考虑场景:如果想用一个函数求两个数、三个数…多个数的和
可以使用可变参数,这样将传入的多个参数形成一个元组,传递给函数
也可以什么都不传,相当于一个空的元组
这种自动形成元组的行为成为装包

def get_sum(*args):
    print(args)
get_sum(1,2)     # (1, 2) 输出的是元组
get_sum(1,2,3,4,5) # (1, 2, 3, 4, 5)
get_sum() # ()

这里是列表

a,*b,c,d = 1,2,3,4,5,6
print(a,b,c,d)
# 1 [2, 3, 4] 5 6

如果有两个可变参数,报错

*a,*b,c,d = 1,2,3,4,5,6  
# SyntaxError: two starred expressions in assignment

拆包

a,b,c = (1,2,3)
print(a,b,c)  # 1 2 3
# 先拆包后装包
*a,b,c = (1,2,3,5,6,7)
print(a,b,c)  # [1, 2, 3, 5] 6 7

在可变参数的函数中传入一个列表,可以看到输出的是只有一个元素的元组
那么怎么才能使得传入的函数的时候是分开的几个数呢?
可以在调用的时候进行拆包,即在调用的时候加*号

def get_sum(*args):
    print(args)
get_sum([1,2,3]) # ([1, 2, 3],)
get_sum(*[1,2,3]) # (1, 2, 3)

def get_sum(a, *args):
    print(a,args)
get_sum(1,2)     # 1 (2,)
get_sum(1,2,3,4,5) # 1 (2, 3, 4, 5)
get_sum([1,2,3]) # [1, 2, 3] ()
get_sum(*[1,2,3]) # 1 (2, 3)

**kwargs

输出是一个字典,而传递给两个位置参数的话会报错,显示需要0个位置参数
那么应该怎么传递呢?
可以传递关键词参数,会转换为字典
kw指的是key word
必须传递关键字参数,会将其转换成key:value的形式,封装到字典里

def show_book(**kwargs):
    print(kwargs)
show_book() # 
show_book('a','b') # 报错:TypeError: show_book() takes 0 positional arguments but 2 were given
show_book(name ='a') # 'name': 'a'

# 拆包传字典
dict1 = 'name':'a', 'num': 2
show_book(**dict1) # 'name': 'a', 'num': 2
# 用一个*拆包,拆出来是两个key值
print(*dict1) # name num
# 直接输出**dict1会报错
# print(**dict1) # TypeError: 'name' is an invalid keyword argument for print()

两种一起用

def show(*args, **kwargs):
    print(args)
    print(kwargs)
show(1,2)  # (1, 2)/n  

dict1 = 'name':'a', 'num': 2
show(1,2,**dict1)
# (1, 2)
# 'name': 'a', 'num': 2

如果很多个值都是不定长参数,那么这种情况下,可以将缺省参数放到 args的后面, 但如果有*kwargs的话,kwargs必须是最后的

def sum_nums_3(a, *args, b=22, c=33, **kwargs):
    print(a)
    print(b)
    print(c)
    print(args)
    print(kwargs)

sum_nums_3(100, 200, 300, 400, 500, 600, 700, b=1, c=2, mm=800, nn=900)

一般情况下默认值参数会放在*args的后面

# c在前面的话,传参的时候第三个参数会赋值给c
def fun(a, b, c = 10, *args, **kwargs):
    print(a,b,c,args,kwargs)
fun(1,2,3,4,5,name='a')
# 1 2 3 (4, 5) 'name': 'a'

# c在可变参数后面,赋值的时候c需要用关键字参数赋值方法
def fun(a, b, *args, c = 10, **kwargs):
    print(a,b,c,args,kwargs)
fun(1,2,3,4,5,name='a')
# 1 2 10 (3, 4, 5) 'name': 'a'

kwargs必须在最后面

返回值

python中函数可以返回多个返回值
如果返回多个值,会将多个值封装到一个元组里返回

def returning():
    return 1,2
res = returning()
a,b = returning() # 1,2
print(res) # (1, 2)

全局变量和局部变量

当函数内出现局部变量和全局变量相同名字时,函数内部中的 变量名 = 数据 此时理解为定义了一个局部变量,而不是修改全局变量的值
(这里和之前学其他语言好像不太一样,在java中的一个方法中可以直接对全局变量进行更改,而在python中不能对函数外的变量直接进行修改,只能做查看的操作;为什么呢,刚刚去写了一段java代码感受了一下,可以做一个不专业的直观的解释,因为java中对变量的声明是需要加变量类型的,所以如果在函数内声明了和全部变量一样的变量,那么在函数内使用的就是这个局部变量,简单来说就是修改变量和声明变量代码是不一样的;而在python中声明变量不需要加变量类型,同样如果在函数中声明了一个和全局变量一样的变量,那么这只能认为是一个声明变量的操作,而不能作为一个修改全局变量的操作,否则就会出现问题,所以在python中函数不能直接对全局变量进行修改)

那么怎么才能在函数中修改全局变量呢?
需要在函数中声明我用的就是全局变量

a = 10
def check():
    global a # 如果不加这句话会报错,因为不能直接对全局变量进行修改
    a -= 10
    print(a)
check() # 0

如果在函数中出现global 全局变量的名字 那么这个函数中即使出现和全局变量名相同的变量名 = 数据 也理解为对全局变量进行修改,而不是定义局部变量
如果在一个函数中需要对多个全局变量进行修改,那么可以一次性全部声明,也可以分开声明

# 可以使用一次global对多个全局变量进行声明
global a, b
# 还可以用多次global声明都是可以的
# global a
# global b

查看所有的全局变量和局部变量

Python提供了两个内置函数globals()和locals()可以用来查看所有的全局变量和局部变量。

def test():
    a = 100
    b = 40
    print(locals())  # 'a': 100, 'b': 40

test()

x = 'good'
y = True
print(globals())  # '__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x101710630>, '__spec__': None, '__annotations__': , '__builtins__': <module 'builtins' (built-in)>, '__file__': '/Users/jiangwei/Desktop/Test/test.py', '__cached__': None, 'test': <function test at 0x101695268>, 'x': 'good', 'y': True

可变数据类型和不可变数据类型

所谓可变类型与不可变类型是指:数据能够直接进行修改,如果能直接修改那么就是可变,否则是不可变
可变类型(修改数据,内存地址不会发生变化)有: 列表、字典、集合
不可变类型(修改数据,内存地址必定发生变化)有:int、float、bool、字符串、元组
(python中有小整数对象池,字符串缓存池)
只有不可变数据类型才需要加global

函数中的注释

在函数的第一行用一个字符串作为函数文档

>>> def test(a,b):
...     "用来完成对2个数求和"  # 函数第一行写一个字符串作为函数文档
...     print("%d"%(a+b))
... 
>>> 
>>> test(11,22)  # 函数可以正常调用
33
>>>
>>> help(test)  # 使用 help 查看test函数的文档说明
Help on function test in module __main__:

test(a, b)
    用来完成对2个数求和

在函数内加一对三引号,会自动出现param…类似的,与java中的/** */相似

def get_info(name: str, age: int):
    """
    接收用户的名字和年龄,拼接一个字符串并返回

    :param name: 接收一个名字
    :param age: 接收用户的年龄,必须是 0-200 间的一个整数
    :return: 返回拼接好的字符串
    """
    return "我的名字叫 %s,今年是 %d 岁" % (name, age)


get_info("吴彦祖", 19)
get_info(520, 19)  # 注意,形参上标注的类型只是提高代码的可读性,并不会限制实参的类型
help(get_info)

引用

sys.getrefcount() 获取引用个数(本身调用这个方法也有一个引用指向目标地址),例如下面的例子 3 + 1 = 4

import sys
list1 = [1,2,3,4]
list2 = list1
list3 = list1
print(sys.getrefcount(list1)) # 4

# del list3
# print(sys.getrefcount(list1)) # 3

del list1 
print(sys.getrefcount(list2)) # 3

嵌套函数

函数只是一段可执行代码,编译后就“固化”了,每个函数在内存中只有一份实例,得到函数的入口点便可以执行函数了。函数还可以嵌套定义,即在一个函数内部可以定义另一个函数,有了嵌套函数这种结构,便会产生闭包问题。

函数嵌套
函数也是一个变量,所以函数内部也可以定义函数,相当于声明了一个变量

如果要修改外部函数的变量,需要在内部函数中声明对外部变量的使用,即加关键字nonlocal

def outer():
    a = 10
    print('outer----hello')
    def inner():  # inner这个函数是在outer函数内部定义的
        b = 20
        nonlocal a # nonlocal表示引用内部函数外侧的局部变量a
        a += b
        print('inner----hello')
    inner()  # inner函数只在outer函数内部可见
    print(inner)   # <function outer.<locals>.inner at 0x00000134E031AEA0>
    # 用来查看局部变量
    print(locals())  # 'a': 10, 'inner': <function outer.<locals>.inner at 0x00000223594EAEA0>

outer()
# inner()  这里会报错,在outer函数外部无法访问到inner函数

闭包

符合以下三个条件是闭包:

是一个嵌套函数
内部函数引用了外部函数的变量
返回值是内部函数

闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数块+引用环境)。

下面这段代码前两个调用中,因为内部函数返回的是函数名,我们知道,相当于一个变量,打印出来是它的地址
而在后两个调用中,因为outer(x)返回的是一个函数名,用函数名调用内部函数,所以返回的是内部函数的返回值,相当于res = outer(x), t = res()

因为无法直接在外部调用内部的inner()函数,所以这里在外部函数中将inner()当做返回值返回,就可以在外部使用这个内部函数

def outer(n):
    num = n
    def inner():
        return num+1
    return inner

print(outer(3))  # <function outer.<locals>.inner at 0x0000020E2905AEA0>
print(outer(5))  # <function outer.<locals>.inner at 0x0000020E2905AEA0>

print(outer(3)()) # 4
print(outer(5)()) # 6

在这段程序中,函数 inner 是函数 outer 的内嵌函数,并且 inner 函数是outer函数的返回值。我们注意到一个问题:内嵌函数 inner 中引用到外层函数中的局部变量num,Python解释器会这么处理这个问题呢? 先让我们来看看这段代码的运行结果,当我们调用分别由不同的参数调用 outer 函数得到的函数时,得到的结果是隔离的(相互不影响),也就是说每次调用outer函数后都将生成并保存一个新的局部变量num,这里outer函数返回的就是闭包。 如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure).

闭包一般在装饰器中使用

装饰器

首先明白,函数名也是一个变量
如下面代码中,将函数名b赋值给a,a变量指向了b函数的地址,因此运行结果是b

def a():
    print('a')


def b():
    print('b')

a = b
a() # b

为了在不修改原来已有函数的基础上,为函数添加新的功能(遵循开放封闭原则),出现了装饰器

为了给房子刷墙,写了一个匿名函数,直接执行发现,貌似没有调用任何函数,但是出现了12的结果,说明@decorator 执行了装饰器函数,装饰器函数的参数是house,即将house函数作为参数传入装饰器中,因此会打印那两行横线,然后return 返回了wapper,house接收这个返回值,house = wrapper
此时,house函数经过了装饰,已经变成了新的函数wrapper,这时候执行house会输出wrapper函数的执行结果

def decorator(func):
    print('--------------1')
    print(func)
    def wrapper():
        print('---------')
        func()
        print('刷墙')

    print('--------------2')
    return wrapper

# house = decorator(house)
@decorator
def house():
    print('毛坯房')

# 输出
# --------------1
# <function house at 0x000002112911AEA0>  (这句话可以看出在执行之前,会进行装饰,将house函数传入装饰器中)
# --------------2

# 装饰后的house变成了wrapper
print(house)  # <function decorator.<locals>.wrapper at 0x000002112911C1E0>  
house()
# 输出
# ---------
# 毛坯房
# 刷墙

装饰器的功能

  • 引入日志
  • 函数执行时间统计
  • 执行函数前预备处理
  • 执行函数后清理功能
  • 权限校验等场景
  • 缓存

带有参数的装饰器

如果原函数有参数,则装饰器内部函数也需要有参数

def check_time(action):
    def do_action(a,b):
        action(a,b)
    return do_action

@check_time
def go_to_bed(a,b):
     print('去睡觉'.format(a,b))

go_to_bed("zhangsan","床上")

不定长参数

def test(cal):
    def do_cal(*args,**kwargs):
    		# 此时传入内部函数的是一个元组,需要拆包
        cal(*args,**kwargs)
    return do_cal

# 体会多个参数的传递,需要*args
@test
def demo(a, b):
    sum = a + b
    print(sum)

# 体会关键字参数的传递,需要**kwargs
@test
def demo2(a, b, c = 4):
	sum = a + b + c
	print(sum)
	
demo(1, 2)
demo2(1, b = 2)

所以一般情况下,装饰器中的参数都会写成这两个可变参数

装饰器修饰有返回值的函数

一般情况下为了让装饰器更通用,可以有return

def t_2(cal):
    def do_cal(*args,**kwargs):
        return cal(*args,**kwargs)  # 需要再这里写return语句,表示调用函数,获取函数的返回值并返回
    return do_cal

@t_2
def demo(a,b):
    return a + b

print(demo(1, 2))  #3

装饰器带参数

了解

执行顺序如下,先检测到outer_check(23),将参数23带入outer_check函数中,执行该函数,返回check_time
然后执行check_time,将play_game传入函数中,返回do_action,此时play_game = check_time

def outer_check(time):
    def check_time(action):
        def do_action():
            if time < 22:
                return action()
            else:
                return '对不起,您不具有该权限'
        return do_action
    return check_time

@outer_check(23)
def play_game():
    return '玩儿游戏'

print(play_game())

带有多个装饰器

可见,先加载最靠近的装饰器,再加载较远的装饰器

# 定义函数:完成包裹数据
def makeBold(fn):
    print(1)

    def wrapped():
        return "<b>" + fn() + "</b>"

    print(2)
    return wrapped


# 定义函数:完成包裹数据
def makeItalic(fn):
    print(3)

    def wrapped():
        return "<i>" + fn() + "</i>"

    print(4

以上是关于Python学习3(函数装饰器)的主要内容,如果未能解决你的问题,请参考以下文章

python之闭包,装饰器

Python 3 学习笔记----装饰器

python学习---装饰器

python中的装饰器

Python学习之路:装饰器前奏

python中的装饰器