0%

Python之函数

这里介绍Python的函数如何定义、调用

abstract.png

函数定义/调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
print("------------------- 函数定义-----------------------")

def greet():
"""
欢迎
"""
print("Hello, World")

def greet_user(username):
"""
欢迎用户
"""
print("Hello,", username)

def two_sum(a, b):
"""
计算两数之和
"""
sum = a+b
return sum

print("---------------------函数调用-------------------------")
greet()
greet_user( "Aaron" )
print(f"two_sum(13, 20) :", { two_sum(13, 20)})

# Python中函数也是对象
print("greeth函数的类型 : ", type(greet))

# 查看函数的文档字符串
print("doc:", greet.__doc__)

# 函数可以赋值给变量,可通过callable函数判断一个变量是否为可调用对象,然后通过()进行调用
f = greet
print("f变量是否为可调用对象: ", callable(f))
f()

figure 1.png

函数实参

位置实参

所谓位置实参,即是按照函数定义中的顺序传递的参数

1
2
3
4
5
6
print("----------------位置实参--------------------")
def get_animal_info(name, age, msg):
print(f"I'm {name}, My age is {age}. MSG --->>> {msg}")

# 位置实参:根据其在函数定义中的顺序传递的参数
get_animal_info("Aaron", 18, "Hello")

figure 2.png

关键字实参

所谓关键字实参,即是通过指定参数名称来传递的参数。无需按照函数定义中的位置顺序依次传入

1
2
3
4
5
6
7
print("----------------关键字实参--------------------")
def get_animal_info(name, age, msg):
print(f"I'm {name}, My age is {age}. MSG --->>> {msg}")

# 关键字实参:指定参数名称来传递的参数,无需按照参数在函数定义中的位置顺序依次传入
get_animal_info(msg="你好", name="Bob", age=22)
get_animal_info(age=7, name="Amy",msg="再见")

figure 3.png

混用位置实参、关键字实参

当位置实参、关键字实参混用时,必须保证 位置实参在前、关键字实参在后 的顺序

1
2
3
4
5
print("----------------混用位置实参、关键字实参--------------------")
def get_animal_info(name, age, msg):
print(f"I'm {name}, My age is {age}. MSG --->>> {msg}")

get_animal_info("Tony", msg="Bye", age=37)

figure 4.png

函数形参

普通形参

对于普通形参,调用时通过位置实参、关键字实参传值均可

1
2
3
4
5
6
7
8
print("------------------- 普通形参 -----------------------------") 
def get_cat_info(name, type):
print("name:",name,"type:",type)

# 对于普通形参,调用时通过位置实参、关键字实参传值均可
get_cat_info("Tom","英短")
get_cat_info("David",type="中华田园猫")
get_cat_info(type="美短",name="Tony")

figure 5.png

*形式的可变形参

对于*形式的可变形参,其会收集未匹配的位置实参 并以元组tuple形式进行存储。形参名称习惯上使用args

1
2
3
4
5
6
7
8
print("------------------- *形式的可变形参 -----------------------------") 

def get_names(first_name, last_name, *args):
print("first name:",first_name,"last name:",last_name)
print("args的类型:",type(args),"args:", args)

get_names("Aaron", "Wang")
get_names("Aaron", "Zhu", "Tom", "Tony")

figure 6.png

**形式的可变形参

对于**形式的可变形参,其会收集未匹配的关键字实参 并以字典dict形式进行存储。形参名称习惯上使用kw

1
2
3
4
5
6
7
8
9
print("------------------- **形式的可变形参 -----------------------------") 

def get_info(name, **kw):
print("name:",name)
print("kw的类型:",type(kw),"kw:",kw)

get_info("Aaron")
get_info("Aaron", city="NanJing", age=18)
get_info(name="Bob", city="HangZhou", age=23)

figure 7.png

限定位置形参

