首页 > 技术文章 > 函数、参数、解构

shichangming 2019-02-15 14:26 原文

1. 函数的概念

函数就是一个函数对象,函数的定义就是将函数名和函数内存中的函数地址对应起来
函数的数学定义是:y=f(x) ,y是函数名,x是自变量,自变量也可以是多个:y=f(x0,x1,x2,x3 ... xn)
python函数:python的函数的组成有:语句块、函数名、参数列表,返回值。

python中的函数的,目的是完成一定的功能,是结构化编程对代码的最基本的封装,一般按照功能组织一段代码,封装是为了复用,减少代码的冗余,使代码更美观可读。

2. 函数的分类

pythonh中的函数分为:内建函数和库函数

3. 函数的定义和调用

3.1. 函数的定义

python函数是通过 def 关键字定义的,具体的定义语法如下:

def add(x, y):
    res = x + y
    return res

def 是函数定义的关键字,x,y是函数的形式参数,上面的 add 就是定义的函数名,
实际上python中的函数名就是一个标识符(变量名)。定义一个函数后,就在内存中创建了一个函数对象,变量名指向的这个函数对象的内存地址。
python中的函数如果没有显示的return语句,默认就会有一个隐式的return None.

3.2. 函数的调用

定义一个函数只是声明一个函数,并没有执行该函数,我们只有在调用函数后才能发挥函数的功能。
函数的调用方式是在函数名的后面加上小括号,并加上必要的参数。
我们在调用函数的时候传入的参数叫实参。

函数调用的示例

add(1,3)

查看对象是否可调用对象可以使用callable()内建函数

callable(add)
=========================
In [5]: def add(x,y): 
   ...:     res = x + y 
   ...:     return res 
   ...:                  

In [6]: callable(add)    
Out[6]: True

返回 True 表示是一个可调用对象

我们通过print()返回的是函数的内存地址

In [7]: print(add)                    
<function add at 0x7fefc90e5730>

或者直接在ipython中执行add,返回是add的一种可见的字符串模式

In [8]: add                           
Out[8]: <function __main__.add(x, y)>

3.3. 函数的复用

函数是可以被其他函数调用的,函数本身也可以调用其他函数

def add(x, y):
    res = x + y
    raturn res

def new_add():
    return add(6, 9)
a = new_add()

执行的结果是

15

函数必须先定义后调用

其实很容易理解,没有定义怎么调用?

def new_add():
    return add(6, 9)
a = new_add()
=================================
In [5]: def new_add(): 
   ...:     return add(6,9) 
   ...:                                                                      

In [6]: a = new_add()                                                        
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-6-3072819d1adb> in <module>
----> 1 a = new_add()

<ipython-input-5-97d8213b487f> in new_add()
      1 def new_add():
----> 2     return add(6,9)
      3 

NameError: name 'add' is not defined

4. 函数的参数定义和传递

4.1. 函数的参数分类

  • 位置参数
    • 普通位置参数
    • 可变位置参数
  • 关键字参数(目前仅python适用)
    • 普通关键字参数
    • keyword-only参数
    • 可变关键字参数

4.2. 位置参数、关键字参数的定义和传递

# 定义
def fn(x,y,z)    # 没有设置默认值
def fn(x,y,z=5)  # 定义的时候,关键字设置的参数值叫做缺省值,要在放在位置之后。
# 调用
fn(1,2,3)        # 适用于上面两种定义方法的调用,位置参数的位置顺序是一一对应的
fn(1,2)          # 只适用于上面有默认值定义方法的调用,这里没有为z传参,使用的是默认的5
fn(x=1,y=2,z=3)  # 也可以全部使用关键字为位置参数传参,此时传参的顺序没有限制。
  • 传参时候的注意点:
    • 位置参数必须要在关键参数之前传入,位置参数是按位置一一对应的
    • 函数调用的时候传入的参数个数和函数定义的时候的参数的个数要相匹配(可变参数除外)。
    • 不能为已经传过的参数重复传参
    • 关键字传参是按名称对应的,与传参定义的顺序没有关系。
fn(1,2,z=3,x=4)  # 这种就是为x重复传参,前面已经 x = 1 了,后面又来了 x = 5.

参数的默认值的作用:

  • 在未传入足够数量的实参的时候,对没有给定的参数赋值为默认值
  • 当参数非常多的时候,避免用户重复输入相同的参数,简化函数的调用

默认参数的定义和调用的典型示例

