Skip to content
This page has been auto-translated and may contain errors.View in English

函数

docs.scrimba.com

随着程序变大,你会在多个地方写相同的逻辑。函数让你写一次逻辑,给它起个名字,然后在任何地方使用它。在一处修复它,所有调用都会自动获得修复。

函数是代码复用和抽象的主要单元。它们封装一个行为,给它起名字,定义清晰的接口(参数和返回值),并使其可从任何地方调用。命名良好的函数也可作为文档:validate_email() 告诉你代码块做什么,无需读取函数体。

在 Python 中,函数是一级对象:一个值,你可以像任何其他值一样传递它,所以它可以被赋给变量、存储在列表或字典中、作为参数传给另一个函数,也可以从函数返回。def 创建那个对象并将其绑定到当前作用域(代码中该名字可见的区域)中的一个名字。将函数视为值是使得 sorted(key=...) 和小型插件表等模式成为可能的原因,值得早点适应。

python
def greet(name):
    return f"Hello, {name}!"

print(greet("王明"))   # "Hello, 王明!"
print(greet("李芳"))     # "Hello, 李芳!"

写一次,到处使用,在一个地方修复。

定义函数

def 关键字开始函数定义,后跟名字、圆括号、冒号和缩进的函数体。函数在调用前什么都不做。用 def 定义它,然后用 () 通过名字调用它。

def 是一个语句,创建一个函数对象并将其绑定到当前作用域中的给定名字。函数体在定义时不被执行;它仅在函数被调用时运行。没有 return 语句的函数隐式地返回 None

def 构建一个函数对象并将其绑定到当前作用域中的名字。当 Python 读取 def 时函数体不会运行,它仅在调用函数时运行。这种分离在实践中很重要:函数体内的打字错误(比如不存在的名字)会保持沉默,直到函数实际被调用,所以从不调用的函数可以隐藏一个 NameError,这种错误只会在某人在生产环境中调用它时才浮现。

python
def say_hello():
    print("Hello!")

say_hello()   # 调用函数
Juno定义函数def name(): 并在下面缩进函数体。在用 name() 调用它之前什么都不会发生,def 只是设置它。如果忘记 return,函数会默默地返回 None,这在我早期开发时坑过我不止一次。
Juno定义函数def 创建函数并将其绑定到名字,但函数体仅在调用时运行。没有 return 意味着函数返回 None。当结果空无一物、不知道原因时,要记住这一点。
Juno定义函数 函数体在 def 时不运行,仅在调用时运行,所以函数内部的坏名字可以不被注意到直到有东西实际调用它。没有 return 的函数返回 None,当调用者期望一个值时这是它自己的安静bug。

参数和实参

参数是函数期望的输入。在圆括号内列出它们。调用函数时,传入的值按顺序匹配到参数。

参数定义函数的接口。实参是在调用时传入的具体值。位置实参按位置匹配;关键字实参按名字匹配。默认值使参数可选。

在调用时,位置实参从左到右绑定,关键字实参按名字绑定。以两种方式传递同一参数,或 Python 无法放置的参数,会在调用时引发 TypeError,而不是在函数体内部。Python 还让你约束一个签名:裸 * 使其后的所有内容都是只能用关键字传递的(调用者必须给它命名),/ 使其前的所有内容都是位置限制的。对于布尔标志和宽签名使用只关键字参数,其中在错误的位置传递一个位置 True 是那种编译通过、但传入错误的bug。

python
def greet(name, greeting):
    print(f"{greeting}, {name}!")

greet("王明", "你好")    # "你好, 王明!"
greet("李芳", "早上好")         # "早上好, 李芳!"

参数是函数定义中的名字。实参是调用函数时实际传入的值。实际上人们会交替使用这两个词;这种区别主要在读文档时重要。

Juno参数和实参 参数是在圆括号中列出的名字,实参是调用时实际传入的值。Python 从左到右对齐它们,所以传入的顺序就是它们落入的顺序。
Juno参数和实参 参数是定义中的名字,实参是调用时的值。位置实参从左到右绑定,所以位置是契约。这两个词会被交替使用,区别仅在读文档时重要。
Juno参数和实参 位置实参从左到右绑定,关键字实参按名字绑定,冲突在调用时会引发 TypeError。裸 * 强制调用者命名后面的内容,这是修复函数其 True/False 标志在调用时会是一个猜谜游戏问题的便宜方法。

默认值

你可以给参数一个默认值。如果调用者没有提供该实参,会使用默认值。有默认值的参数必须在没有默认值的参数之后。

默认值使参数可选。它们在定义时求值一次,而不是在每次调用时。这对可变默认值很重要:def f(items=[]) 在所有调用间共享相同的列表。修复方法是使用 None 作为默认值,并在函数体内创建列表。

细节是这样咬人的:默认值在 def 运行时构建一次,并被每个使用它的调用共享。对于不可变默认值如 0"Hello"(一个你不能原地改变的值)这很好。对于可变默认值如列表或字典(一个你可以原地改变的值)这是个陷阱,因为每个回到默认值的调用都会复用同一对象:

python
def add_item(item, items=[]):   # 列表创建一次,永远共享
    items.append(item)
    return items

add_item("a")   # ['a']
add_item("b")   # ['a', 'b']  <- 不是一个新列表

修复方法是标准的:默认为 None,然后在函数体内构建一个新的容器。

python
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
python
def greet(name, greeting="你好"):
    print(f"{greeting}, {name}!")

greet("王明")           # "你好, 王明!"
greet("王明", "早上好")     # "早上好, 王明!"

有默认值的参数必须在没有默认值的参数之后。

Juno默认值 给参数一个默认值,它就变成可选的,调用者可以跳过它。有默认值的必须在必需的参数之后,这个顺序是 Python 强制的规则。节省了在每次调用时写相同值的时间。
Juno默认值 默认值使参数可选,必须放在必需的参数之后。陷阱是:默认值在定义时求值一次,所以永远不要使用可变的默认值如 []。默认为 None 并在函数体内构建列表。
Juno默认值 默认值在 def 时构建一次并在调用间共享,所以 items=[] 悄悄地在调用间累积。默认为 None 并在函数体内构建新的。这在真实代码审查中比几乎任何其他 Python 滑脚都出现得更频繁。

关键字实参

调用函数时,你可以给实参命名。这使调用可读,尤其对有许多参数的函数,并让你以任何顺序传递它们。

关键字实参使函数调用自说明。你可以混合位置和关键字:位置实参必须首先出现。对于有布尔标志或许多相似类型参数的函数,关键字实参会阻止从以错误顺序传递实参而导致的静默错误。

关键字实参按名字绑定,所以它们读得好并解放你免于记住位置。调用站点的规则:位置实参首先出现,关键字实参之后,两次命名同一参数会引发 TypeError。值得做的设计举动是相反的:在你自己的签名中放一个裸 * 使调用者必须命名参数(def connect(host, *, timeout=30))。这是免费的,并把一个晦涩的 connect("db", 30) 转变为一个 connect("db", timeout=30) 它在之后某人在中间添加参数时仍然有效。

python
def describe_player(name, score, level):
    print(f"{name} | 分数: {score} | 级别: {level}")

describe_player("王明", 87, 5)                        # 位置实参
describe_player(name="王明", level=5, score=87)       # 关键字实参,任意顺序
describe_player("王明", level=5, score=87)            # 混合:位置实参首先
Juno关键字实参 在调用时给实参命名(score=87),调用就解释了自己,加上你可以以任何顺序传递它们。一个规则:任何位置实参必须在命名的实参之前。对于有堆积参数的函数很好。
Juno关键字实参 命名实参使调用自说明且顺序无关,位置实参首先需要。对于宽签名或布尔标志,偏好关键字实参所以没人必须数位置来读调用。
Juno关键字实参 关键字实参按名字绑定,必须跟随任何位置实参。真正的杠杆是你自己的签名:裸 * 强制调用者命名其后的内容,所以在某人在中间插入新参数后调用仍然读得好。

返回值

return 向调用者发送一个值。没有 return,函数返回 None一旦 return 运行,函数立即退出。那个代码块之后的任何代码都被跳过。

return 从函数退出并向调用者传递一个值。没有显式 return 的函数隐式地返回 Nonereturn 可以出现在函数体任何地方并可能被使用多次;到达的第一个函数结束函数。这使早返回对守卫条款有用。

return 把一个值交给调用者并在这个点结束函数,一个在结束时没有 return 运行的函数返回 None。多个返回很好的风格,不是味道:一个早期的 return 对失败或边界情形在上面("守卫条款"模式)读得远好于在一个大 if 内嵌整个函数体。一个在 try 块内 return 前值得知道的微妙之处:它仍然首先运行匹配的 finally 块,所以 finally 中的清理发生即使你早期返回。

python
def add(a, b):
    return a + b

result = add(3, 4)   # result = 7
print(result)

return 也立即退出函数。代码块之后的任何代码都不运行。

Juno返回值return 把一个值返回给任何调用函数的人,函数在那里停止,其后的任何东西都被跳过。留下 return 你得到 None 返回。print 显示一个值,return 把它返回给使用,花我一段时间感受那个差别。
Juno返回值return 传递一个值并立即退出,没有 return 意味着 None。你可以有好几个,一个对边界情形的早期 return 在上面通常读得比包装所有东西在一个大 if 中更清楚。
Juno返回值return 立即退出,落到结尾返回 None。依赖早期返回作为守卫条款而不是深嵌套。记住 try 内的 return 仍然运行 finally 块,所以清理在那里触发甚至在早期退出。

返回多个值

Python 让你通过用逗号分隔来返回多个值。调用者将它们作为元组接收并可以在一行中将它们解包到分离的名字。

用逗号返回多个值把它们打包到元组。调用者用匹配的名字解包。这是自然产生多个结果的函数的习惯 Python。这不是特殊的特性;这是元组打包和解包。

return a, b 把值打包到元组,调用者用 low, high = f() 把它们拉出。它对两个或三个结果读得很干净。在那之后,位置变成一个责任:没人调用函数时记得第三项是计数还是平均数,一个交换的对是一个静默的bug。此时返回起名字的东西,一个 NamedTuple@dataclass,所以调用者可达到 result.average 而不是 result[2]。以任一方式用 tuple[int, str] 标注返回所以一个类型检查器可以捕获一个调用者解包错误数量的值。

python
def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([3, 7, 1, 9, 4])
print(low, high)   # 1 9

low, high = ... 语法是解包:Python 把每个返回的值分配给对应的名字。

Juno返回多个值 用逗号分隔值,函数把它们一起作为元组返回。调用者在一行捕获它们,low, high = min_max(...),每个值一个名字。当一个函数自然地产生多个答案时很方便。
Juno返回多个值 逗号把值打包到元组,调用者用匹配的名字解包。这是平常的元组打包和解包,不是特殊的特性。对两个或三个结果很干净,在那之后则不太好。
Juno返回多个值 逗号返回打包一个元组,对两个或三个值很好。在那之后,位置转为陷阱没人能读,所以返回一个 NamedTuple@dataclass 让调用者使用 result.average 而不是数索引。

作用域

在函数内创建的变量仅在该函数内存在。你不能从外部看到它们。在所有函数外定义的变量随处可见,但在没有显式声明的情况下,你不能从函数内改变它。

在函数内创建的名字是局部的:它仅活在那里并在函数返回时消失。在文件顶级定义的名字是全局的。从函数内读取全局变量无需仪式,但赋值给一个需要 global name,否则 Python 将赋值视为创建一个阴蔽全局的新局部变量。实际上你很少想要 global:把值作为实参传入并用 return 返回结果使函数的效果在调用站点可见。

