一、函数作用
代码出现大量重复操作时,考虑将操作重构为函数(Functions)。
例如,我们想做一个记账本,记录日期、地点和消费内容及金额。
不用函数时,我们要这样写:
print("5月10号") print("去水果店") print("买苹果,花费5.2元") print("5月11号") print("去水果店") print("买橘子,花费6元") print("5月12号") print("去超市") print("买牛奶,花费60元")
看起来很多重复的地方,我们把核心内容抽象出来,然后封装成函数。
记录日期、地点和消费内容及金额,这些内容会改变,我们把它们作为参数传递给函数((date, place, item, price)作为参数传入函数)。
def record_consume(date, place, item, price): print(date) print(f"去{place}") print(f"买{item},花费{price}元") record_consume("5月10日", "水果店", "苹果", 5.2) record_consume("5月11日", "水果店", "橘子", 6) record_consume("5月12日", "超市", "牛奶", 60)
定义函数后,我们只需要用函数名(参数)方式调用
(record_consume("5月10日", "水果店", "苹果", 5.2))。
二、函数定义
2.1 函数的定义和调用
def record_consume(date, place, item, price): """记账函数,传入参数日期,地点,物品,价格""" print(date) print(f"去{place}") print(f"买{item},花费{price}元") record_consume("5月10日", "水果店", "苹果", 5.2)
可以看出函数定义的语法:
定义 函数使用关键字 def,后跟函数名与括号内的形参列表。函数语句从下一行开始,并且有4空格缩进。
def 函数名(参数列表): 函数内容
函数内的第一条语句是字符串时,该字符串就是文档字符串,也称为 docstring,利用文档字符串可以自动生成文档,还可以让开发者在浏览代码时直接查阅文档;最好养成在代码中加入文档字符串的好习惯。
函数执行的过程:
有实际值的实参 传递给 函数中形式上占位的参数(形参
具体细节:(初次阅读时可跳过)
函数在 执行 时使用函数局部变量符号表,所有函数变量赋值都存在局部符号表中。
引用变量的查找顺序从从内到外的。首先,在局部符号表里查找变量,然后,是外层函数局部符号表,再是全局符号表,最后是内置名称符号表。因此,尽管可以引用全局变量和外层函数的变量,但最好不要在函数内直接赋值(除非是 global 语句定义的全局变量,或 nonlocal 语句定义的外层函数变量)。
在调用函数时会将实际参数(实参)引入到被调用函数的局部符号表中;因此,实参是使用 按值调用 来传递的(该值 是对象的 引用 而不是对象的值)。1当一个函数调用另外一个函数时,会为该调用创建一个新的局部符号表。
上面我们定义了一个没有返回值的函数,
下面来看一个有返回值的函数,该函数使用return 返回一个列表。
def distance_to_avg(scores): """计算到平均值的差值""" avg = sum(scores) / len(scores) return [x - avg for x in scores] scores = [90, 80, 70, 80, 90] distances = distance_to_avg(scores) # 将返回的列表赋值给distances变量 print(distances)
2.2 函数参数
2.2.1 默认值参数
如果参数中某些值通常固定,可以为它设置默认值。
语法:
def 函数名(必选参数,默认值参数=默认值):
例如:我们有一个计算列表均值或总和的函数,我们通常使用均值功能,因此将mood设置默认值为‘avg’。
def calculate_list_sta(list_a, mood='avg'): if mood == 'avg': return sum(list_a) / len(list_a) elif mood == 'sum': return sum(list_a) else: return -1
调用时默认模式就是'avg',只有模式不是'avg'的时候需要传mood参数。
scores = [90, 80, 70, 80, 90] avg = calculate_list_sta(scores) s = calculate_list_sta(scores,"sum") print(avg,s)
关于默认值的细节:
默认值在定义作用域里的函数定义中求值:
i = 5 def f(arg=i): print(arg) i = 6 f() # 5
默认值只计算一次,使用列表、字典等可变对象时,会产生累积效果。
def f(a, L=[]): L.append(a) return L print(f(1)) #[1] print(f(2)) #[1, 2] print(f(3)) #[1, 2, 3]
调用f(2)的时候,L不再赋值为[],而是使用已有的L=[1]。
如果不想在后续调用之间共享默认值,应该使用下面方法:
def f(a, L=None): if L is None: L = [] L.append(a) return L
默认值为None,在函数内赋值[]。
2.2.2 关键字参数
可用key=value 形式关键字参数调用函数。
例如:
print(calculate_list_sta(scores, 'sum')) print(calculate_list_sta(list_a=scores, mood='sum')) print(calculate_list_sta(mood='sum', list_a=scores)) print(calculate_list_sta(mood='sum',scores)) # 错误,位置参数必须在关键字参数前
2.2.3 *name 和 **name形式参数
*name 接收一个元组,该元组包含形参列表之外的参数。
**name接收一个字典。
def cheeseshop(kind, *arguments, **keywords): print("-- Do you have any", kind, "?") print("-- I'm sorry, we're all out of", kind) for arg in arguments: print(arg) print("-" * 40) for kw in keywords: print(kw, ":", keywords[kw])
cheeseshop("Limburger", "It's very runny, sir.", "It's really very, VERY runny, sir.", shopkeeper="Michael Palin", client="John Cleese", sketch="Cheese Shop Sketch")
"Limburger"传给kind,剩下的被传给*arguments,下面三行的key=value形式被当成字典,传给**keywords。
-- Do you have any Limburger ? -- I'm sorry, we're all out of Limburger It's very runny, sir. It's really very, VERY runny, sir. ---------------------------------------- shopkeeper : Michael Palin client : John Cleese sketch : Cheese Shop Sketch
2.2.4 特殊参数
通常,参数传递有上面的两种方式,位置参数,关键字参数。
通过/和*标记,可以限制传参方式。
/前面仅限位置参数,
*后面仅限关键字参数
def standard_arg(arg): print(arg) standard_arg(2) standard_arg(arg=2) def pos_only_arg(arg, /): print(arg) pos_only_arg(1) pos_only_arg(arg=1) #报错,只能通过位置传参,不能用关键字传参 def kwd_only_arg(*, arg): print(arg) kwd_only_arg(3) #报错,只能关键字传参 kwd_only_arg(arg=3) # /前面仅位置传参,*后面仅关键字,standard不受约束 def combined_example(pos_only, /, standard, *, kwd_only): print(pos_only, standard, kwd_only) combined_example(1, 2, kwd_only=3) combined_example(1, standard=2, kwd_only=3)
限制传参方式可以避免潜在的传参冲突问题:
def foo(name, **kwds): return 'name' in kwds foo(1, **{'name': 2}) # 冲突,1会先传递给name,{'name':2}会以关键字方式再传递给name
使用位置限定就可以避免,现在name只能从通过位置参数传递。
def foo(name, /, **kwds): return 'name' in kwds print(foo(1, **{'name': 2}))
2.2.5 任意实参列表
*arg可以传递可变数量的实参。
def write_multiple_items(file, separator, *args): file.write(separator.join(args))
通常,可变参数*arg放在最后。因为它会把剩余的参数全部读入。*arg后面的只能是关键字参数。
def concat(*args, sep="/"): return sep.join(args) concat("earth", "mars", "venus") concat("earth", "mars", "venus", sep=".")
2.3 解包实参列表
使用*解包(unpack),将参数从列表中取出:
list(range(3, 6)) # 正常调用,使用两个参数 args = [3, 6] list(range(*args)) # 使用解包(unpack),两个参数从列表中取出
同样,字典可以用 **
操作符传递关键字参数:
def parrot(voltage, state='a stiff', action='voom'): print("-- This parrot wouldn't", action, end=' ') print("if you put", voltage, "volts through it.", end=' ') print("E's", state, "!") d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"} parrot(**d)
2.4 Lambda 表达式
lambda 关键字用于创建小巧的匿名函数。例如:lambda a, b: a+b
函数返回两个参数的和。在语法上,它只能是单个表达式。在语义上,它只是常规函数的语法糖。
最常见的用法是用在排序中,将其作为排序的关键字函数。
pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')] pairs.sort(key=lambda pair: pair[1]) pairs
三、提升函数可读性
3.1 文档字符串
文档字符串用来说明对象用途。
第一行为摘要。大写字母开头,句号结尾。
第二行空白。
后面可以包含若干段落,描述对象的使用等。
def my_function(): """Do nothing, but document it. No, really, it doesn't do anything. """ pass print(my_function.__doc__)
#输出: Do nothing, but document it. No, really, it doesn't do anything.
3.2 函数注解
注解(annotations)用来说明数据类型。以字典形式存放在函数的__annotations__属性中,不会影响函数的其他部分。
语法:形参后跟:标注参数类型,参数列表后用 -> 返回值类型。
def 函数名(形参: 参数类型, 关键字参数: 参数类型 = '默认值') -> 返回值类型:
def f(ham: str, eggs: str = 'eggs') -> str: print("Annotations:", f.__annotations__) print("Arguments:", ham, eggs) return ham + ' and ' + eggs
3.3 编码风格
当代码变长、变复杂时,统一、可读性高的编码风格非常重要。
PEP 8 (Python Enhancement Proposal,python增强建议)是Python风格指南。内容比较多,可以去官网查看:https://peps.python.org/pep-0008/ 后面我也会整理一部分常用的。
另外补充一个技巧,如果你用的是PyCharm,可以通过Reformat(快捷键:ctrl+alt+enter)进行格式化代码,优化代码排版。
本文思维导图: