首页/Python 与数据实践/函数设计与 lambda
Python 与数据实践 · 第 04 章

函数设计与 lambda

写过几章代码后你大概已经察觉:同一段计算逻辑,常常在好几个地方反复出现。函数就是解决这个问题的——把一段逻辑起个名字封装起来,哪里要用就喊一声名字。代码不再重复,读起来也更清爽

约 26 分钟10,204 字33 个代码示例

把一段逻辑装进"盒子"里

写过几章代码后你大概已经察觉:同一段计算逻辑,常常在好几个地方反复出现。函数就是解决这个问题的——把一段逻辑起个名字封装起来,哪里要用就喊一声名字。代码不再重复,读起来也更清爽。

用 def 关键字定义函数:

# 定义一个最简单的函数
def brew():
    print("咖啡冲好了,请慢用~")

# 调用它
brew()  # 咖啡冲好了,请慢用~

传参的五种方式

按位置对号入座

调用时实参按先后顺序一一对应到形参上:

def order_total(unit_price, count):
    return unit_price * count

pay = order_total(25, 4)
print(pay)  # 100

用名字指定:关键字参数

按参数名传值,谁先谁后无所谓:

def car_profile(brand, price, color):
    print(f"{color}的{brand},指导价 {price} 万")

car_profile(price=20, color="红色", brand="劲风")  # 红色的劲风,指导价 20 万

和位置参数混用时:位置参数必须排在关键字参数前面。

car_profile("劲风", price=20, color="红色")   # 正确
# car_profile(brand="劲风", 20, "红色")      # 不行!位置参数不能落在关键字参数后面

提前备好默认值

定义时给参数一个默认值,调用时可以不传:

def serve(kind, size="中杯", ice="少冰"):
    print(f"{size}{kind},{ice}")

serve("拿铁")           # 中杯拿铁,少冰
serve("美式", "大杯")   # 大杯美式,少冰

注意: 带默认值的参数必须排在没有默认值的参数后面。

# 这样写直接语法报错
# def serve(size="中杯", kind):  # SyntaxError!
#     pass

来者不拒:*args

不确定调用方会塞多少个位置参数时,用 *args 全盘接收:

def exam_total(*scores):
    total = 0
    for s in scores:
        total += s
    return total

print(exam_total(80, 92, 75))      # 247
print(exam_total(66, 71, 88, 90))  # 315
print(exam_total())                # 0

args 的真身: 在函数内部,它就是一个元组。

def peek_args(*scores):
    print(type(scores))  # <class 'tuple'>
    print(scores)        # (80, 92, 75)

peek_args(80, 92, 75)

一切收进字典:**kwargs

不确定会收到哪些关键字参数时,用 **kwargs 接住:

def show_laptop(**specs):
    for part, detail in specs.items():
        print(f"{part}: {detail}")

show_laptop(cpu="M3", ram="16G", disk="512G")
# cpu: M3
# ram: 16G
# disk: 512G

kwargs 的真身: 在函数内部,它就是一个字典。

def peek_kwargs(**specs):
    print(type(specs))  # <class 'dict'>
    print(specs)        # {'cpu': 'M3', 'ram': '16G'}

peek_kwargs(cpu="M3", ram="16G")

完整的参数排列顺序

定义函数时,各类参数必须按下面的次序摆放:

def func(位置参数, 默认参数, *args, **kwargs):
    pass

# 综合示例
def combo(a, b=2, *rest, **opts):
    print(f"a={a}, b={b}")
    print(f"rest={rest}")
    print(f"opts={opts}")

combo(1, 3, 4, 5, x=10, y=20)
# a=1, b=3
# rest=(4, 5)
# opts={'x': 10, 'y': 20}

返回值的三个细节

用 return 把结果递出去

def circle_area(r):
    return 3.14 * r ** 2

area = circle_area(3)
print(area)  # 28.26

一次返回多个值

Python 函数可以同时交出多个结果,本质上是打包成了一个元组:

def hi_lo(temps):
    return min(temps), max(temps)

low, high = hi_lo([18, 27, 33, 21])
print(low)   # 18
print(high)  # 33

# 只用一个变量接,接到的是整个元组
both = hi_lo([18, 27, 33])
print(both)        # (18, 33)
print(type(both))  # <class 'tuple'>

不写 return,返回的是 None

def beep():
    print("滴——")

noise = beep()  # 滴——
print(noise)    # None

给函数挂一块说明书(Docstring)

函数体开头放一个三引号字符串,就是它的说明文档:

def delivery_fee(distance, weight):
    """
    估算快递运费。

    参数:
        distance: 配送距离,单位公里
        weight: 包裹重量,单位千克

    返回:
        预计运费(元)
    """
    return 8 + distance * 0.5 + weight * 1.2