Python 3.8版本引入限定位置形参。具体地,在形参列表中使用 /斜杠 进行分隔。其中 /斜杠 前面的形参都是限定位置形参,调用时只能使用位置实参传参

1
2
3
4
5
6
7
print("------------------- 限定位置形参 -----------------------------") 

def sum(a,b, /, name):
print("a:",a,"b:",b,"name:",name)

sum(1, 2, "Aaron")
sum(1, 2, name="Aaron")

figure 8.png

限定关键字形参

限定关键字形参,又称为命名关键字形参。具体地,在形参列表中使用 *星号 进行分隔,其中 *星号 后面的形参都是限定关键字形参,调用时只能使用关键字实参传参

1
2
3
4
5
6
7
print("------------------- 限定关键字形参 -----------------------------") 

def get_stu_info(a,b, *, name, sex):
print("a:",a,"b:",b,"name:",name,"sex:",sex)

get_stu_info(11,22,name="Aaron",sex="Man")
get_stu_info(a=3,b=4,name="Amy",sex="Woman")

figure 9.png

组合拳

定义函数时,可以组合使用上述多种形参。形参组合顺序为:限定位置形参、普通形参、*形式的可变形参、限定关键字形参、**形式的可变形参。特别地,如果形参列表中存在 *形式的可变形参 时,则 限定关键字形参 前面无需添加 *星号分隔符;否则需要添加。故,下述函数定义示例中的 my_city、my_sex 均为 限定关键字形参

1
2
3
4
5
6
7
8
def mix_func1(a,b,c, /, first_name, last_name, *, my_city, my_sex, **kw):
pass

def mix_func2(a,b,c, /, *, my_city, my_sex, **kw):
pass

def mix_func3(a,b,c, /, first_name, last_name, *args, my_city, my_sex, **kw):
pass

调用函数时,位置实参、关键字实参 与 各种形参的传递规则为:

  • 对于位置实参而言,其会按顺序依次传递给 限定位置形参、普通形参,剩余的位置实参全部传递给可变形参*
  • 对于关键字实参而言,其会根据参数名匹配传递给 限定关键字形参、剩余的普通形参,剩余的关键字实参全部传递给可变形参**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
print("------------------- 组合拳 -----------------------------") 

# 形参组合顺序:限定位置形参、普通形参、可变形参*、限定关键字形参、可变形参**
def mix_param(a,b,c, /, first_name, last_name, *args, my_city, my_sex, **kw):
print("限定位置形参: ", "a -->",a, "b -->",b,"c --->",c)
print("普通形参: ", "first_name --->",first_name, "last_name --->", last_name)
print("可变形参*: ", "args --->", args )
print("限定关键字形参: ", "my_city --->",my_city, "my_sex --->", my_sex)
print("可变形参**: ", "kw --->", kw)

print("-----------------------------------------------")
mix_param(1,2,3, "Aaron", "Zhu", "Cat","Dog", my_city="BeiJing", my_sex="male", my_age=13, my_phone="119")

print("\n-----------------------------------------------")
mix_param(111,222,333, last_name="Wang", first_name="Amy", my_age=35, my_phone="120", my_city="ShangHai", my_sex="female")

figure 10.png

具有默认值的形参

规则

Python中形参支持设置默认值。可以从 限定位置形参 或 普通形参中的任意一个形参 开始设置默认值,其后面所有的 限定位置形参、普通形参 都必须设置默认值

1
2
3
4
5
6
# 可以从 限定位置形参 或 普通形参中的任意一个形参 开始设置默认值,其后面所有的 限定位置形参、普通形参 都必须设置默认值
def default_fun1(a,b=2,c=3, /, first_name="Luca", last_name="Li", *, my_city, my_sex, **kw):
pass

def default_fun2(a,b,c, /, first_name, last_name="Li", *, my_city, my_sex, **kw):
pass

限定关键字形参则可以任意设置默认值,不需要保证其后面所有的限定关键字形参都有默认值