# 定义
def longin(host="127.0.0.1",port=80,username='test',password='youdonotknow'):
# 调用
login()    # 全部使用默认参数
login(host='192.168.200.1')   # 只有host使用自定义的参数,其他使用默认参数

4.3. 可变位置参数的定义和传递

  • 在形参之前使用 * 表示该形参是可变的参数,一般使用 *args表示,可以匹配任意多个位置参数
  • 贪婪收集多个位置参数为一个tuple

定义

def add(*args)
    total = 0
    for i in args:
        tatal += i
        return total

传递

add(1,2,3)   # 1,2,3传递进函数之后会被 *args 全部接受并组成一个元组(1,2,3) 

4.4. 可变关键字参数的定义和传递

  • 形参前使用 ** 符号,表示可以接收多个关键字参数
  • 收集的实参名称和值组成一个字典

定义

def fn(**kwargs):
    for k,v in kwargs.items():
        print(k,v)

传参

fn(host='127.0.0.1',port=80,username='test',password='test')

4.5. 可变参数的混用

def fn(username, password, **kwargs):
def fn(username, *args, **kwargs):
def fn(username, **kwargs, *args):   # 错误的写法,*args 不能写在 **kwargs之后。

4.6. 可变参数的总结

  • 可变的参数分为可变的位置参数和可变的关键字参数
  • 位置可变参数在形参前使用一个星号 *
  • 关键字可变参数在形参前使用两个星号 **
  • 位置可变参数和关键字可变参数都可以收集若干个实参,位置可变参数收集成一个tuple,关键字可变参数收集成一个字典
  • 混合使用参数的时候,普通参数需要放在参数列表前面,可变参数要放在参数列表后面,位置可变参数需要在关键字可变参数之前
def fn(x,y,*args,**kwargs):

fn(3,5,7,9,10,a=1,b="python")
fn(3,5)
fn(3,5,7)
fn(x=3,y=4,5,6,a=2,b="test") # 错误的传参,5,6不能放在x=3,y=4之后
fn(7,9,y=5,x=3,a=3,b='test') # 错误的传参,7,9分别赋值给了x,y,但是y=5,x=3又重复赋值了。

4.7. keyword-only 参数

  • 在 *, *args 之后的参数只能是关键字形式的参数
  • 如果在一个星号 * 参数之后或者一个位置可变参数之后,出现的普通参数,实际上以及各不是普通参数,而是一个keyword-only参数。

定义

def fn(*args,x=1,y,**kwargs):   # x和y都是keyword-only参数,keyword-only参数可以设置默认值。

传参

def dn(*args,x):
# args可以看作是已经截获了所有的位置参数,x不是用关键字参数就不可能拿到实参。

思考:下面的定义方式是否可以?

def fn(**kwargs,y): 

上面的定义是不可以的,因为在一个 * 号之后的都是keyword 类型的参数,也就是说 y 也是一个keyword参数。
所以实际上,传参的时候应该是 y=1 这样的,但是这样传参后 y=1就会被 **keyword截获,y 还是拿不到值。所以语法是错误的。

keYword-only参数的另一种形式

def fn(*, x,y):
    print(x,y)
fn(x=t=5,y=6)

* 号之后,普通的参数都变成了keyword-only参数

5. 可变参数的默认值

  • 参数列表参数一般顺序是,普通参数,缺省参数,可变位置参数,keyword-only参数(可带缺省值)、可变关键字参数
  • 代码中的参数定义应该是见名知意,应该有好的代码习惯

下面是几种函数可变参数以及参数默认值的定义以及传参的示例

def fn(*args,x=5):
    print(x)
    print(args)

fn()      # 等价于fn(x=5), x=5  args=()
fn(5)     # 等价于fn(x=5), 传参后,x=5  args=()
fn(x=6)   # 传参后,x=6  args=()
fn(1,2,3,x=10)  # 传参后,x=10  args=(1,2,3)

====================================================
def fn(y,*args,x=5):
    print('x={},y={}'.format(x,y))
    print(args)

fn()     # y 是一个普通的位置参数,必须传入一个
fn(5)    # y=5, x=5,
fn(x=6)  # 没有为 y 设置变量
fn(1,2,3,x=10)  # y=1,x=10 ,args = (2,3)
fn(1,2,y=3,x=10)  # y 重新赋值了
=====================================================
def fn(x=5,**kwargs):
    print('x={}'.format(x))
    print(kwargs)

