1.1 写函数 · Writing Functions

1.1 写函数 · Writing Functions

本章目标

完成本章后,你能够:

  1. identify 代码中的重复模式并说出「三次法则」(rule of three)
  2. write 带命名参数、默认值与显式 return() 的函数
  3. predict 给定输入时函数的输出(含向量化行为)
  4. diagnose 参数未匹配 / 作用域泄漏两类高频错误
  5. evaluate 别人写的函数是否值得抽象(AI 织入点:让 AI 当评审)

前置自测(≤5 分钟)

能独立完成以下两问再继续;否则先回补 tidyverse 入门要求:

重要Check In:前置自测
  1. 用 dplyr 对 palmerpenguins::penguins 按 species 分组求 bill_length_mm 均值(忽略缺失值)。
  2. 解释 |> 管道把左边的东西传到了右边函数的哪个位置。

1. 为什么写函数:三次法则

我曾经把同一段清洗代码复制了 11 遍——第 12 份数据来的时候发现前 11 遍里有个 bug。 函数不是高级技巧,是止损策略。

三次法则:同一段逻辑复制粘贴第三次出现时,就必须抽成函数。

复制粘贴的三宗罪:

罪 后果
改一处漏十处 bug 复利
无命名 代码不解释意图
无法测试 每份拷贝都要单独验证

函数解决三件事:命名(是什么)·参数(变什么)·返回(给什么)。

2. 函数解剖学

最小可用的函数:

mean_bill <- function(data, species) {
  data |>
    dplyr::filter(species == {{ species }}) |>
    dplyr::summarise(mean_bill = mean(bill_length_mm, na.rm = TRUE)) |>
    dplyr::pull(mean_bill)
}

mean_bill(palmerpenguins::penguins, "Adelie")   # 38.8

三个组成部分:

  1. 名字:动词或动词短语(mean_bill 而非 func1)
  2. 参数:数据 data 与变化点 species——函数是「不变的东西 + 可变的洞」
  3. 返回值:R 默认返回最后一个表达式;建议在非平凡函数里显式 return()
注记

{ species }(大括号拥抱)是 tidyverse 的「函数内引用列名」惯例, 来自 Programming with dplyr。 现在只需照用,原理在 Unit 3 展开。

3. 参数设计:默认值与命名调用

mean_bill <- function(data, species = "Adelie", digits = 1) {
  data |>
    dplyr::filter(.data$species == .env$species) |>
    dplyr::summarise(mean_bill = mean(bill_length_mm, na.rm = TRUE)) |>
    dplyr::pull(mean_bill) |>
    round(digits)
}
pg <- palmerpenguins::penguins
mean_bill(pg)                    # 全默认
mean_bill(pg, species = "Gentoo")# 命名调用,跳过 digits

设计准则:

  • 高频用法给默认值,函数「开箱即得 80% 场景」
  • 调用时用命名参数传非默认值——位置传参是未来 bug 的温床
警告高频错误:位置传参错位

mean_bill(pg, 1) 不会报错!1 被当成 species, filter(species == 1) 静默返回空表 → 后续 mean() 得 NaN。 排查口诀:看见诡异的 NaN/0 行,先查参数顺序。

4. 向量化思维:数据框就是循环

R 的函数大多天生向量化:round(x) 不需要你写 for 循环。

vals <- c(1.234, 5.678, 9.012)
# 不写循环 ✗
for (i in seq_along(vals)) vals[i] <- round(vals[i], 1)
# 直接向量化 ✓
round(vals, 1)

判断题:什么时候必须自己迭代?——当每一步依赖上一步的状态时 (如逐月滚动预测)。其余交给向量化或 1.2 章的 map()。

5. 作用域:函数有独立的房间

函数先查自己的参数与局部变量;找不到时,会沿定义它的环境向外查找,因此也可能读到全局变量。

警告高频错误:作用域泄漏
threshold <- 30
flag_big <- function(x) x[x > threshold]   # 体内引用全局变量:能跑,但是债

在别的电脑/脚本里 threshold 不存在 → 炸。 规则:函数需要什么,就把什么写成参数。

6. 非正式测试:写完就查三件事

每个新函数交付前跑「三例检查」:典型值、边界值、垃圾值。

重要Practice Exercise 1(copy 档)

写 count_species(data, island) 返回各物种计数表,并过三例检查: ① 典型:penguins, "Biscoe";② 边界:一个不存在的岛(预期是什么?先猜再跑); ③ 垃圾:count_species(penguins, 123)。 把三例的预期 vs 实际写成三行注释。

重要Practice Exercise 2(adapt 档)

把本章 mean_bill() 改造成 summarize_col(data, col, by, digits): 任意数值列 × 任意分组列。要求:{ } 引用列、digits 有默认值、过三例检查。

重要Practice Exercise 3(create 档 · AI 环节)

第一轮(禁 AI):为体检数据集写 bmi_category(height_cm, weight_kg), 返回 BMI 分级向量(<18.5 偏瘦 / 18.5–24 正常 / 24–28 超重 / ≥28 肥胖, 中国标准)。 第二轮(开放 AI):把你的函数贴给 Posit Assistant,只问一个问题: 「这个函数的边界值测试清单应该有哪些?」补齐清单后再跑一遍。 记录:AI 补出了哪几个你没想到的边界?

Capstone · 压轴项目

任务:自选真实数据集(本班提供:体检 / penguins / nycflights 三选一), 找出你分析脚本里最重复的三段代码,各抽成函数,并用三例检查验证。 交付:Quarto 报告一页——三个函数源码 + 三例检查表 + 一段「抽象前后」对比说明。

维度 达到 良好 卓越
抽象质量 三段均成函数 参数设计合理(默认值/命名) 函数名即文档,可复用于新数据
验证 三例齐 预期先于运行写下 发现并修复了至少一个边界 bug
表达 报告完整 对比说明有说服力 提炼出个人「何时抽象」判断规则

SOURCES · 来源映射

讲义节 素材 性质
§1–§5 结构与练习设计 posit::conf(2025) r-programming 01-functions-*.qmd(Emma Rand, Garrett Grolemund, Ian Lyttle · CC-BY-SA 4.0) 改编
{ } 惯例 dplyr 官方 Programming with dplyr vignette 引用
讲义文字、中国 BMI 标准练习、rubric 本项目 原创

本章以 CC-BY-SA 4.0 发布。