从这一节开始,我们会正式进入“如何组织代码”的阶段。函数是 Python 中最基础也最重要的抽象单位,很多看似零散的逻辑,一旦收拢成函数,代码的复用性、可测试性和可读性都会显著提升。
但函数真正难的地方并不在于 def 这个关键字,而在于参数设计、默认值、返回值表达、作用域边界以及可读性控制。这一节我们会围绕这些高频细节,把函数这件事彻底讲扎实。
如何定义一个函数?
函数最基础的作用,就是把一段有明确输入和输出的逻辑封装起来,给它一个名字,让它能够被重复调用。
Python 中定义函数的形式非常直接:
def greet(name):
return f"Hello, {name}"从语法上看,这件事很简单,但从组织代码的角度看,它背后至少解决了三件事:
- 给一段逻辑起了一个稳定名字;
- 把输入集中到参数里,而不是依赖外部零散变量;
- 把结果通过返回值显式交出去,而不是偷偷改一堆外部状态。
所以,函数最值得建立的心智不是“它能执行一段代码”,而是:它是一个边界清晰的处理单元。
这也是为什么很多初学者一开始写代码时,会觉得“直接顺着写下来更快”,但代码一多就开始混乱。不是因为函数太麻烦,而是因为没有函数,代码天然缺少边界。
一个好的函数,往往可以用一句话说清楚它做什么。比如:
load_config():加载配置;parse_user_input(text):解析用户输入;calculate_total_price(items):计算总价。
如果你发现一个函数很难被一句话概括,通常说明它的职责已经开始混杂了。
参数类型:位置参数、关键字参数与默认参数
函数之所以有价值,很大程度上是因为它能接收外部输入。而“输入怎么设计”,基本决定了这个函数好不好用。
最常见的参数,就是位置参数:
def add(x, y):
return x + y调用时按顺序传值:
result = add(1, 2)这类写法适合参数很少、语义很明确的场景。但只要参数一多,或者参数之间容易混淆,关键字参数就会开始体现优势:
def create_user(name, age, city):
return {"name": name, "age": age, "city": city}
user = create_user(name="Colin", age=28, city="Shanghai")关键字参数最大的价值不是“写法更长”,而是让调用点自己带上了说明。你不用再去猜 28 是年龄、工龄还是权限等级。
默认参数则进一步解决了“某些输入通常有一个默认值”的问题:
def connect(host, port=3306):
return f"{host}:{port}"这意味着函数作者已经替调用者做了一次常见选择。调用者如果不传,就接受默认行为;如果有特殊需求,再显式覆盖。
这里有一个特别值得养成的习惯:默认参数应该表达“常规情况”,而不是偷懒。
换句话说,你不应该因为“懒得传”就乱给默认值,而应该因为“绝大多数场景确实如此”才设置默认值。否则默认值会误导使用者,让函数表面上简单,实际行为却不可靠。
可变参数与关键字可变参数
有些函数并不适合提前写死参数个数,这时就会用到可变参数:
def add_all(*numbers):
return sum(numbers)*numbers 会把额外的位置参数收集成一个元组。它适合“数量不固定,但类型和语义一致”的输入场景,比如一组数字、一组文件路径、一组过滤条件。
与之对应的,还有关键字可变参数:
def build_profile(**kwargs):
return kwargs**kwargs 会把多余的关键字参数收集成字典。它适合元信息透传、配置聚合、包装底层调用等场景。
不过要特别注意,*args 和 **kwargs 很方便,但也很容易被滥用。只要不是确实需要“开放式参数”,就不要因为偷省参数设计而直接上这两个写法。因为一旦函数签名太宽泛,调用者和阅读者都很难快速知道它到底接受什么。
所以,实用上的建议是:
- 规则明确时,优先写清楚具体参数;
- 参数个数不固定时,再考虑
*args; - 参数集合天然开放时,再考虑
**kwargs。
默认参数的常见陷阱
函数参数里最经典的坑,就是把可变对象当默认值。
例如:
def append_item(item, items=[]):
items.append(item)
return items很多人第一次看到时会以为每次调用都会得到一个新列表,但实际上这个默认列表只会在函数定义时创建一次,后续调用会反复复用它。
这就会导致非常诡异的行为:
print(append_item(1)) # [1]
print(append_item(2)) # [1, 2]正确做法通常是把默认值设成 None,在函数内部再创建新对象:
def append_item(item, items=None):
if items is None:
items = []
items.append(item)
return items这个问题之所以重要,不只是因为“有个语法坑”,而是因为它提醒我们:函数定义阶段和函数调用阶段是两回事。
很多看起来写在参数里的东西,并不是“每次调用时重新计算”的。只要你建立了这个意识,后面理解装饰器、闭包、类属性等内容时都会更顺。
返回值与多返回值解包
函数除了接收输入,还要把结果交出来。Python 里最常见的方式,就是 return。
def divide(a, b):
return a / breturn 的核心价值,是让函数的输出显式、稳定、可组合。你可以把返回值继续传给别的函数,也可以拿来做判断、存储或测试。
没有写 return 时,Python 其实会默认返回 None。这也意味着:如果一个函数主要是执行动作,而不是产出结果,那返回 None 是合理的;但如果它承担计算职责,却忘了 return,那往往就是 bug。
Python 还支持“返回多个值”:
def get_user():
return "Colin", 28
name, age = get_user()表面上看像是返回了两个值,实际上本质上是返回了一个元组,只不过 Python 允许你在接收时直接解包。
这种写法很适合一组紧密相关的结果,比如:
success, dataname, agemin_value, max_value
但也要注意,多返回值并不等于想返回几个就返回几个。如果一个函数已经要返回四五个字段,往往说明你需要重新思考返回结构,可能更适合返回字典、dataclass,或者一个更明确的对象。
换句话说,多返回值的重点不是“酷炫”,而是让一组天然绑定的数据能被轻量表达出来。
变量作用域:局部、全局与闭包
函数一旦引入,就会自然带来另一个关键问题:变量到底在哪个范围内有效?
最常见的是局部作用域,也就是函数内部定义的变量只在函数内部可见:
def greet(name):
message = f"Hello, {name}"
return message这里的 message 就是局部变量。函数执行结束后,它就不应该再被外部依赖。
与之对应的是全局变量,也就是定义在模块顶层的变量:
APP_NAME = "Python Guide"函数内部默认可以读取全局变量,但如果你要在函数里直接修改全局变量,就必须显式声明 global。不过从工程实践上说,这通常不是一个值得鼓励的方向。因为一旦函数频繁读写外部全局状态,代码行为就会越来越难推断。
更值得理解的,是闭包场景。也就是内部函数引用了外层函数里的变量:
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment这里的 increment 即使在外层函数执行结束后,仍然能“记住” count,这就是闭包。
闭包本身并不神秘,你可以把它理解成:函数不仅能执行逻辑,也能携带一部分上下文。
后面学装饰器时,这个能力会非常重要。
所以,作用域真正要掌握的是这几个判断:
- 这个变量是函数自己的内部状态,还是外部共享状态?
- 这个函数是不是在偷偷依赖模块外部环境?
- 变量修改发生在哪里,阅读代码的人能不能一眼看出来?
只要这几个问题不清楚,函数就容易从“封装逻辑”退化成“制造隐性依赖”。
文档字符串与函数可读性
当函数越来越多时,另一个常见问题就出现了:语法上函数写对了,但别人读不懂你这个函数到底想干什么。
这时,函数名、参数名、文档字符串都会直接影响可读性。
先看最基本的文档字符串:
def calculate_discount(price, rate):
"""根据原价和折扣率计算折后价格。"""
return price * (1 - rate)文档字符串不是为了把代码重复一遍,而是为了补足“光靠函数名和参数名还说不清”的信息,比如:
- 这个函数的输入期待什么格式;
- 返回值代表什么;
- 有没有边界条件或异常情况。
除了文档字符串,更重要的是函数本身的设计要尽量可读。一个常见经验是:
- 函数名用动词短语,表达动作;
- 参数名表达真实业务语义,不要只写
x、data、obj; - 函数尽量只做一件事;
- 避免隐式副作用,尤其不要在别人看不见的地方偷偷改外部状态。
很多时候,最好的“文档”并不是额外写一大段说明,而是函数签名本身就已经把意图说清楚了。
总结与预告
这一节我们围绕函数把代码组织的第一层抽象讲清楚了,包括参数设计、默认值陷阱、返回值表达以及作用域边界。函数能力一旦打牢,后面的模块化、测试和工程拆分都会顺畅很多。
下一节我们会继续向上抽象,从单文件脚本过渡到模块和包,看看 Python 项目是如何真正组织起来的。