Python 函数和装饰器

函数的定义和调用

定义:def 关键词开头,空格之后接函数名称和圆括号(),最后还有一个英文冒号":"。

函数名:在Python中函数即变量,所以函数名也同样遵循变量的命名约束。数字字母下划线组成,不能以数字开头且应具有描述函数功能的作用。

括号:是必须加的,先别问为啥要有括号,总之加上括号就对了!

注释:每一个函数都应该对功能和参数进行相应的说明,应该写在函数下面第一行。以增强代码的可读性。

调用:就是函数名() 函数名加括号 !不加不执行

来个函数

>>> def bar():
...   print("hello function!
")
... 
>>> bar   # 不加括号给的是这个函数的地址,
<function bar at 0x10da670d0>
>>> bar()  # 加了括号才会被执行
hello function!

我们现在想让一个函数完成两个数的加和操作,即我们自己来实现sum()这个内建函数的功能。

def mysum(a,b):
    sum = a+b
result = mysum(1,2)
print(result)

结果为:None

怎么样才能让他能像sum()一样呢? 现在的函数功能已经完美的实现了,但是没人知道这个函数已经把任务完成了! 怎么证明自己呢,函数要把执行的结果返回让大家看到!

在函数的最后加上 return关键字

def mysum(a,b):
    sum = a+b
    return sum

result = mysum(1,2)
print(result)

结果为:3

详细讨论这个返回值

1、没有return关键字

这样的情况在Python中是被允许的,默认的Python返回了 None

2、有return但是后面没有值

def bar():
    print(“I’m bar!”)
    return
print(bar)

  结果为:None

3、有return有返回值,当然上述例子mysum就是例子,显然是被允许的,并且这个返回值能被一个变量名引用到。

4、有return 有多个返回值

def mysum(a,b):
    sum = a+b
    return a,b,sum

result = mysum(1,2)
print(result)

结果为:(1, 2, 3)

当返回值为多个值的时候Python会把几个对象封装成一个元组,同时我们可以用多个变脸分别一一对应的取到每一个返回值,也可以用一个变量取到这个元组

注意 在Python中,将用逗号 "," 分割的连续的几个值,认为是一个元组

>>> 1,2,3
(1, 2, 3)

序列解压

>>> a,_,_,_,b = [i for i in range(5)]
>>> a
0
>>> b
4
>>> a,*_,b,c = 'unpack string'
>>> a
'u'
>>> b
'n'
>>> c
'g'
>>> _
['n', 'p', 'a', 'c', 'k', ' ', 's', 't', 'r', 'i']
>>> type(_)
<class 'list'>
>>> a,_,c = {1:'a',2:'b',3:'c'}
>>> a
1
>>> c
3
>>> *_,end = (1,2,3,4,5,6)
>>> end
6

return的作用:

当函数被调用时,Python解释器会由上到下的执行函数体中的语句,当执行到 return关键字时即终止函数并返回结果:之后的代码不会被执行!

def mysay():
    print("hello first!")
    print('我是华丽的分割线'.center(30, "*"))
    return
    print("hello second!")
mysay()

  结果为:

hello first!
***********我是华丽的分割线***********

由此可见,return 什么都不写跟 直接不写return 还是有区别的,return可以指明函数在哪里结束,如果没有return 也没用中途报错 函数将执行完整个函数体里的代码。

函数参数

在两数字之和的时候我们看到在函数名后的括号里有两个参数a,b。这两个数称为函数的形参,当函数调用时传入具体的参数称为函数执行时候的实参。

参数可以有多个,定义和调用时,参数都需要用逗号分割。

函数的参数分为:位置参数,关键字参数,默认参数,动态参数

注意:函数传参必须是一个不可变对象的引用或对象本身。因为Python的传参遵循了主流面向对象的传参方式:形参实参共享对象。

就实参而言

位置传参

这种方式,必须严格的按照形参的位置来传递实参,不然,结果将不是我们期望的样子,参数多了少了都会报错

def func(arg1,arg2,arg3)
    pass
func('name','age','salary')     # 此时 :arg1 = 'name'  arg2 = ’age'  arg3 = ’salary'

关键字传参数

这种方式,丝毫不需要在意位置,并且有默认值,即使我们不传入实参,也不会报错。

def func(arg1,arg2,arg3)
    pass
func(arg3=’name',arg2 = ’age', arg1 = ’salary')     # 此时 :arg1 = 'salary’  arg2 = ’age'  arg3 = ’name’

混用

在两种方式混用时,必须遵循,位置传入的参数在前,关键字传入的参数在后,并且,一个形参只能被传入唯一的一个值。

def func(arg1,arg2,arg3)
    pass
func('name',arg3 = ’age',arg2 = ’salary')     # 此时 :arg1 = 'name'  arg2 = ’salary’  arg3 = ’age’

就形参而言

位置参数:位置参数必须被传入单一的对应值,无论用何种方式传参。

关键字参数:形参的关键字参数,也就是形参的默认值。多数时候不需要修改的值我们多设为默认值。

def style(id, color="blue"):
    """这是一个样式控制函数,大多数时候背景颜色为蓝色
        此时 将color 设置为 蓝色
    """
    return id, color
 
print(style(3,'red'))
print(style(3))

注意:尽可能的避开可变类型称为默认值的情况(避开容器类型),因为它们是对象的引用的集合,当参数被传入函数时候,所做的一切操作都不会修改容器本身,而是修改了其中的对象。造成了变化的累积效应。

def init_data(name,lst =[]):
    lst.append(name)
    return lst
 
print(init_data('monkey'))
 
print(init_data('JIAJIA'))
 
['monkey']
['monkey', 'JIAJIA']
# 不是想象中的初始化两个列表,而是在同一个表上累积

形参和实参是共享对象的,lst 是一个可变对象的引用,函数每次调用时默认传的是同一个可变的对象,是同一个地址,每次修改的都是相同的可变对象。

动态参数

def bar(id,age=18,*args,**kwargs):
    print(id,age,args,kwargs)
 
bar(1608,'谁收留我1','谁收留我2','看看谁要我3',sex='famale')

# 1608
# 谁收留我1
# ('谁收留我2', '看看谁要我3')
# {'sex': 'famale'}

位置参数必须要按照位置传参,多余的位置参数会被封装成元组的形式保存在args中,多余的关键字参数会被打包成字典封装在kwargs中。

函数嵌套

函数的嵌套调用就是在函数中调用另一个函数。

def func():
    print('func')
 
def bar():
    print('The bar')
    func()
 
bar()     # 在bar中调用func()

定义

def func():
    funcname = 'func'
    print(funcname)
    def bar():
        funcname = 'bar'
        print(funcname)
        def foo():
            funcname = 'foo'
            print(funcname)
        foo()
    bar()
func()

nonlocal 在嵌套函数中使用 nonloacl 关键字 声明变量为上层函数变量,而非本层函数。

nonlocal的使用规则:

1、他必须在某函数的内嵌函数中使用。

2、在他声明的变量之前当前函数中不能有同名的变量存在。

注意 : 无法绑定到全局变量,只能是局部变量。

def outfunc():
    name = 'outfunc'
    def infunc():
        nonlocal name
        name = 'infunc'
        print(name)
    infunc()
    print(name)
outfunc()

作用链域

name = 'error! '
def outfunc():
    print('Outfunc name ="{}"'.format(name))
 
def func0():
    name = 'monkey'
    def func1():
        print('In func1 name ="{}"'.format(name))
        def func2():
            outfunc()
            print('In func2 name ="{}"'.format(name))
        func2()
    func1()
    outfunc()
func0()

# In func1 name ="monkey"
# Outfunc name ="error! "
# In func2 name ="monkey"
# Outfunc name ="error! "

注意

函数会在执行前检查函数体中变量的定义,如果在本层没有找到,就往它的外一层找,还没找到就再往外层,直到找到全局,还没找到就报错,找到了就使用。

高阶函数

定义:函数的参数或返回值是函数的函数,就称为高阶函数(当返回值为函数本身,则称为函数的递归)。

函数的参数是另一个函数

def bar():
    print("  This is bar!")
 
def foo(func):
    print("This is foo:")
    func()
 
foo(bar)

将 bar 作为函数 foo 的参数传入,可以在foo中执行bar 即 foo 中的func。

函数的返回值是另一个函数

def foo():
    print("This is foo:")
    def foo_inner():
        print("This is foo_inner")
    return foo_inner
ret = foo()
ret()

将 foo_inner 作为返回值传出,在函数外执行。

函数闭包

嵌套函数中,内部函数调用外部函数的变量

def outer():
    name = 'outer'
    def inner():
        print(name)
    print(inner.__closure__)
outer()

# (<cell at 0x100f28408: str object at 0x100e51538>,)

closure方法来判断是否为闭包

以cell 开头,这就是一个闭包 返回的是一个 None 则不是闭包

0x100e51538 是inner的地址

str是使用外层变量的类型 后面是其内存地址

闭包的正确姿势

def outer():
    name = 'outer'
    def inner():
        print(name)
    return inner
func = outer()
func()

这样子来,name 会被长久的保存下来,因为func接受了outer内部函数的地址,而内部函数使用了outer下的变量 name 所以,这个变量得以在内存中长久的保留。

如果在一个内部函数里对在外部作用域(但不是在全局作用域)的变量进行引用,但不在全局作用域里,则这个内部函数就是一个闭包。

实际上,闭包的用处/优点有两条:

从函数外可以读取函数内部的变量,或直接执行内层函数

让这些变量的值始终保持在内存中(也可以理解为保留当前运行环境)

装饰器

装饰器是什么?完成怎么样的功能?

装饰器要求再不修改函数代码,不修改函数调用方式的前提下,为函数添加新的功能。

装饰器的本质是函数

在Python中函数和变量其实本质上是一样的,变量可以指向一个函数对象。函数即变量!

初级版本装饰器

定义一个被装饰的函数:

def func():
    print("被装饰的函数")

版本一

我们想到定义一个 decrator_v1 函数 接受被装饰的函数,加上新功能后返回这个函数的地址。然后再将func变量指向 decrator_v1 函数,参数为 func 貌似大功告成。

def decrator_v1(foo):
    print('这是新功能')
    foo()
    return foo
func = decrator_v1(func)
func()

# 这是新功能
# 被装饰的函数
# 被装饰的函数

  哎呀~ 为什么 跟预期的结果不一样??? 从头到尾的捋了一遍!卧槽 func 函数被执行了两边,并且 第二遍压根就没鸟我们的新功能!

1 当执行func = decrator_v1(func)的时候,调用了decratoe_v1这时执行了我们想要的结果。

2 但是当返回foo 被变量func接受时,我们执行func,就执行了foo,此时的foo就是原来我们定义时候的func函数。因此,出现了两次执行,第二次只执行了func。

3 我们希望把 新功能和func本身封装在一起,要执行一起执行,就不会出现这样扯淡的情况了很显然,很自然我们想到了函数的闭包,封装到一个函数 那么 来吧 让我们封装一下。

版本二

def add_way(func):               #1
    def wrapper():                          #3
        print('这是新功能')                          #6
        func()                                          #7
    return wrapper                              #4
 
func = add_way(func)                 #2
func()  

# 这是新功能
# 被装饰的函数

完美~ 终于完成了使命~ !

解释一下代码:

相对于版本一来说,定义的wrapper函数就是用来解决版本一中调用就立即执行add_way()的问题的。

现在基本的满足了功能上的需求,没有改变调用方式,也没有改变原函数的代码。Python提供了一个 语法糖 "@" 来帮我们完成func = add_way(func)这件事情!

语法糖版本

def add_way(func):
    def wrapper():
        print('这是新功能')
        func()
    return wrapper
 
@add_way
def func():
    print("语法糖版本:被装饰函数")
 
func()

到现在为止,装饰器就基本上成型了~!

回顾一下我们都做了什么:

1、利用高阶函数把 被装饰函数当成参数出入装饰器对象,然后 (函数的嵌套)用函数封装 新增方法(功能)之后把 封装后的对象
2、作为装饰器对象(函数)的返回值,然后将 被装饰函数名作为装饰器对象的引用,这样 就实现了装饰器
不改变函数的代码
不改变方法的调用方式
实现新增功能

再来打磨一下:

被装饰函数属性变化,依靠某些属性工作的模块可能无法工作

print('装饰之后的函数文档',func.__doc__)
print('装饰之后的函数名',func.__name__)
print('装饰之后的函数哈希值',func.__hash__)
结果与被装饰之前是不一样的~ 那么依靠__name__ 属性工作的模块就无法正常工作了~ 

被装饰函数如何传参?

终极版本装饰器

打磨后的装饰器:

注意

1、from functions import wraps 用wraps装饰器 来 装饰 传入我们自定义装饰器要装饰的函数,即接收的那个函数。

2、wrapper 要接受func(被装饰函数的)的所有参数

3、wrapper函数要返回func(被装饰函数的)返回值。

#!/usr/bin/env python3
#_*_ coding: utf-8 _*_
__author__ = "monkey"
 
 
from functools import wraps
# 真正完备的装饰器
# 先来看一下解释器对wraps函数的注释
"""Decorator factory to apply update_wrapper() to a wrapper function
 
   Returns a decorator that invokes update_wrapper() with the decorated
   function as the wrapper argument and the arguments to wraps() as the
   remaining arguments. Default arguments are as for update_wrapper().
   This is a convenience function to simplify applying partial() to
   update_wrapper()."""
 
 
def func( *args,**kwargs):
    '''
    :param args:接受多余的位置参数
    :param kwargs: 接受多余的关键字参数
    :return: 返回拿到的所有参数
    '''
    print('我是func')
    return args,kwargs
 
print('装饰之前的函数文档',func.__doc__)
print('装饰之前的函数名',func.__name__)
print('装饰之前的函数哈希值',func.__hash__)
 
 
def decrator_end(func):               #1
    '''
    :param func: 接受被装饰函数对象,以便与在装饰器内部执行
    :return: 一个封装新功能的对象的地址
    '''
    @wraps(func)        #在这里会把wrap 函数的性质完完整整的转变成func函数的样子
    def wrapper(*args,**kwargs):                          #3
        '''
        wrapper 因为这里的wrapper要接受func传入的所有参数
        :param args: 我的含义大家都知道
        :param kwargs: 我跟上面一样
        :return: 被装饰函数的执行结果
        '''
        print('这是新功能')                          #6
        ret = func(*args,**kwargs)                                          #7
        return ret
 
    return wrapper                              #4
 
func = decrator_end(func)                 #2
x = func('hook','monkey',name='monkey')
print(x)
 
print('装饰之后的函数文档',func.__doc__)
print('装饰之后的函数名',func.__name__)
print('装饰之后的函数哈希值',func.__hash__)

结果:

装饰之前的函数文档
 
    :param args:接受多余的位置参数
    :param kwargs: 接受多余的关键字参数
    :return: 返回拿到的所有参数
     
装饰之前的函数名 func
装饰之前的函数哈希值 <method-wrapper '__hash__' of function object at 0x10969ce18>
这是新功能
我是func
(('hook', 'monkey'), {'name': 'monkey'})
装饰之后的函数文档
 
    :param args:接受多余的位置参数
    :param kwargs: 接受多余的关键字参数
    :return: 返回拿到的所有参数
     
装饰之后的函数名 func
装饰之后的函数哈希值 <method-wrapper '__hash__' of function object at 0x1098bf7b8>

注意观察 装饰之后的属性变化,虽然貌似完全是被装饰函数本身,但是通过__hash__方法我们知道那并不是原来的__hash__事情总不是那么尽善尽美,达到需求即可!这样就可以了,真的要修改__hash__ 可以自己在函数中 重写 类的__hash__方法即使这样 他仍然不是原来的函数,内存不一样,怎么改 都只是很像 改变不了他们是两个对象的事实,但是对于使用而言,对用户 或 调用者透明,封装之后他们是不是一个对象这个问题没人会去关注!所以不必要非苛求完美!

原文地址:https://www.cnblogs.com/monkey-code/p/13159351.html