
很多刚写 Python 的朋友,甚至有些老开发者,都会犯一个 “不起眼” 的错:函数明明没有返回值(比如只做打印、保存数据这类操作),却不明确标注返回None。比如写个打印问候的函数:
# 常见的“偷懒”写法:没标返回类型,也没写return
def print_greeting(name):
print(f"Hello, {name}!")你可能觉得 “反正没返回值,标不标无所谓”,但实际用的时候,别人(甚至一周后的你自己)看到这个函数,可能会猜:“它到底返不返回东西?返回的是字符串还是别的?” 万一有人傻呵呵地写result = print_greeting("Bob"),以为result能拿到点什么,最后发现是None,就得花时间查问题 —— 这就是没显式声明返回None的坑。
今天咱们就掰扯清楚:给函数显式声明返回None(也就是写-> None),到底有啥用?学会这一点,能让你的代码更规范、更少 bug,还能在面试时加分!
在讲好处之前,得先明确一个基础知识点:Python 里所有函数都有返回值。如果函数里没写return语句,或者只写了return(没带值),Python 会自动让它返回None。
比如这 3 个函数,实际返回值都是None,效果完全一样:
# 1. 没写return
def func1():
print("func1")
# 2. 写了return但没值
def func2():
print("func2")
return
# 3. 显式return None
def func3():
print("func3")
return None
# 测试一下:三个函数返回值都是None
print(func1()) # 输出 func1 + None
print(func2()) # 输出 func2 + None
print(func3()) # 输出 func3 + None既然 Python 会 “偷偷” 返回None,为啥还要多此一举显式声明-> None?这就像你出门时,明明门会自动关,但还是会手动推一下确认 —— 为了 “明确” 和 “不出错”。
接下来逐个讲好处,每个好处都配代码示例,保证你能跟着跑、能理解。
现在写 Python 代码,很多团队都会用 “静态类型检查工具”(比如mypy),它能在你运行代码前,就检查出 “返回值类型不匹配” 的错误。而显式声明-> None,就是给这个工具 “指路”,让它知道你这个函数就该返回None,一旦返回了别的东西,马上报错。
先安装mypy(用 pip 就行,代码能直接运行):
pip install mypy然后写两个函数,一个显式标了-> None,一个没标,看看mypy的反应:
# 函数1:显式声明返回None,但不小心返回了int(故意写的错代码)
def add_num(a: int, b: int) -> None:
return a + b # 这里错了:应该没返回值,却返回了int
# 函数2:没标返回类型,同样返回了int
def multiply_num(a: int, b: int):
return a * b用mypy检查这个文件(假设文件名叫test.py):
mypy test.py你会看到这样的结果:
test.py:2: error: Incompatible return value type (got "int", expected "None")
Found 1 error in 1 file (checked 1 source file)——mypy只报错了 “显式标了-> None” 的函数 1,没报错函数 2!因为函数 2 没标返回类型,mypy不知道你想返回啥,就没法检查。
如果函数实际返回None,却标了别的类型,mypy也会报错:
# 函数3:实际返回None,却标了返回int(错代码)
def print_sum(a: int, b: int) -> int:
print(a + b) # 没return,实际返回None
# 用mypy检查,会报:
# test.py:6: error: Missing return statement for function declared to return "int"总结:显式声明-> None,能让静态检查工具帮你提前发现 “返回值类型不对” 的 bug,不用等代码运行起来才踩坑 —— 尤其是大型项目,这个好处太重要了!
“自文档化” 就是说:别人看你的函数声明,不用读函数里面的代码,就知道这个函数是干嘛的、有没有返回值。显式-> None就像一个 “标签”,告诉所有人:“这个函数只做事,不返回东西,你别指望从它那拿到结果!”
比如有两个处理数据的函数,一个标了-> None,一个没标:
# 函数A:显式标了-> None
def save_to_db(data: dict) -> None:
"""把数据保存到数据库"""
# 里面可能有连接数据库、插入数据的代码(省略)
pass
# 函数B:没标返回类型
def process_data(data: dict):
"""处理数据"""
# 里面可能有清洗数据、过滤无效值的代码(省略)
pass别人用这两个函数时:
-> None + 注释 “保存到数据库”,马上知道 “这是写操作,不返回数据,调用就行,不用赋值”;对比维度 | 显式声明返回 None(def func () -> None) | 隐式返回 None(def func ()) |
|---|---|---|
可读性 | 高:一眼知道 “无返回值” | 低:需读函数内部才知道 |
理解成本 | 低:不用猜功能定位 | 高:容易误解 “可能有返回值” |
维护效率 | 高:改代码时不会误加返回值 | 低:改代码时可能忽略返回逻辑 |
新人友好度 | 高:新手一看就懂 | 低:新手容易踩 “赋值无用” 的坑 |
举个真实场景:比如print()函数,它的官方类型声明就是def print(*args, **kwargs) -> None—— 你一看就知道,调用print("hello")就行,不用写result = print("hello"),因为result肯定是None,没用。
Python 里有很多 “原地操作” 的函数(比如list.sort()、dict.update()),它们会直接修改原对象,不返回新东西(也就是返回None);而对应的 “返回新对象” 函数(比如sorted()、dict.copy()),会生成新对象返回,不修改原对象。
很多新手会搞混这俩,比如:
# 新手常见错误:把list.sort()的返回值赋值给变量
lst = [3, 1, 2]
new_lst = lst.sort() # 以为new_lst是排序后的列表
print(new_lst) # 输出None,傻了!如果显式声明返回None,这个坑就能避免 —— 因为list.sort()的官方类型声明是def sort(...) -> None,一看就知道 “这是原地排序,别赋值!”;而sorted()的声明是def sorted(...) -> list,一看就知道 “返回新列表,要赋值用!”
# 1. 原地操作函数:list.sort() -> None
lst1 = [3, 1, 2]
print("排序前lst1:", lst1) # 输出:排序前lst1: [3, 1, 2]
# 调用sort(),不赋值(因为返回None)
lst1.sort()
print("排序后lst1:", lst1) # 输出:排序后lst1: [1, 2, 3]
# 2. 返回新对象函数:sorted() -> list
lst2 = [3, 1, 2]
print("排序前lst2:", lst2) # 输出:排序前lst2: [3, 1, 2]
# 调用sorted(),赋值(因为返回新列表)
new_lst2 = sorted(lst2)
print("新列表new_lst2:", new_lst2) # 输出:新列表new_lst2: [1, 2, 3]
print("原列表lst2没变:", lst2) # 输出:原列表lst2没变: [3, 1, 2]
# 3. 再举一个:dict.update() -> None(原地修改) vs dict.copy() -> dict(返回新对象)
d1 = {"name": "Alice"}
# update()是原地修改,返回None,不赋值
d1.update({"age": 20})
print("修改后d1:", d1) # 输出:修改后d1: {'name': 'Alice', 'age': 20}
d2 = {"name": "Bob"}
# copy()返回新字典,要赋值
new_d2 = d2.copy()
new_d2["age"] = 22
print("原d2:", d2) # 输出:原d2: {'name': 'Bob'}
print("新new_d2:", new_d2) # 输出:新new_d2: {'name': 'Bob', 'age': 22}总结:显式-> None就像 “警告牌”,告诉使用者 “这个函数只改原对象,别赋值!”,完美避开 “赋值 None” 的新手坑。
讲完好处,再聊聊大家常犯的错误,每个错误都给代码示例,帮你避坑。
比如函数实际没返回值(返回 None),但类型注解标了-> str,mypy会报错,自己也容易误解:
# 错代码:实际返回None,却标了-> str
def get_user_name(user_id: int) -> str:
print(f"用户ID: {user_id}") # 没return,实际返回None
# 用mypy检查,报错:
# Missing return statement for function declared to return "str"正确写法:如果函数只打印不返回,就标-> None:
def get_user_name(user_id: int) -> None:
print(f"用户ID: {user_id}")有些人为了 “显式”,会在函数末尾加return None,但其实如果函数里已经有return(没值),或者函数逻辑到最后自然结束,再加return None就是多余的:
# 没必要的写法:函数逻辑到最后结束,加return None多余
def print_age(age: int) -> None:
if age < 18:
print("未成年")
return # 这里已经返回None了
print("成年")
return None # 多余!删掉不影响
# 正确简化写法:
def print_age(age: int) -> None:
if age < 18:
print("未成年")
return
print("成年")注意:不是不能写return None,而是 “没必要”—— 显式标注-> None已经够了,函数末尾的return None可省则省,代码更简洁。
这是新手最高频的错,比如用list.sort()、dict.update()的返回值赋值:
# 错1:用list.sort()赋值
lst = [5, 2, 4]
sorted_lst = lst.sort() # sorted_lst是None
print(sorted_lst[0]) # 报错:TypeError: 'NoneType' object is not subscriptable
# 错2:用dict.update()赋值
d = {"a": 1}
new_d = d.update({"b": 2}) # new_d是None
print(new_d["b"]) # 同样报错正确做法:记住 “标了-> None的函数,调用时别赋值”,直接用:
lst = [5, 2, 4]
lst.sort() # 直接调用,不赋值
print(lst[0]) # 输出2,正确
d = {"a": 1}
d.update({"b": 2}) # 直接调用,不赋值
print(d["b"]) # 输出2,正确面试时,面试官可能会问这些问题,结合咱们讲的知识点,用大白话答就行,不用背术语。
回答:主要有 3 个原因:
-> None就知道函数 “只做事不返回值”,不用读内部代码;list.sort() -> None,一看就知道别赋值,避免踩坑。回答:
list.sort()是原地排序,返回 None(显式标了-> None),调用时不能赋值,赋值会得到 None;sorted()是返回新的排序后的列表(标了-> list),调用时要赋值,才拿得到新列表;-> None的作用是 “提醒使用者”:list.sort()只改原列表,别指望从它那拿返回值,避免新手赋值用的错误。回答:
-> None,明确告诉工具和使用者,函数返回 None。回答:
先写函数:
def remove_duplicates(lst: list) -> None:
"""原地去除列表中的重复元素(保持第一次出现的顺序)"""
# 用临时集合存已出现的元素,遍历列表修改
seen = set()
i = 0
while i < len(lst):
if lst[i] in seen:
lst.pop(i) # 原地删除重复元素
else:
seen.add(lst[i])
i += 1
# 测试
my_lst = [1, 2, 2, 3, 1]
remove_duplicates(my_lst)
print(my_lst) # 输出:[1, 2, 3]设计原因:
-> None,告诉使用者别赋值;return lst,工具会报错,避免返回值类型不对;显式声明函数返回None,看起来是个小细节,但能解决很多实际问题:
尤其在团队开发或写开源代码时,这个小细节能显著降低沟通成本和维护成本 —— 别人不用猜你的函数行为,你自己后期改代码也不会迷糊。
从现在开始,写 Python 函数时:
-> None;-> None的函数,别赋值用;mypy这类工具辅助检查,让代码更规范。这么做,你的代码会越来越专业,bug 也会越来越少!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。