# 查看文档
print(delivery_fee.__doc__)

lambda:随手写一个"一次性"函数

lambda 表达式用来创建匿名函数——没有名字、写完就用、用完就扔。当某个逻辑特别简单、又只在一个地方用到时,它能省下不少笔墨。

语法长这样

lambda 参数1, 参数2, ... : 表达式
# 常规写法
def add_tax(price):
    return price * 1.13

# 等价的 lambda 写法
add_tax_l = lambda price: price * 1.13

print(add_tax(200))    # 226.0
print(add_tax_l(200))  # 226.0

三条规矩:

  • 冒号右边只能是一个表达式,写不了多条语句
  • 表达式的计算结果自动作为返回值,不用写 return
  • 一般不给它起名字,现场定义现场用

lambda 还是 def?一张表看懂

对比项 lambda 普通函数(def)
名字 匿名,通常不命名 必须有函数名
函数体 仅限一个表达式 可以写任意多条语句
返回值 表达式结果自动返回 需要显式写 return
适合场景 简单、一次性 复杂、要反复调用

选择原则: 一句话能说清、用完就扔 → 用 lambda;步骤多、或者多处要复用 → 老老实实 def。


高阶函数:把函数当参数传来传去

能接收函数作为参数的函数,叫高阶函数——这正是 lambda 大显身手的舞台。

map() —— 批量加工

对可迭代对象里的每个元素套用同一个操作,返回一个迭代器:

prices = [30, 45, 128, 66]

# 先定义再传入,可行但啰嗦
def half(p):
    return p / 2

result = map(half, prices)
print(list(result))  # [15.0, 22.5, 64.0, 33.0]

# 配合 lambda,一行到位
result = map(lambda p: p / 2, prices)
print(list(result))  # [15.0, 22.5, 64.0, 33.0]

map() 的语义: 给每个元素做一次"转换"。

# 歌名列表 → 歌名长度列表
songs = ["稻香", "晴天", "七里香"]
lens = map(lambda s: len(s), songs)
print(list(lens))  # [2, 2, 3]

# 两列成绩逐位相加
chinese = [86, 92, 78]
math = [90, 85, 88]
totals = map(lambda c, m: c + m, chinese, math)
print(list(totals))  # [176, 177, 166]

filter() —— 按条件筛

只留下让条件成立的那些元素:

scores = [59, 61, 88, 45, 73, 100]

# 留下及格的
passed = filter(lambda s: s >= 60, scores)
print(list(passed))  # [61, 88, 73, 100]

# 留下长度超过 2 的单词
words = ["ok", "filter", "go", "lambda"]
long_ones = filter(lambda w: len(w) > 2, words)
print(list(long_ones))  # ['filter', 'lambda']

filter() 的语义: 给元素做一次"体检",达标的留下。

# 留下正数
changes = [-3, 5, -1, 0, 2]
ups = filter(lambda x: x > 0, changes)
print(list(ups))  # [5, 2]

sorted() 的 key —— 自定义排序依据

sorted() 和列表的 .sort() 都接受 key 参数,用来回答"按什么排":

cities = ["Chengdu", "Beijing", "Shanghai", "Xiamen"]

# 按长度排(短的在前)
by_len = sorted(cities, key=lambda s: len(s))
print(by_len)  # ['Xiamen', 'Chengdu', 'Beijing', 'Shanghai']

# 按末尾字母排
by_tail = sorted(cities, key=lambda s: s[-1])
print(by_tail)  # ['Beijing', 'Shanghai', 'Xiamen', 'Chengdu']

# 按长度降序
by_len_desc = sorted(cities, key=lambda s: len(s), reverse=True)
print(by_len_desc)  # ['Shanghai', 'Chengdu', 'Beijing', 'Xiamen']
# 按绝对值排,不看正负
swings = [-120, 85, -60, 210, -15]
by_abs = sorted(swings, key=lambda x: abs(x))
print(by_abs)  # [-15, -60, 85, -120, 210]

max() / min() 的 key —— 按规则找冠军

cities = ["Chengdu", "Beijing", "Shanghai", "Xiamen"]

# 最长的城市名
print(max(cities, key=lambda s: len(s)))  # Shanghai

# 最短的城市名
print(min(cities, key=lambda s: len(s)))  # Xiamen
players = [
    {"nick": "阿风", "kills": 12},
    {"nick": "小雨", "kills": 9},
    {"nick": "大山", "kills": 15},
]

# 击杀数最高的选手
mvp = max(players, key=lambda p: p["kills"])
print(mvp)  # {'nick': '大山', 'kills': 15}