1
2
3
# 限定关键字形参则可以任意设置默认值,不需要保证其后面所有的限定关键字形参都有默认值
def default_fun3(a,b,c, /, first_name, last_name, *, my_city="GuangZhou", my_sex, **kw):
pass

陷阱:使用可变对象作为形参默认值

当使用可变对象作为形参默认值,可能会发生意外情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
print("------------------- 将可变对象作为函数参数的默认值 -----------------------------") 

def foo(name, names=[]):
names.append(name)
return names

print("显式传入一个list,不使用函数的默认值")
names1 = foo("Aaron", [])
names2 = foo("Bob", [])
print(f"names 1 : {names1}")
print(f"names 2 : {names2}")
print(f"names1 is names2 : {names1 is names2}")

print("\n使用函数的默认值")
names3 = foo("Tony")
names4 = foo("Amy")
print(f"names 3 : {names3}")
print(f"names 4 : {names4}")
print(f"names3 is names4 : {names3 is names4}")

当显式传入一个list,不使用函数的默认值时。测试结果符合预期;但使用函数的默认值时,我们会发现names3、names4竟然指向的是同一个列表对象

figure 11.png

这是因为Python中,默认值是在函数定义时计算的,其只会被计算一次(通常发生在模块加载时)。具体地,默认值会存储在函数对象的defaults属性当中,故其会被重复使用的。所以,如果函数参数的默认值是一个可变对象时,那么每次使用该默认值时,其实是同一个对象。同理,当类的方法形参的默认值使用可变对象,也会存在上述问题

1
2
3
4
5
6
7
8
9
10
def foo(name, names=[]):
names.append(name)
return names

names3 = foo("Tony")
names4 = foo("Amy")

print(f"foo.__defaults__ : { foo.__defaults__ }")
print(f"foo.__defaults__[0] is name3 : { foo.__defaults__[0] is names3 }")
print(f"foo.__defaults__[0] is name4 : { foo.__defaults__[0] is names4 }")

figure 12.png

针对上述问题,可以解决的办法是使用不可变对象作为函数形参的默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print("------------------- 将不可变对象作为函数参数的默认值 -----------------------------") 

def bar(name, names=None):
if( names == None ):
names = []
names.append(name)
return names

names5 = bar("Red")
names6 = bar("Blue")

print(f"names5 is names6 : {names5 is names6}")
print(f"names 5 : {names5}")
print(f"names 6 : {names6}")

figure 13.png

函数注解

函数注解会作为元数据存储在函数的annotations属性里,仅此而已。目前Python解释器不会利用它进行任何检查、验证、限制。后续可供IDE、框架、装饰器等工具使用。具体地:

  • 函数声明中可在形参后添加 :符号 和 注解表达式
  • 对于有默认值的形参, 可在 形参与=号 之间添加 :符号 和 注解表达式
  • 对于返回值, 可在 函数声明末尾与:号 之间添加 ->符号 和 注解表达式
  • 其中,注解表达式可以是任何类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import inspect

print("-------------------- 函数注解 -------------------")

def sub_str(word:str, count:int=5) -> str:
"""
截取字符串str前count个字符
"""
if( count<996 ):
return word[:count]
else:
return 1314

print(f"res1 : {sub_str('Hello World', 5)}")
print(f"res2 : {sub_str(5432, 6789)}")

# 通过 __annotations__ 属性查看函数注解
print(f"sub_str annotation : {sub_str.__annotations__}")

# 通过 函数签名 查看函数注解
sig = inspect.signature(sub_str)
for param in sig.parameters.values():
print(f"函数参数{param.name} 的注解: {param.annotation}")

print(f"函数返回值的注解: {sig.return_annotation}")

figure 14.png

参考文献

  1. Python编程·第3版:从入门到实践 Eric Matthes著
  2. Python基础教程·第3版 Magnus Lie Hetland著
  3. 流畅的Python·第1版 Luciano Ramalho著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