fn()          # x=5, kwargs = {}
fn(5)         # x=5, kwargs = {}
fn(x=6)       # x=6, kwargs = {}
fn(y=3,x=10)  # x=10, kwargs = {"y":3}
fn(3,y=10)    # x=3, kwargs = {"y":10}
=====================================================
def fn(x,y,z=3,*args,m=4,n,**kwargs):   # 此函数必须为x,y,n传入参数
    print(x,y,z,m,n)    
    print(args)        
    print(kwargs)
=====================================================
def connect(host='127.0.0.1',port=80,user='test',password='test',**kwargs):
    print(host,port)  
    print(user,password)
    print(kwargs)

connect(db='cmdb')    # 127.0.0.1 80 test test 

In [2]: connect(db='cmdb')
127.0.0.1 80
test test
{'db': 'cmdb'}

In [3]: connect(host='192.168.1.1',db='cmdb')
192.168.1.1 80s
test test
{'db': 'cmdb'}

In [4]: connect(host='192.168.1.1',db='cmdb',password='mysql')
192.168.1.1 80
test mysql
{'db': 'cmdb'}

6. 参数的解构

  • 给参数提供参数的时候,可以在集合类型前使用 * 或者 ** , 把集合类型的解构开,取出所有元素作为函数的实参。
  • 非字典类型使用 * 解构成为之参数
  • 字典类型使用 ** 解构成关键字参数
  • 提取出来的元素要和参数的要求匹配,也要和参数的类型匹配

通过几个简单的加法函数来演示

def add(x,y):
    return x + y

add(4,5)     # 正确
add((4,5))   # 不正确,需要解构,(4,5) 是一个参数