reduce() —— 一路折叠成一个值

reduce() 藏在 functools 模块里,作用是把序列反复"两两合并",最终压成一个值:

from functools import reduce

fees = [12, 35, 8, 60]

# 累加求和
total = reduce(lambda a, b: a + b, fees)
print(total)  # 115

# 连乘
product = reduce(lambda a, b: a * b, [1, 2, 3, 4, 5])
print(product)  # 120

# 手工版求最大值
peak = reduce(lambda a, b: a if a > b else b, fees)
print(peak)  # 60

练习

练习一至练习四聚焦函数的定义、参数传递与返回值;练习五综合运用 lambda 表达式与内置高阶函数。

练习一:默认参数、*args 与列表联动

逐行阅读下面的代码,先在纸上写出每个 print 的输出,再运行验证。

def add_tasks(first, tasks=None, *others):
    if tasks is None:
        tasks = []
    tasks.append(first)
    for item in others:
        tasks.append(item)
    return tasks

list1 = add_tasks("开晨会")
print(list1)

list2 = add_tasks("写周报", ["复盘", "面试"], "报销", "团建")
print(list2)

print(list1)

练习二:**kwargs 与字典联动

逐行阅读下面的代码,先在纸上写出每个 print 的输出,再运行验证。

def restock(shelf, **incoming):
    result = shelf.copy()  # 浅拷贝
    for name, value in incoming.items():
        if name in result:
            result[name] += value
        else:
            result[name] = value
    return result

shelf = {"pen": 10, "notebook": [3, 4]}
filled = restock(shelf, pen=6, notebook=[7], ruler="缺货")
print(filled)

print(len(filled))
print(filled.get("eraser", "没找到"))

练习三:多返回值与容器遍历联动

逐行阅读下面的代码,先在纸上写出每个 print 的输出,再运行验证。

def split_groups(members):
    mid = len(members) // 2
    return members[:mid], members[mid:]

squad = ["张三", "李四", "王五", "赵六", "钱七", "孙八"]
group_a, group_b = split_groups(squad)
print(group_a, group_b)

def locate_rounds(log, target):
    positions = []
    for i, val in enumerate(log):
        if val == target:
            positions.append(i)
    return positions

rounds = [90, 88, 90, 75, 90]
hits = locate_rounds(rounds, 90)
print(hits)

combined = group_b + [len(hits)]
print(combined)

练习四:函数综合编程

按要求实现下面三个函数,并自行编写测试代码逐个调用,确认功能正确。

任务 1:展开嵌套清单

实现函数 unfold_layers,接收一个可能多层嵌套的列表(如 [7, [8, 9], [[10], 11]]),把所有元素摊平成一维列表返回(如 [7, 8, 9, 10, 11])。

nested1 = [7, [8, 9], [[10], 11]]
print(unfold_layers(nested1))  # [7, 8, 9, 10, 11]

nested2 = [[[7]], 8, [9, [10, [11]]]]
print(unfold_layers(nested2))  # [7, 8, 9, 10, 11]

print(unfold_layers([]))         # []
print(unfold_layers([7, 8, 9]))  # [7, 8, 9]

任务 2:序列变链式结构

实现函数 build_chain,接收一个列表或元组,把它转换成链式嵌套字典返回。

chain1 = build_chain((3, 6, 9))
print(chain1)
# {'value': 3, 'next': {'value': 6, 'next': {'value': 9, 'next': None}}}

chain2 = build_chain(["甲", "乙"])
print(chain2)
# {'value': '甲', 'next': {'value': '乙', 'next': None}}

print(build_chain([]))  # None

任务 3:合并任意多个字典

实现函数 combine_settings,接收数量不定的字典参数,把它们合并成一个大字典。

合并规则:取并集;若键撞车,后传入的值覆盖先传入的值(浅合并即可)。

c1 = {"volume": 30, "subtitle": ["简", "英"]}
c2 = {"subtitle": ["繁"], "quality": "1080p"}
c3 = {"volume": 55, "autoplay": False}

merged = combine_settings(c1, c2, c3)
print(merged)
# {'volume': 55, 'subtitle': ['繁'], 'quality': '1080p', 'autoplay': False}

print(combine_settings(c1))
# {'volume': 30, 'subtitle': ['简', '英']}

print(combine_settings())
# {}

练习五:lambda 与高阶函数综合实战

基于下面这份超市商品数据完成练习。

