0.4 函数与作用域

概述

你已经熟悉 JS 的 function、箭头函数、默认参数、rest 参数、闭包、高阶函数(HOC)。Python 的函数机制高度相似,但参数系统更强大:支持位置参数、关键字参数、默认参数、*args / **kwargs。装饰器是 Python 里比 JS 高阶函数更常见的用法。这一章逐一对照。

JS/TS ↔ Python 对照表

概念

JavaScript

Python

说明

声明函数

function f() {}

def f():

def 关键字

箭头函数

(x) => x * 2

lambda x: x * 2

lambda 只能单表达式

默认参数

function f(a = 1)

def f(a=1):

语义一致

rest 参数

function f(...args)

def f(*args):

*args 收集位置参数

关键字参数

无(用对象模拟)

def f(**kwargs):

**kwargs 收集命名参数

参数解构

function f({ a, b })

def f(a, b): 调用时传 f(**d)

** 展开字典

返回多值

return [a, b]

return a, b(自动打包成 tuple)

Python 更自然

高阶函数

函数是一等公民

同样是一等公民

可传函数、返回函数

闭包

支持

支持

变量捕获机制类似

立即调用 IIFE

(() => {})()

少见,用模块实现

见下文

装饰器

手动包一层(HOC)

@decorator 语法糖

Python 语法级支持

参数是否可变

可变

可变,但有陷阱

见"可变默认参数"

函数内改外部变量

闭包捕获

nonlocal 关键字

赋值语义不同

重载

无(用参数判断)

无(用默认参数/类型判断)

都不原生支持

核心概念

1. def vs function / 箭头函数

function add(a, b) {
  return a + b;
}
const double = (x) => x * 2;
def add(a, b):
    return a + b

double = lambda x: x * 2   # lambda 只用于极简场景
  • def 定义命名函数,lambda 定义匿名函数(只能是一个表达式,不能有语句)。

  • 类比:deffunctionlambda ≈ 箭头函数,但 lambda 能力更弱(不能写多行、不能有赋值)。

2. 位置参数、关键字参数、默认参数

Python 调用函数时可以按参数名传参(JS 只能用对象解构模拟):

// JS:用对象模拟关键字参数
function greet({ name, greeting = "Hello" }) {
  return `${greeting}, ${name}`;
}
greet({ name: "Alice" });
# Python:原生支持关键字参数
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}"

greet("Alice")                       # 位置参数
greet(name="Alice")                  # 关键字参数
greet(greeting="Hi", name="Alice")   # 关键字可乱序
  • 调用时 f(a=1) 就叫关键字参数,这是 JS 没有的、非常实用的特性。

  • 定义时 def f(a=1)a=1默认参数

3. *args / **kwargs vs rest 参数

// JS:rest 收集所有参数
function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6
# Python:*args 收集位置参数
def sum_all(*nums):
    return sum(nums)

sum_all(1, 2, 3)   # 6

# **kwargs 收集关键字参数(JS 没有直接对应)
def print_info(**info):
    for k, v in info.items():
        print(k, v)

print_info(name="Alice", age=30)
  • *args 把多余位置参数打包成 tuple,类似 JS 的 ...args(打包成数组)。

  • **kwargs 把关键字参数打包成 dict,是 JS 没有的。

4. lambda vs 箭头函数

const nums = [1, 2, 3];
const squares = nums.map(x => x * x);
nums = [1, 2, 3]
squares = list(map(lambda x: x * x, nums))
# 但更 Pythonic 的是推导式:
squares = [x * x for x in nums]
  • lambda 只能写单个表达式,不能包含 if/for 语句(lambda 里可用条件表达式)。

  • 实际项目中,能用推导式就用推导式,lambda 主要作为 sortedmapfilter 的简短回调。

5. 作用域 LEGB vs 词法作用域

