Python 函数核心知识详解:定义、参数与作用域管理
本文详细讲解了 Python 函数的核心知识,涵盖函数定义语法、参数传递机制(位置参数、关键字参数及可变参数)、命名空间与作用域(LEGB 规则)、返回值处理、函数作为对象的高级用法、Lambda 表达式、柯里化、生成器与迭代器协议以及异常处理最佳实践。文章通过丰富的代码示例展示了如何编写高效、可复用的函数,并强调了文档字符串和规范编码的重要性,旨在帮助开发者系统掌握 Python 函数编程技巧。

本文详细讲解了 Python 函数的核心知识,涵盖函数定义语法、参数传递机制(位置参数、关键字参数及可变参数)、命名空间与作用域(LEGB 规则)、返回值处理、函数作为对象的高级用法、Lambda 表达式、柯里化、生成器与迭代器协议以及异常处理最佳实践。文章通过丰富的代码示例展示了如何编写高效、可复用的函数,并强调了文档字符串和规范编码的重要性,旨在帮助开发者系统掌握 Python 函数编程技巧。

函数是 Python 中最重要、最基础的代码组织和复用方式。根据经验,如果你需要多次重复相同或类似的代码,就非常值得写一个可复用的函数。通过给一组 Python 语句一个函数名,形成的函数可以帮助你的代码更加可读、易维护。
函数声明时使用 def 关键字,返回时使用 return 关键字。良好的函数通常包含文档字符串(docstring),用于说明函数的功能、参数和返回值。
def my_function(x, y, z=1.5):
"""
计算基于参数的数学表达式
:param x: 第一个数值
:param y: 第二个数值
:param z: 第三个数值,默认为 1.5
:return: 计算结果
"""
if z > 1:
return z * (x + y)
else:
return z / (x + y)
有多条返回语句是没有问题的。如果 Python 达到函数的尾部时仍然没有遇到 return 语句,就会自动返回 None。在编写函数时,建议始终明确返回值类型,避免隐式返回 None 导致调用方出错。
每个函数都可以有位置参数和关键字参数。关键字参数最常用于指定默认值或可选参数。在前面的函数中,x 和 y 是位置参数,z 是关键字参数。这意味着函数可以通过以下任意一种方式进行调用:
my_function(5, 6, z=0.7)
my_function(3.14, 7, 3.5)
my_function(10, 20)
函数参数的主要限制是关键字参数必须跟在位置参数后(如果有的话)。你可以按照任意顺序指定关键字参数;这可以让你不必强行记住函数参数的顺序,而只需用参数名指定。
也可以使用关键字参数向位置参数传参。在前面的例子中,我们也可以这样写:
my_function(x=5, y=6, z=7)
my_function(y=6, x=5, z=7)
在部分场景中,这样做有助于代码可读性,特别是在参数较多且含义明确时。
此外,Python 还支持可变数量参数:
*args:接收任意数量的位置参数,打包为元组。**kwargs:接收任意数量的关键字参数,打包为字典。def print_args(*args, **kwargs):
print("位置参数:", args)
print("关键字参数:", kwargs)
print_args(1, 2, name="Alice", age=25)
函数有两种连接变量的方式:全局、本地。在 Python 中另一种更贴切地描述变量作用域的名称是命名空间。在函数内部,任意变量都是默认分配到本地命名空间的。本地命名空间是在函数被调用时生成的,并立即由函数的参数填充。当函数执行结束后,本地命名空间就会被销毁(除了一些特殊情况)。
考虑以下函数:
def func():
a = []
for i in range(5):
a.append(i)
当 func() 调用时,空的列表会被创建,五个元素被添加到列表,之后 a 会在函数退出时被销毁。假设我们像下面这样声明 a:
a = []
def func():
for i in range(5):
a.append(i)
在函数外部给变量赋值是可以的,但是那变量必须使用 global 关键字声明为全局变量:
a = None
def bind_a_variable():
global a
a = []
bind_a_variable()
print(a) # 输出 []
我简单的讲下 global 关键字的用法。通常全局变量用来存储系统中的某些状态。如果你发现你大量使用了全局变量,可能表明你需要面向对象编程(使用类)来封装状态。
Python 的作用域遵循 LEGB 规则:Local(局部)、Enclosing(嵌套/闭包)、Global(全局)、Built-in(内置)。理解这一规则对于调试变量未定义错误至关重要。
当我在使用 Java 和 C++ 编程后第一次使用 Python 编程时,我最喜欢的特性就是使用简单语法就可以从函数中返回多个值。以下是代码:
def f():
a = 5
b = 6
c = 7
return a, b, c
a, b, c = f()
在数据分析和其他科研应用,你可能会发现经常需要返回多个值。这里实质上是返回了一个对象,也就是元组,而元组之后又被拆包为多个结果变量。在前面的例子中,我们可以用下面的代码代替:
return_value = f()
在这个例子中,return_value 是一个 3 个元素的元组。像之前那样一次返回多个值还有一种潜在的、更有吸引力的实现:
def f():
a = 5
b = 6
c = 7
return {'a': a, 'b': b, 'c': c}
具体用哪种技术取决于你需要做什么的事。元组适合固定结构的数据,字典适合带标签的数据。
由于 Python 的函数是对象,很多在其他语言中比较难的构造在 Python 中非常容易实现。假设我们正在做数据清洗,需要将一些变形应用到下列字符串列表中:
states = [' Alabama ', 'Georgia!', 'Georgia', 'georgia', 'FlOrIda',
'south carolina##', 'West virginia?']
任何处理过用户提交数据的人都对这样的数据感到凌乱。为了使这些数据整齐、可用于分析,有很多事情需要去做:去除空格、移除标点符号、调整适当的大小写。一种方式是使用内建的字符串方法,结合标准库中的正则表达式模块 re:
import re
def clean_strings(strings):
result = []
for value in strings:
value = value.strip()
value = re.sub('[!#?]', '', value)
value = value.title()
result.append(value)
return result
结果如下:
clean_strings(states)
# ['Alabama', 'Georgia', 'Georgia', 'Georgia', 'Florida', 'South Carolina', 'West Virginia']
另一种会让你觉得有用的实现就是将特定的列表操作应用到某个字符串的集合上:
def remove_punctuation(value):
return re.sub('[!#?]', '', value)
clean_ops = [str.strip, remove_punctuation, str.title]
def clean_strings(strings, ops):
result = []
for value in strings:
for function in ops:
value = function(value)
result.append(value)
return result
像这种更为函数化的模式可以使你在更高层次上方便地修改字符串变换方法。clean_strings 函数现在也具有更强的复用性、通用性。
你可以将函数作为一个参数传给其他的函数,比如内建的 map 函数,可以将一个函数应用到一个序列上:
for x in map(remove_punctuation, states):
print(x)
Python 支持所谓的匿名或 lambda 函数。匿名函数是一种通过单个语句生成函数的方式,其结果是返回值。匿名函数使用 lambda 关键字定义,该关键字仅表达'我们声明一个匿名函数'的意思:
def short_function(x):
return x * 2
equiv_anon = lambda x: x * 2
匿名函数在数据分析中非常方便,因为在很多案例中数据变形函数都可以作为函数的参数。匿名函数代码量小(也更为清晰),将它作为参数进行传值,比写一个完整的函数或者将匿名函数赋值给局部变量更好。举个例子,考虑下面的示例:
def apply_to_list(some_list, f):
return [f(x) for x in some_list]
ints = [4, 0, 1, 5, 6]
apply_to_list(ints, lambda x: x * 2)
你也可以写成 [x * 2 for x in ints],但是在这里我们能够简单地将一个自定义操作符传递给 apply_to_list 函数。
另一个例子,假设你想要根据字符串中不同字母的数量对一个字符串集合进行排序:
strings = ['foo', 'card', 'bar', 'aaaa', 'abab']
strings.sort(key=lambda x: len(set(list(x))))
和 def 关键字声明的函数不同,匿名函数对象自身并没有一个显式的 __name__ 属性,这是 lambda 函数被称为匿名函数的一个原因。注意,lambda 函数只能包含一个表达式,不能包含复杂的逻辑块。
柯里化是计算机科学术语(以数学家 Haskell Curry 命名),它表示通过部分参数应用的方式从已有的函数中衍生出新的函数。例如,假设我们有一个不重要的函数,其功能是将两个数加一起:
def add_numbers(x, y):
return x + y
使用这个函数,我们可以衍生出一个只有一个变量的新函数,add_five,可以给参数加上 5:
add_five = lambda y: add_numbers(5, y)
第二个参数对于函数 add_numbers 就是柯里化了。这里并没有什么神奇的地方,我们真正做的事只是定义了一个新函数,这个新函数调用了已经存在的函数。内建的 functools 模块可以使用 partial 函数简化这种处理:
from functools import partial
add_five = partial(add_numbers, 5)
print(add_five(10)) # 输出 15
这在配置特定行为或减少重复代码时非常有用。
通过一致的方式遍历序列,例如列表中的对象或者文件中的一行行内容,这是 Python 的一个重要特性。这个特性是通过迭代器协议来实现的,迭代器协议是一种令对象可遍历的通用方式。例如,遍历一个字典,获得字典的键:
some_dict = {'a': 1, 'b': 2, 'c': 3}
for key in some_dict:
print(key)
当你写下 for key in some_dict 的语句时,Python 解释器首先尝试根据 some_dict 生成一个迭代器:
dict_iterator = iter(some_dict)
迭代器就是一种用于在上下文中(比如 for 循环)向 Python 解释器生成对象的对象。大部分以列表或列表型对象为参数的方法都可以接收任意的迭代器对象。包括内建方法比如 min、max 和 sum,以及类型构造函数比如 list 和 tuple。
用迭代器构造新的可遍历对象是一种非常简洁的方式。普通函数执行并一次返回单个结果,而生成器则'惰性'地返回一个多结果序列,在每一个元素产生之后暂停,直到下一个请求。如需创建一个生成器,只需要在函数中将返回关键字 return 替换为 yield 关键字:
def squares(n=10):
print('Generating squares from 1 to {0}'.format(n ** 2))
for i in range(1, n + 1):
yield i ** 2
当你实际调用生成器时,代码并不会立即执行:
gen = squares()
# <generator object squares at 0x...>
直到你请求生成器中的元素时,它才会执行它的代码:
for x in gen:
print(x, end=' ')
# Generating squares from 1 to 100
# 1 4 9 16 25 36 49 64 81 100
生成器表达式来创建生成器更为简单。生成器表达式与列表、字典、集合的推导式很类似,创建一个生成器表达式,只需要将列表推导式的中括号替换为小括号即可:
gen = (x ** 2 for x in range(100))
上面的代码与下面更为复杂的生成器是等价的:
def _make_gen():
for x in range(100):
yield x ** 2
gen = _make_gen()
在很多情况下,生成器表达式可以作为函数参数用于替代列表推导式,节省内存:
sum(x ** 2 for x in range(100))
标准库中的 itertools 模块是适用于大多数数据算法的生成器集合。例如,groupby 可以根据任意的序列和一个函数,通过函数的返回值对序列中连续的元素进行分组:
import itertools
first_letter = lambda x: x[0]
names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']
for letter, names_iter in itertools.groupby(names, first_letter):
print(letter, list(names_iter))
下表是一些我认为经常用到的 itertools 函数的列表。你可以通过查询 Python 官方文档来获得更多关于内建工具库的信息。
优雅地处理 Python 的错误或异常是构建稳定程序的重要组成部分。在数据分析应用中,很多函数只能处理特定的输入。例如,Python 的 float 函数可以将字符串转换为浮点数字,但是对不正确的输入会产生 ValueError:
float('something') # 抛出 ValueError
假设我们想要在 float 函数运行失败时可以优雅地返回输入参数。我们可以通过将 float 函数写入一个 try/except 代码段来实现:
def attempt_float(x):
try:
return float(x)
except ValueError:
return x
如果 float(x) 执行时抛出了异常,则代码段中的 except 部分代码将会被执行。
你可能会注意到,除了 ValueError,float 函数还会抛出其他的异常:
float((1, 2)) # 抛出 TypeError
你可能只想处理 ValueError,因为 TypeError(输入的不是字符串或数值)可能表明你的程序中有个合乎语法的错误。为了实现这个目的,在 except 后面写下异常类型:
def attempt_float(x):
try:
return float(x)
except ValueError:
return x
你可以通过将多个异常类型写成元组的方式同时捕获多个异常(小括号是必不可少的):
def attempt_float(x):
try:
return float(x)
except (TypeError, ValueError):
return x
某些情况下,你可能想要处理一个异常,但是你希望一部分代码无论 try 代码块是否报错都要执行。为了实现这个目的,使用 finally 关键字:
f = open(path, 'w')
try:
write_to_file(f)
finally:
f.close()
这样,我们可以让 f 在程序结束后总是关闭。类似的,你可以使用 else 来执行当 try 代码块成功执行时才会执行的代码:
f = open(path, 'w')
try:
write_to_file(f)
except:
print('Failed')
else:
print('Succeeded')
finally:
f.close()
掌握 Python 函数的基础知识是成为熟练开发者的关键一步。从基本的定义到高级的生成器和异常处理,这些概念构成了 Python 编程的核心。在实际项目中,合理设计函数接口、规范参数传递、妥善处理异常,能够显著提升代码的质量和可维护性。建议开发者在阅读官方文档的同时,多动手实践,深入理解每个特性的底层机制。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online