Python 用 LEGB 规则解析一个名字,按顺序检查四个地方:Local(这个函数)、Enclosing(一个包装这个的外部函数)、Global(模块)、Built-in(len 之类的名字)。读取走那个链;赋值总是创建局部除非你另外说。那个不对称是bug的源头:count += 1 对全局而没有 global count 行会引发 UnboundLocalError,因为赋值为整个函数标记 count 局部并读取发生在任何局部值存在之前。global name 选择进赋值回到模块级;nonlocal name 取代最近的包装函数,这是一个闭包从使其的函数更新变量的方式。把两个都当作一个smell:到达并改变外部状态的函数比花费输入并返回输出的一个更难测试。

Juno作用域 在函数内做出的变量仅活在那里,外部看不到它。你可以从内部读一个外部变量,但改变一个需要首先有 global 行。几乎从不到达那个:传入值,返回结果,函数保持简单可跟踪。
Juno作用域 在函数内做出的名字是局部的并在它返回时消失。读一个全局是免费的,写一个需要 global name 或 Python 做一个阴蔽它的局部。偏好参数进和 return 出所以函数的效果在调用保持可见。
Juno作用域 为查找 LEGB,但赋值总是除非你声明 globalnonlocal 做局部,这是为什么 count += 1 对全局抛出 UnboundLocalError。两个关键字都是一个smell:改变外部状态的函数比一个花费输入并返回输出的更难测试。
python
def calculate():
    result = 42   # 这个函数的局部
    return result

calculate()
print(result)   # NameError,result 不在这里存在
python
count = 0

def increment():
    global count    # 声明你想修改全局
    count += 1

increment()
print(count)   # 1

使用 global 应该是最后的手段。它使代码更难推理。偏好传入值并返回它们。作用域直接构建在赋值如何把名字绑定到值上,在变量和类型章中覆盖。

*args 和 **kwargs

有时你不知道函数会收到多少实参。*args 收集任何数量的位置实参到一个元组。**kwargs 收集任何数量的关键字实参到一个字典。名字 argskwargs 是约定;星号很重要。

*args 收集多余位置实参到元组。**kwargs 收集多余关键字实参到字典。两者都可以与常规参数结合。常规参数首先出现,然后 *args,然后只关键字参数,然后 **kwargs。它们对把实参通过传递给另一个函数的包装函数有用。

*args 把剩余的位置实参聚集到 tuple 中,**kwargs 把剩余的关键字实参聚集到 dict 中。镜像图像发生在调用站点:func(*some_list) 把列表铺展到位置实参,func(**some_dict) 把字典铺展到关键字实参。那个对称是通过包装工作的:def timed(*args, **kwargs): return inner(*args, **kwargs) 不命名一个单一参数就转发任何它得到的。有用,但它费用你读得好的签名:一个包装吞掉所有东西到 *args, **kwargs 的显示调用者和类型检查器什么它接受,所以当你实际需要通过任意实参,不作为一个普通函数的默认形状时到达它。

python
def total(*args):
    return sum(args)