JS 有词法作用域(函数作用域 + 块级作用域)。Python 用 LEGB 规则查找变量名:

  • Local:当前函数内部

  • Enclosing:外层函数(闭包)

  • Global:模块全局

  • Built-in:内置名字(如 lenprint

// JS
const x = "global";
function outer() {
  const x = "enclosing";
  return function inner() {
    return x; // "enclosing"
  };
}
# Python
x = "global"
def outer():
    x = "enclosing"
    def inner():
        return x      # "enclosing"
    return inner

关键差异:在函数内给变量赋值,Python 默认会创建局部变量,而不是修改外部变量

let count = 0;
function increment() {
  count++;      // 修改外部变量,OK
}
count = 0
def increment():
    global count   # 必须声明,否则报错
    count += 1

def outer():
    n = 0
    def inner():
        nonlocal n  # 修改外层函数变量需 nonlocal
        n += 1
  • 读外部变量不需要声明,但赋值/修改外部变量需要 global(模块级)或 nonlocal(闭包外层)。

6. 闭包

闭包机制和 JS 几乎一致:函数内定义函数,内层函数捕获外层变量。

function counter() {
  let count = 0;
  return () => ++count;
}
def counter():
    count = 0
    def inc():
        nonlocal count   # 修改捕获的变量需要 nonlocal
        count += 1
        return count
    return inc

7. 装饰器 vs 高阶函数 / HOC

你已经熟悉 React 的 HOC:一个函数接收组件、返回增强后的组件。Python 的装饰器本质相同——接收函数、返回新函数,但有语法糖 @

// JS:高阶函数手动包装
function withLogging(fn) {
  return function (...args) {
    console.log("调用", fn.name);
    return fn(...args);
  };
}
const wrapped = withLogging(add);
# Python:装饰器 + @ 语法糖
def with_logging(fn):
    def wrapper(*args, **kwargs):
        print("调用", fn.__name__)
        return fn(*args, **kwargs)
    return wrapper

@with_logging          # 相当于 add = with_logging(add)
def add(a, b):
    return a + b
  • @decorator 就等价于 fn = decorator(fn),只是写在定义处更清晰。

  • 类比 React 的 @connect(...)@withRouter(...)(TS 里也有类似装饰器语法)。

8. 纯函数与可变默认参数陷阱

Python 的默认参数只在函数定义时求值一次,可变默认参数会被共享,这是经典陷阱:

// JS:每次调用都会新建数组
function append(item, arr = []) {
  arr.push(item);
  return arr;
}
append(1); // [1]
append(2); // [2]  ← 每次都是新数组
# Python:默认参数只求值一次,共享同一个 list!
def append_item(item, arr=[]):
    arr.append(item)
    return arr

append_item(1)   # [1]
append_item(2)   # [1, 2]  ← 出错!被共享了

正确写法:用 None 作哨兵值:

def append_item(item, arr=None):
    if arr is None:
        arr = []
    arr.append(item)
    return arr

代码示例

带默认值、可变参数、关键字参数的完整例子

// JS
function createUser(name, { age = 18, ...rest } = {}) {
  return { name, age, ...rest };
}
createUser("Alice", { age: 30, city: "北京" });
# Python
def create_user(name, age=18, **extra):
    return {"name": name, "age": age, **extra}

create_user("Alice", age=30, city="北京")

排序回调对比

// JS
const sorted = users.sort((a, b) => a.age - b.age);
# Python
sorted_users = sorted(users, key=lambda u: u["age"])
# 或更清晰:用 operator
from operator import itemgetter
sorted_users = sorted(users, key=itemgetter("age"))

最佳实践

  • 参数多的函数优先用关键字参数调用,提升可读性:f(user=u, force=True)

  • 可变默认参数用 None 哨兵模式,绝不用 [] / {} 作默认值。

  • 短回调用 lambda,稍复杂就写具名 def,别硬塞进 lambda

  • 需要"增强函数"时用装饰器,跨切面逻辑(日志、计时、缓存、鉴权)尤其合适。

  • 修改外部变量时明确写 global / nonlocal,不要依赖隐式行为。

  • 返回多个值直接 return a, b(自动打包 tuple),调用方 a, b = f() 解包。

前端开发者常见陷阱

  1. 忘写 return:Python 函数没有 return 时返回 None,和 JS 的 undefined 类似但名字不同,容易在调试时困惑。

  2. 缩进定义函数体def 后面冒号,函数体缩进,忘了缩进直接 IndentationError

  3. lambda 能力有限:不能写多行逻辑,别像箭头函数那样随便塞大段代码。

  4. 可变默认参数被共享:这是 Python 转过来最常见的坑,务必用 None 哨兵。

  5. 函数内赋值不修改外部变量:以为闭包能像 JS 那样 count++,实际需要 nonlocal / global

  6. *** 用混*args 打包位置参数(tuple),**kwargs 打包关键字参数(dict)。

  7. 装饰器执行时机@decorator定义时执行一次,不是每次调用执行,别在里面放"每次调用才想做的事"。

  8. 没有函数提升:JS 有函数提升,Python 必须先定义后调用,顺序写反会 NameError

  9. 默认参数在定义时求值:如果默认值依赖运行时状态,会得到"过期"的值。

总结

  • def 定义函数,lambda 是弱化版箭头函数(单表达式)。

  • Python 参数系统更强:关键字参数、*args**kwargs 都是 JS 没有的利器。

  • 作用域遵循 LEGB 规则,修改外部变量需 global / nonlocal(与 JS 隐式捕获不同)。

  • 装饰器 = 高阶函数 + @ 语法糖,等价于 fn = decorator(fn)

  • 可变默认参数会被共享,永远用 None 哨兵模式替代 [] / {} 默认值。