简介:本文系统讲解在Python中创建自定义函数的完整方法:先拆解def语法四要素与命名规范,再以字符串拼接函数为例,指出初学者常见的join调用方向写反、内置名object被遮蔽等陷阱,并给出带类型注解、文档字符串与输入校验的规范实现,进一步覆盖默认参数、可变参数、返回值设计、错误处理与自测方法,帮助你写出健壮、通用、可复用的Python函数。
在 Python 开发中,自定义函数是最基础、也最常用的代码组织形式。它把一段具有独立职责的逻辑封装成可调用的单元,让同一功能在程序的不同位置只需一次调用即可复用,也让代码结构更清晰、更易于测试与维护。本文将系统讲解如何在 Python 中创建一个高质量的自定义函数,并以“字符串拼接”这一典型场景为例,剖析常见陷阱、规范写法与进阶方向。
设想一个“拼接两个字符串”的需求散落在脚本的多处。如果每次都靠复制粘贴,一旦需求变化(比如要加分隔符),就必须修改多段分散的代码,漏改一处就埋下隐患。自定义函数的价值正在于此:
在 Python 中创建函数的基本语法如下:
def function_name(parameter_list):"""可选的文档字符串"""# 函数体return return_value # 可选
它由四个关键部分构成:
concatenate_strings;需要特别强调,Python 依靠缩进划定函数体的范围,通常以 4 个空格为一个层级,务必避免制表符与空格混用。
初学者很容易写出类似下面的代码:
def concatenate_strings(str1, str2):# 用于拼接两个字符串object = [str1, str2].join('')return object
意图是正确的,但这段代码实际无法运行,且隐藏了几个值得认真剖析的问题。
陷阱一:join 的调用方向写反。 在 Python 中,join 是字符串的方法,而不是列表的方法。标准写法是 ''.join([str1, str2]),含义是“用空字符串把序列中的元素连接起来”。列表对象上根本没有 join 成员,上述代码在运行时会直接抛出 AttributeError: 'list' object has no attribute 'join'。这是把其他语言的语法习惯带入 Python 的典型错误。
陷阱二:用 object 作变量名。 object 是 Python 内置的基础类型名,所有类最终都派生自它。虽然作为局部变量名在语法上合法,但这种“遮蔽内置名”的习惯可能在复杂代码里造成难以排查的问题,应当主动规避。
陷阱三:意图说明不规范。 仅用一行井号注释描述用途,既无法被文档工具(如内置的 help)识别,也没有说明参数含义与返回值契约。规范做法是使用文档字符串(docstring),并配合类型注解。
修正之后的版本如下:
def concatenate_strings(str1: str, str2: str, separator: str = "") -> str:"""拼接两个字符串。Args:str1: 第一个字符串。str2: 第二个字符串。separator: 插入在两个字符串之间的分隔符,默认直接拼接。Returns:拼接后的完整字符串。Raises:TypeError: 当任一参数不是字符串时抛出。"""if not isinstance(str1, str) or not isinstance(str2, str):raise TypeError("str1 与 str2 必须均为字符串类型")return separator.join((str1, str2))
调用时只需传入参数,即可得到拼接结果:
name = concatenate_strings("张", "三", separator="·")print(name) # 输出:张·三plain = concatenate_strings("Hello", "World")print(plain) # 输出:HelloWorld
相比最初的版本,这个实现有三点关键改进:一是把 join 的调用方向改正,保证代码可以真正运行;二是引入带默认值的 separator 参数,让函数的适用面更宽,同时默认行为仍保持“直接拼接”,兼容最初的需求;三是补充类型注解与完整文档字符串,让编辑器与文档工具都能自动提示用法契约。
如果暂时不需要分隔符,两个字符串的拼接也可以直接使用运算符或格式字符串:
def concatenate_strings(str1: str, str2: str) -> str:return f"{str1}{str2}"
函数定义多个参数后,调用时既可按位置依次传入,也可按“参数名=值”的关键字方式传入。关键字调用可读性更强,参数较多时建议显式写出:
concatenate_strings(str1="Hello", str2="World", separator="-")
默认参数让调用方可以省略部分实参,但必须牢记一条铁律:不要用列表、字典等可变对象作为默认值。默认值在函数定义时只求值一次,后续多次调用会共享同一个对象,造成状态累积:
def add_tag(tag, tags=[]): # 错误:多次调用共享同一个列表tags.append(tag)return tagsdef add_tag(tag, tags=None): # 正确:函数内部每次新建列表if tags is None:tags = []tags.append(tag)return tags
当输入的字符串个数不确定时,*args 能把任意多个位置实参收进一个元组,配合仅限关键字的参数,可以写出更通用的拼接函数:
def concat_all(*parts: str, separator: str = "") -> str:"""拼接任意数量的字符串。"""if not all(isinstance(p, str) for p in parts):raise TypeError("所有 parts 必须是字符串")return separator.join(parts)print(concat_all("2025", "01", "15", separator="-")) # 输出:2025-01-15
函数的计算结果应通过 return 交还给调用方,而不是在函数内部直接打印。这种“只计算、不输出”的设计带来的是可组合性——调用方可以自由决定把结果存储、继续加工或传入下一个函数:
result = concatenate_strings("order_", str(1024))filename = concatenate_strings(result, ".log")
若一个函数需要产出多个结果,可以利用元组一次返回,调用方解包接收:
def split_full_name(full: str):first, last = full.split(" ", 1)return first, lastfirst, last = split_full_name("Ada Lovelace")
此外,建议采用“卫语句”风格:把非法输入的判断放在函数体最前面,一旦命中立即抛出或返回,让主逻辑保持扁平、易读。
函数面对的异常输入通常有两类:类型错误与空值。对类型错误,应在函数入口尽早抛出信息明确的异常(如上文的 TypeError),这比让错误在深层逻辑中延迟爆发更利于定位;调用方则可按需捕获处理:
try:concatenate_strings("age", 18)except TypeError as exc:print(f"参数不合法:{exc}")
对空值场景,应在设计阶段就明确预期行为(例如空字符串如何拼接),并写进文档字符串,避免不同调用方对边界语义产生分歧。
写好的函数值得配上几行验证。日常开发中,用 assert 即可完成快速自检:
assert concatenate_strings("a", "b") == "ab"assert concatenate_strings("Hello", "World", " ") == "Hello World"assert concatenate_strings("", "") == ""
把这些断言与函数放在同一模块或同目录的测试文件中,每次改动后运行一遍,就能确认行为未发生意外变化。这正是单元测试中“回归验证”思想的最小落地形式。
掌握基础之后,函数相关的知识还有三条值得延伸的路径:
lambda a, b: a + b 适合一次性、单行的简单逻辑(例如作为排序的关键字函数);正式逻辑仍应以 def 定义,便于命名与复用;另外,如果后续打算把稳定的工具函数部署到无服务器计算(函数计算)类的平台,以独立可调用的服务单元对外提供能力,那么“无隐藏状态、纯计算、契约清晰”的函数形态,恰好与这类环境的最佳实践天然契合。在函数层面养成的良好习惯,会在代码走向服务化、云端化时持续产生复利。
在 Python 中创建一个自定义函数,入门门槛很低——一个 def、一个恰当的名字、一组参数和一个返回值即可完成;但写一个“好”的函数,需要综合考量命名、参数契约、文档、错误处理与可测性。以本文的字符串拼接函数为例:一个看似简单的需求,先后经历了 join 用法纠错、内置名遮蔽消除、默认参数引入、类型注解与文档补充、入口校验加固,最终演化为一段健壮、通用、可维护的代码。建议读者按同样的思路梳理自己项目中的工具函数——当这种“定义—纠错—加固—验证”的闭环成为习惯,无论单个函数的质量,还是整个工程的可维护性,都会稳步提升。