t = (4,5)
add(t[0],t[1])
add(*t) 或 add(*(4,5)) add(*[4,5]) add(*(4,6))
add(*range((1,3))
def add(x,y):
    return x+y

add(*(4,5))   # 9
add((*[4,5])) # 9
add(*{4,6})   # 10
d = {"x":1,"y":2}
add(**d)    # 3
add(**{"x":1,"y":2})  # 3
add(**{"a":1,"b":2})  # 报错,因为解构之后是 a=1,b=2,而函数定义的时候是 x,y
add(*{"a":1,"b":2})   # 通过一个 * 解构之后,是只对第一层的 key 解构,解构是 字符串相加 'ab'
def add(*args):
    total = 0
    for i in args:
        total += i
    return total

add(1,2,3)     # 6
add(*(1,2,3))  # 6
add(*range(10))  # 45

7. 函数参数的定义及传参示例

7.1. 示例1

定义

def fn(x,y,*args,**kwargs):
    print('x={}; y={}'.format(x,y))
    print('args   is {}'.format(args))
    print('kwargs is {}'.format(kwargs))

调用传参

fn(1,2,3,4,5)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
x=1; y=2
args   is (3, 4, 5)
kwargs is {}
fn(1,2,3,4,x=5)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
Traceback (most recent call last):
  File "D:/pyporject/test/test6.py", line 8, in <module>
    fn(1,2,3,4,x=5)
TypeError: fn() got multiple values for argument 'x'
fn(y=2,x=1,z=3)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
x=1; y=2
args   is ()
kwargs is {'z': 3}

7.2. 示例2

定义

此时的x,y是keyword-only参数

def fn(*args,x,y,**kwargs):
    print('x={}; y={}'.format(x,y))
    print('args   is {}'.format(args))
    print('kwargs is {}'.format(kwargs))

调用传参

fn(1,2,3,4,5)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
Traceback (most recent call last):
  File "D:/pyporject/test/test6.py", line 7, in <module>
    fn(1,2,3,4,5)
TypeError: fn() missing 2 required keyword-only arguments: 'x' and 'y'
fn(1,2,3,4,5,x=6,y=7)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
x=6; y=7
args   is (1, 2, 3, 4, 5)
kwargs is {}
fn(1,2,3,4,5,x=6,y=7,z=9)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
x=6; y=7
args   is (1, 2, 3, 4, 5)
kwargs is {'z': 9}
fn(x=6,y=7,z=9)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
x=6; y=7
args   is ()
kwargs is {'z': 9}

7.3. 示例3

定义

此时的x,y是keyword-only参数

def fn(*,x,y):
    print('x={}; y={}'.format(x,y))

调用传参

fn(x=6,y=7)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
x=6; y=7

7.4. 示例4

定义

此时的x,y是keyword-only参数

def fn(x,y,*args, m=9, n, **kwargs):  # 由于 m和n是keyword_only参数,所以可以写成 m=9, n 的形式,顺序没有关系,因为传参的时候是按照名称对应传参的。
    print('x={}; y={}'.format(x,y))
    print('args   is {}'.format(args))
    print('m={}; n={}'.format(m,n))
    print('kwargs is {}'.format(kwargs))

调用传参

fn(1,2,3,4,5)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
Traceback (most recent call last):
  File "D:/pyporject/test/test6.py", line 8, in <module>
    fn(1,2,3,4,5)
TypeError: fn() missing 1 required keyword-only argument: 'n'
fn(1,2,3,4,5,n=6)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
x=1; y=2
args   is (3, 4, 5)
m=9; n=6
kwargs is {}
fn(1,2,3,4,5,m=9,n=119,z=555)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
x=1; y=2
args   is (3, 4, 5)
m=9; n=119
kwargs is {'z': 555}
fn(1,2,m=9,n=119,z=555)
--------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
x=1; y=2
args   is ()
m=9; n=119
kwargs is {'z': 555}

8. * 和 ** 在函数定义和函数调用时候的区别 ※※※

  • *args, 和 **kwargs 在函数定义的时候叫做可变位置参数和可变关键词参数,用来收集任意数量的位置参数和关键字参数
  • * 和 ** 在函数调用的时候叫做参数解构,只能用在函数对实参进行解构时,不能单独使用。

在片函数的源码中就有类似的用法

函数的示例

def fn(*args,**kwargs):
    print(args)
    print(kwargs)

函数传参后 *args**kwargs 分别收集位置参数和关键字参数

fn(1, 2, 3, a=4, b=5)
----------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
(1, 2, 3)
{'a': 4, 'b': 5}

函数传参的时候,实参中使用 *** 的时候,* 将对应的对可解构的对象(list,set,range,str,tuple等)解构为位置参数,实参中使用 * 只可以对字典的key进行解构,** 将对应的字典解构为关键字参数
实参解构后还是会被函数的 *args**kwargs分别将位置参数和关键数参数收集

fn(*(1, 2, 3), **{'x': 'love', 'y': 'you'})
# === 等价于 ===
# fn(1, 2, 3, x='love', y='you')
----------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
(1, 2, 3)
{'x': 'love', 'y': 'you'}
fn(*[1, 2, 3], **{'x': 'love', 'y': 'you'})
# === 等价于 ===
# fn(1, 2, 3, x='love', y='you')
----------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
(1, 2, 3)
{'x': 'love', 'y': 'you'}
fn(*'123', **{'x': 'love', 'y': 'you'})
# === 等价于 ===
# fn("1", "2", "3", x='love', y='you')
----------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
('1', '2', '3')
{'x': 'love', 'y': 'you'}

等价的效果

fn(1, 2, 3, x='love', y='you')
----------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
(1, 2, 3)
{'x': 'love', 'y': 'you'}
fn("1", "2", "3", x='love', y='you')
----------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
('1', '2', '3')
{'x': 'love', 'y': 'you'}

在函数传参的时候,我们可以使用 * 将多个可解构的对象解构为位置参数,使用 ** 将多个字典解构为关键自参数

fn(*'123', *(4, 5, 6), **{'x': 'love', 'y': 'you'})
----------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
('1', '2', '3', 4, 5, 6)
{'x': 'love', 'y': 'you'}
fn(*'123', *(4, 5, 6), **{'x': 'love', 'y': 'you'}, **{'m': 666, 'n': 999})
----------------------------------------------------------
C:\python36\python.exe D:/pyporject/test/test6.py
('1', '2', '3', 4, 5, 6)
{'x': 'love', 'y': 'you', 'm': 666, 'n': 999}

9. 封装解构中的解构和函数参数解构中的参数解构的区别

  • 封装解构中的解构使用的是 *, 不支持 **
  • 封装解构中的解构使用的是 *,对字典来说只能对key解构
  • 函数中的参数解构同时支持 ***
  • 函数中的参数解构,* 可以理解为是对可解构对象(list,set,range,str,tuple等)进行解构,解构之后就是一个一个的位置参数,对字典只能对key进行解构。
  • 函数中的参数解构,** 是对字典的解构,解构之后就是一个一个的key value 对儿。

封装解构中的解构请参考《封装与解构》部分的相关记录

推荐阅读