total(1, 2, 3)          # 6
total(1, 2, 3, 4, 5)   # 15
python
def display(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

display(name="王明", score=87, level=5)

你可以用常规参数混合它们。常规参数首先出现:

python
def describe(title, *tags, **metadata):
    print(f"{title} | 标签: {tags} | 元数据: {metadata}")

describe("Python 入门", "初学者", "python", author="王明", year=2024)
Juno*args 和 **kwargs*args 舀出任何数量的额外位置实参作为元组,**kwargs 舀出额外的命名实参作为字典。单词 argskwargs 仅是约定,*** 做真实的工作。当你不能提前说有多少东西进来时很方便。
Juno*args 和 **kwargs*args 收集多余位置实参到元组,**kwargs 多余关键字实参到字典。签名中的顺序:常规参数,然后 *args,然后只关键字,然后 **kwargs。对转发实参直接到另一个函数的包装最有用。
Juno*args 和 **kwargs*** 在签名中收集并在调用站点铺展,这是让一个包装用 inner(*args, **kwargs) 通过所有东西所需的。费用是一个签名它告诉调用者什么,所以为真实通过、不普通函数保持它。

文档字符串

文档字符串是函数顶部的字符串,描述它做什么。Python 编辑器和工具用它来显示帮助当你悬停函数调用上时。使用三个引号,对简单函数写一行。

文档字符串是函数体中的首项,一个三个引号中的字符串。工具读它:help(func) 打印它,你的编辑器在悬停调用时显示它,所以它恰好当你忘记函数做什么时回报。约定是一个摘要行,然后一个空行和更多细节如果需要。把它作为必要的任何被称为来自多个地方,对一个一次性帮手其名字已经说一切是可选的。

文档字符串是函数、类或模块体中的首语句,它应该说函数做什么和它返回什么,不是重述参数列表读者已经能看到。它对任何其契约不可见的东西赚取保留:边界情形,它抛出什么,一个空输入做什么。当类型提示在签名中已经执行类型时,跳过正式参数按参数块那个细节属于提示,不重复在离开日期的散文中。目标是你想在凌晨2点读的行,在调试一个你六个月前写的调用时。

python
def normalise(value, min_val, max_val):
    """Scale a value to the 0-1 range given the known min and max."""
    return (value - min_val) / (max_val - min_val)
python
def build_url(base, version, resource, *, secure=True):
    """
    Build an API endpoint URL.

    Returns a fully-qualified URL string. If secure is False,
    the URL will use http instead of https.
    """
    scheme = "https" if secure else "http"
    base = base.replace("https://", "").replace("http://", "")
    return f"{scheme}://{base}/{version}/{resource}"

为任何函数其目的不清楚从其名字和签名写文档字符串。

Juno文档字符串 文档字符串是函数顶部的三引号字符串,描述它做什么。你的编辑器在悬停函数时显示它,那是你会高兴它在那时的时候。当名字和输入不已经使目的清楚时随时写一个。
Juno文档字符串 函数体的首行,三引号:help() 和你的编辑器在悬停时冒出来。一个摘要行覆盖大多函数,仅当行为需要它时添加下方的细节。对任何从好几个地方调用的必需的,对一个单行其名字说一切时可跳过。
Juno文档字符串 说函数做什么,它返回什么和边界情形,不是重述读者已看到的参数列表。让类型提示执行类型所以你不保留两倍相同的事实。目标是在凌晨2点调试一个来自六个月前的调用你想读的行。

类型提示

类型提示让你标注函数期望和返回的类型。Python 在运行时不强制它们,但编辑器用它们在运行任何东西前捕获错误。-> 在冒号前指定返回类型。

类型提示是工具验证的文档。编辑器和类型检查器(mypy、pyright)用它们在运行时前捕获类型不匹配。它们在标准 Python 中没有运行时效果。-> None 是对没有返回值函数的正确标注。对通用容器,使用 list[int]dict[str, int](Python 3.9+)。

类型提示在运行时改变什么都不,Python 运行相同地带或不带它们。它们的整个值是程序外的层:一个类型检查器(mypy 或 pyright)在它运行前读代码,你的编辑器在你打字时标志坏调用。所以一个说谎的提示比没有提示更坏,因为检查器信任它。使用 list[int]dict[str, int]str | None 直接(现代 Python 上没有 typing 导入需要),并标注返回什么都不的函数为 -> None,它也发信号调用它在表达式(x = log(...))是错误。回报登陆在从许多地方调用的函数:检查器捕获一个调用者传递错误类型而不是那个显示为一个在运行时三帧深的混乱失败。

python
def greet(name: str, score: int) -> str:
    return f"{name} scored {score}"
python
def log(message: str) -> None:
    print(f"[LOG] {message}")
python
def top_scores(scores: list[int], n: int) -> list[int]:
    return sorted(scores, reverse=True)[:n]

类型提示是可选的但对将被从多个地方调用的任何函数有价值。它们是工具可以验证的文档。

Juno类型提示 类型提示注记函数花费和给回什么,如 name: str-> str 对返回。Python 当它运行时不会强制它们,但你的编辑器读它们并在你击中运行前警告你。把它们看作工具可以检查的注释。
Juno类型提示 提示是类型检查器可以验证的文档,有没有运行时效果。-> None 是正确的标注当函数返回什么都不时,list[int] / dict[str, int] 在现代 Python 上工作没有 typing 导入。对从好几个地方调用的值得的。
Juno类型提示 提示在运行时做什么都不,它们的值是检查器和编辑器读它们,所以一个说谎的提示比无提示更坏。使用 str | Nonelist[int] 直接,-> None 对无返回。赢是在它失败三帧深前捕获一个错误打字调用者。

作为值的函数

Python 中的函数是值,如字符串或数字。你可以把它们赋给变量并传递给其他函数。这是 sorted() 如何接受 key= 函数。

函数是一级对象:它们有类型(function),可以储存在变量和集合中,可以作为实参或返回值传递。这是 sorted(key=...)map()filter() 之类的高阶函数的基础。

函数是一个你按引用传递的对象,从不是拷贝,所以把一个交给另一个函数是便宜的。从这个长出的模式是闭包:在另一个函数内定义的一个仍然在外部函数已经返回后到达外部函数的变量。这是你如何即时构建一个配置的函数,一个 make_multiplier(3) 返回一个乘以3的函数,3捕获和保持。真实代码的两个注意:闭包捕获变量,不是在捕获时的值,哪个在循环中构建闭包时捕获人们(它们全部看到循环的最终值);和堆积许多返回的函数使调用路径难以跟踪,所以当它们读清楚时使用闭包并当状态长大时到达一个类。

python
def double(x):
    return x * 2

def apply(func, value):
    return func(value)

apply(double, 5)   # 10

通过函数作为实参显示持续地用 sorted()map()filter()。你也会在Lambda和列表推导式章看到它。

Juno作为值的函数 函数是一个值,如字符串或数字,所以你可以把它储存在变量中并把它交给另一个函数。那完全是什么让你通过一个函数作为 key=sorted()。我第一次感到陌生,然后它点击了并转身几乎处处出现。
Juno作为值的函数 函数是一级对象:赋它,把它储存在列表,通过它,返回它。那是 sorted(key=...)map()filter() 后面的引擎,和每个回调你稍后会接起来。
Juno作为值的函数 通过一个函数是引用,不是拷贝,闭包让内部函数在外部已返回后保持到达外部的变量。它捕获变量,不是值,所以在循环中构建闭包咬每一次。当捕获状态长大时,类读得更好。

实际应用

两个一起工作的函数:letter_grade 转换分数到字母,summarise 对列表中的每个分数调用它:

python
def letter_grade(score: int) -> str:
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    else:
        return "F"

def summarise(scores: list[int]) -> None:
    total = sum(scores)
    avg = total / len(scores)
    grades = []
    for s in scores:
        grades.append(letter_grade(s))
    print(f"平均值: {avg:.1f}")
    print(f"等级: {', '.join(grades)}")

summarise([87, 92, 74, 65, 91])

一个日志格式化器和一个使用它的文件处理器,带有一个 dry_run 默认参数防止副作用除非显式禁用:

python
def format_log(level: str, message: str) -> str:
    return f"[{level.upper():5}] {message}"

def process_file(path: str, dry_run: bool = True) -> bool:
    print(format_log("info", f"处理 {path}"))
    if dry_run:
        print(format_log("info", "干运行,没有修改"))
        return True
    return True

process_file("report.csv")
process_file("report.csv", dry_run=False)

一个单值正规化器和一个构建在它顶部的列表正规化器,带有类型提示和文档字符串。列表函数计算范围一次并重用标量函数对每项:

python
def normalise(value: float, min_val: float, max_val: float) -> float:
    """Scale value to the 0-1 range given known min and max."""
    if max_val == min_val:
        return 0.0
    return (value - min_val) / (max_val - min_val)

def normalise_column(values: list[float]) -> list[float]:
    """正规化一个完整的值列."""
    lo, hi = min(values), max(values)
    return [normalise(v, lo, hi) for v in values]

raw = [10.0, 25.0, 5.0, 40.0, 15.0]
print(normalise_column(raw))

类型提示在这里服务两个目的:它们文档化函数期望什么,它们让一个类型检查器捕获调用者通过一个字符串列表时的错误。