goods = [
    {"title": "红富士苹果", "brand": "绿洲农场", "price": 12.9, "stock": 300},
    {"title": "赣南脐橙", "brand": "绿洲农场", "price": 9.9, "stock": 450},
    {"title": "海南香蕉", "brand": "绿洲农场", "price": 6.5, "stock": 800},
    {"title": "阳光玫瑰葡萄", "brand": "绿洲农场", "price": 29.9, "stock": 150},
    {"title": "库尔勒香梨", "brand": "绿洲农场", "price": 11.5, "stock": 260},
    {"title": "沙瓤西瓜", "brand": "绿洲农场", "price": 15.8, "stock": 120},
    {"title": "樱桃小番茄", "brand": "绿洲农场", "price": 13.9, "stock": 240},
    {"title": "盒装蓝莓", "brand": "绿洲农场", "price": 39.9, "stock": 80},
    {"title": "水仙芒", "brand": "绿洲农场", "price": 14.8, "stock": 230},
    {"title": "东北大米", "brand": "金穗粮油", "price": 59, "stock": 200},
    {"title": "有机小米", "brand": "金穗粮油", "price": 15, "stock": 180},
    {"title": "压榨花生油", "brand": "金穗粮油", "price": 89, "stock": 90},
    {"title": "高筋面粉", "brand": "金穗粮油", "price": 22, "stock": 160},
    {"title": "荞麦挂面", "brand": "金穗粮油", "price": 18, "stock": 210},
    {"title": "即食燕麦片", "brand": "金穗粮油", "price": 21, "stock": 190},
    {"title": "厄瓜多尔白虾", "brand": "蓝海水产", "price": 49.9, "stock": 85},
    {"title": "鲜活鲈鱼", "brand": "蓝海水产", "price": 25, "stock": 60},
    {"title": "带鱼段", "brand": "蓝海水产", "price": 19.9, "stock": 140},
    {"title": "冷冻扇贝肉", "brand": "蓝海水产", "price": 35, "stock": 75},
    {"title": "头水紫菜", "brand": "蓝海水产", "price": 8.5, "stock": 300},
    {"title": "每日鲜牛奶", "brand": "晨光乳业", "price": 13.9, "stock": 400},
    {"title": "老酸奶", "brand": "晨光乳业", "price": 8.5, "stock": 350},
    {"title": "动物黄油", "brand": "晨光乳业", "price": 32, "stock": 110},
    {"title": "芝士片", "brand": "晨光乳业", "price": 26, "stock": 150},
    {"title": "纯羊奶", "brand": "晨光乳业", "price": 19.9, "stock": 90},
    {"title": "高原水牛奶", "brand": "晨光乳业", "price": 15.9, "stock": 130},
    {"title": "原味瓜子", "brand": "久久零食", "price": 11, "stock": 260},
    {"title": "琥珀核桃仁", "brand": "久久零食", "price": 25, "stock": 130},
    {"title": "芒果干", "brand": "久久零食", "price": 16.9, "stock": 170},
    {"title": "香辣豆皮", "brand": "久久零食", "price": 5.5, "stock": 600},
    {"title": "蜜汁猪肉脯", "brand": "久久零食", "price": 28, "stock": 95},
    {"title": "海苔卷", "brand": "久久零食", "price": 9.9, "stock": 280},
    {"title": "开口板栗仁", "brand": "久久零食", "price": 19, "stock": 145},
    {"title": "天然矿泉水", "brand": "山泉饮品", "price": 24, "stock": 1000},
    {"title": "白桃气泡水", "brand": "山泉饮品", "price": 45, "stock": 300},
    {"title": "鲜榨橙汁", "brand": "山泉饮品", "price": 12, "stock": 220},
    {"title": "冷萃咖啡液", "brand": "山泉饮品", "price": 39, "stock": 130},
    {"title": "蜂蜜柚子茶", "brand": "山泉饮品", "price": 29, "stock": 160},
    {"title": "椰子水", "brand": "山泉饮品", "price": 10.9, "stock": 260},
    {"title": "柠檬茶", "brand": "山泉饮品", "price": 8.8, "stock": 320},
]

充分运用本章学过的 lambda 表达式与内置高阶函数,完成下面的任务:

  1. 按单价从低到高排序

  2. 按单价从高到低排序

  3. 按库存金额从低到高排序(库存金额 = 单价 × 库存数量)

  4. 找出"绿洲农场"的所有商品,得到一个字符串列表

  5. 找出单价最高的商品所属的品牌列表(字符串列表)

  6. 得到每个品牌的平均单价 结果示例:

[
    {"brand": "山泉饮品", "avg_price": 24.1},
    {"brand": "绿洲农场", "avg_price": 17.233333333333334},
    # ……其余品牌以此类推
]

下一章:05 · 作用域规则与闭包