去做题

第二单元:参数传递 · 给函数递东西

Unit 2: Passing Parameters — Feeding Data to Functions
计算机科学 5 · 安息云峰 Cloudrip Mountain · 猎手和猎物 Hunters & Prey

2.1 为什么要参数

Why Parameters

上一单元你学会了用 def 把行为封装成函数,比如 commandSoldiers() 一次遍历所有士兵下令。但那样的函数有个局限:它内部"要处理谁"是写死的。本单元你将给函数装上一个"入口"——参数(parameter),让同一个函数每次可以处理不同的对象。这样你就能写一个 commandSoldier(soldier),把任意一名士兵"递"给它去指挥。

本单元主线:参数就是函数的"输入口"。你将学会在 def commandSoldier(soldier): 的括号里声明形参、在调用 commandSoldier(friend) 时把实参传进去、理解参数在函数内部就像一个局部变量、并用这套机制写出两个各带参数的函数——commandSoldier 和 commandArcher,分别指挥士兵和弓箭手。

2.1.1 从"固定"到"通用"(From Fixed to Flexible)

2.1.1.1 让一个函数服务无数对象 One function, many objects

没有参数的函数只能做一件固定的事;有了参数,同一个函数就能被"喂"进不同的数据,做同一类事情。关卡默认代码的注释点明了这个思想:"这个函数有一个名为 soldier 的参数。参数就像变量一样。参数的值是在调用函数时确定的。"换句话说,commandSoldier 不再"内定"要指挥谁,而是等你调用时告诉它"这次指挥这一个"。

无参数:处理谁写死了
def commandSoldiers():
    for s in hero.findFriends():
        # 内部自己找对象
        pass
有参数:处理谁由外部决定
def commandSoldier(soldier):
    # soldier 由调用者传进来
    enemy = soldier.findNearestEnemy()
    # ...

2.1.2 形参与实参(Parameter vs. Argument)

2.1.2.1 一个是"占位名",一个是"真东西" A placeholder name vs. a real value

有两个词要分清:定义时括号里写的那个名字叫形参(形式参数,只是个占位的名字,如 soldier);调用时括号里真正传进去的值叫实参(实际参数,如 friend)。调用发生的一瞬间,实参的值会被"装进"形参这个名字里,函数体就拿着它开始工作。

📮 形参是信箱,实参是信
形参 soldier:函数门口挂着的一个空信箱,写着名字"soldier"。
实参 friend:调用 commandSoldier(friend) 时,把 friend 这封"信"投进信箱。
函数体:打开信箱取出内容,此后用 soldier 指代的就是这次投进来的那位士兵。

2.2 带参数的函数

Functions with Parameters

2.2.1 括号里的形参(Declaring a Parameter)

2.2.1.1 在 def 的括号里写下名字 Put a name in the parentheses

上一单元函数的括号总是空的 ()def commandSoldier(soldier): 就声明了这个函数需要"一个东西",并在函数内部用 soldier 这个名字来称呼它。这个名字是你自己起的,起得贴切能让代码更好读。

带参数的函数头部 def commandSoldier ( soldier ) : 函数名 形参(占位名)

2.2.2 参数是局部变量(Parameters Are Local Variables)

2.2.2.1 在函数里,形参就像普通变量 Inside, it behaves like any variable

关卡说"参数就像变量一样"——这话很关键。一旦进入函数体,形参 soldier 就和你平时写的普通变量没有区别:你可以 soldier.findNearestEnemy()、可以 soldier.pos、可以拿它去 command。它是一个局部变量——只在这个函数内部有效,出了函数就"不存在"了。

def commandSoldier(soldier):
    # 在函数里,soldier 就是个普通变量,可自由使用
    enemy = soldier.findNearestEnemy()
    if enemy:
        hero.command(soldier, "attack", enemy)
"局部"是什么意思?形参 soldier 只在 commandSoldier 这个函数内部有意义。你没法在函数外面用它,别的函数也看不见它。每次调用函数,都会新建一个属于这次调用的 soldier,用完即弃。这种"各管各的、互不干扰"的特性,正是函数好用、不易出错的原因之一。

2.2.3 实参传递机制(How Arguments Get Passed)

2.2.3.1 调用一瞬间完成"赋值" The value is bound at call time

参数传递的本质,就是在调用的那一刻把实参"赋值"给形参。关卡默认代码里有一句提示注释:"将这位朋友赋值给 commandSoldier 函数中的 soldier 变量"——对应的调用是 commandSoldier(friend)。执行这行时,等价于在函数内部悄悄做了 soldier = friend,然后才开始跑函数体。

调用瞬间:实参 friend → 形参 soldier commandSoldier(friend) 调用处:传入实参 friend 赋值 soldier = friend def ...(soldier): 函数内:soldier 已装好值
名字可以不一样:调用时传的实参叫 friend,函数里的形参叫 soldier——两个名字不同完全没问题。重要的是"值"的传递,不是名字的匹配。函数只关心"你递给我一个单位对象",至于它在外面叫什么,函数并不在乎。

2.3 分派不同单位

Dispatching Different Units

2.3.1 按 type 判断分流(Dispatch by Type)

2.3.1.1 遍历盟友,按兵种送进不同函数 Route each friend to the right function

本关的部队里既有士兵又有弓箭手,两者战术不同,需要分别指挥。做法是遍历所有盟友,用 friend.type 判断它是哪种兵,再把它"递"给对应的函数:士兵送进 commandSoldier(friend),弓箭手送进 commandArcher(friend)。这就是"按类型分派"——同一个 friend,根据身份走向不同的处理函数。

for friend in hero.findFriends():
    if friend.type == "soldier":
        commandSoldier(friend)    # 把这名士兵递进去
    elif friend.type == "archer":
        commandArcher(friend)     # 把这名弓箭手递进去
同一个变量,去向由类型决定:循环里的 friend 每轮都是不同的单位。if/elif 就像一个岔路口的分拣员:看一眼 friend.type,是士兵就走左边这条路进 commandSoldier,是弓箭手就走右边进 commandArcher。参数机制让"把当前这个单位交给某函数"变得非常自然。

2.3.2 士兵与弓箭手分工(Soldiers vs. Archers)

2.3.2.1 两种兵,两套战术 Two unit types, two tactics

为什么要拆成两个函数?因为士兵和弓箭手的最优打法不同。关卡规定得很清楚:"弓箭手应该只攻击距离小于 25 米的敌人。士兵应该攻击任何敌人。"士兵是近战肉盾,见敌就冲;弓箭手是脆皮远程,必须克制——只打射程内的敌人,否则原地待命,绝不主动追出去。

🛡️ commandSoldier

士兵勇往直前:findNearestEnemy() 找到任何敌人就 command(soldier, "attack", enemy),冲上去打。

🏹 commandArcher

弓箭手克制作战:只在 distanceTo(enemy) < 25 时攻击,否则 command(archer, "move", archer.pos) 原地待命,守住驯鹿。

为什么弓箭手要"原地待命"?关卡概览说明:弓箭手的默认行为是"上前追赶敌人",一旦追出去离驯鹿太远,食人魔就会趁机从山丘伏击驯鹿。所以要主动下达 command(archer, "move", archer.pos)——命令它"待在自己当前位置不动",用射程守株待兔,敌人进 25 米才放箭。

2.4 实战关卡精讲

Level Strategy & Analysis

这一节聚焦关卡的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。

关卡 · 猎手和猎物

🦌 猎手和猎物 —— 士兵冲锋,弓箭手守猎物

扣哒世界 猎手和猎物关卡 - koudashijie.com
图2:猎手和猎物——士兵前压清剿食人魔,弓箭手贴身守护驯鹿,只射入射程的敌人

策略:进攻与护卫分层

食人魔在猎捕你的驯鹿,你要反过来"猎猎手"。核心策略是分层布防:让士兵作为前排主动出击、把食人魔顶在外围;让弓箭手作为后排贴着驯鹿、当敌人闯进射程时才补刀。关卡简介一句话概括——"在召唤士兵进攻的同时,让弓箭手在士兵背后"。攻守分层,才能既清敌又不丢猎物。

思路拆解

算法结构是"若干带参数的函数 + 一个分派循环"。先补全两个无参函数 pickUpCoin(找最近金币就 move 过去)和 summonTroops(钱够就 summon("soldier"))。再写两个带参数的指挥函数:commandSoldier(soldier) 里 soldier.findNearestEnemy() 找到敌人就攻击;commandArcher(archer) 里 archer.findNearestEnemy() 后加一层距离判断 archer.distanceTo(enemy) < 25,够近才攻击、否则 command(archer, "move", archer.pos) 原地待命。最后在 while True 里遍历 findFriends(),按 friend.type 把每个单位分派给对应函数。

# 关键片段:弓箭手——够近才射,否则守在原地
def commandArcher(archer):
    enemy = archer.findNearestEnemy()
    if enemy and archer.distanceTo(enemy) < 25:
        hero.command(archer, "attack", enemy)
    else:
        hero.command(archer, "move", archer.pos)   # 待在原地

算法要点回顾

这一关把参数传递用得淋漓尽致:两个指挥函数各带一个形参,分派循环把不同兵种的单位当作实参传进去。妙在 commandSoldier 和 commandArcher 结构几乎一样(都是"找敌人→判断→下令"),只是判断条件不同——这说明参数化的函数让"同类行为、不同细节"的代码得以清晰地各自封装,主循环则只负责"谁该进哪个函数"。

弓箭手别乱跑:如果忘了写 else 分支里的 command(archer, "move", archer.pos),弓箭手就会按默认行为主动追敌、脱离驯鹿,导致驯鹿被伏击。"没敌人/敌人太远时,明确命令弓箭手原地不动"是本关成败的关键。
distanceTo 与射程 25:archer.distanceTo(enemy) 算的是弓箭手到敌人的距离,而非英雄到敌人。因为判断"敌人是否进入这名弓箭手的射程",必须站在弓箭手自己的位置上量。这也再次体现了参数的价值——函数内用 archer 这个形参,就能针对"当前这名弓箭手"精确计算。

2.5 常见错误

Common Mistakes

2.5.1 新手最容易踩的坑(Pitfalls for Beginners)

2.5.1.1 参数传递的典型问题 Typical issues with parameters

扣哒世界 参数传递常见错误速查 - koudashijie.com
图3:调用漏传参、形参实参混淆、弓箭手不写待命分支,是本单元最常见的失误
易错点清单:
  • 调用漏传参——函数定义带了形参却调用成 commandSoldier(),缺少实参会报错;应写 commandSoldier(friend)。
  • 定义忘写形参——def commandSoldier(): 括号空着,函数里却用 soldier,会因变量未定义而出错。
  • 函数里用错名字——形参叫 soldier,函数体里却写 friend,两者对不上;函数内应统一用形参名。
  • 弓箭手漏写 move 待命——没有 else 分支,弓箭手默认追敌脱离驯鹿,导致猎物被伏击。
  • 距离判断写反——想"够近才射"却写成 distanceTo(enemy) > 25,结果只打远处、近处不打。
  • type 判断拼错——把 "archer" 写错或大小写不符,弓箭手永远进不了对应分支。
  • distanceTo 用错主体——写成 hero.distanceTo(enemy),量的是英雄到敌人,而非弓箭手到敌人。
  • 攻击传错目标——"attack" 第三参数应是敌人对象,"move" 才传坐标/位置。

本单元小结 ✨

恭喜你给函数装上了"输入口"!这一单元你理解了参数的威力:在 def commandSoldier(soldier): 的括号里声明形参,让同一个函数能处理不同对象;分清了形参(占位名)与实参(真值),明白调用 commandSoldier(friend) 的一瞬间等价于把 friend 赋值给 soldier;也知道了参数在函数内部就是一个局部变量,可以像普通变量一样使用。最后你用参数化的 commandSoldier 和 commandArcher,配合按 type 分派的循环,让士兵冲锋、弓箭手守猎物,打赢了"猎手和猎物"。来回顾一下核心概念:

核心概念关键点对应关卡
声明形参def 名字(形参):猎手和猎物
形参 vs 实参占位名 vs 传入的真值猎手和猎物
参数传递机制调用瞬间 形参 = 实参猎手和猎物
参数是局部变量只在函数内有效,可自由使用猎手和猎物
同函数处理不同对象每次调用喂进不同实参猎手和猎物
按 type 分派if type=="soldier" 走不同函数猎手和猎物
士兵战术见敌就攻击猎手和猎物
弓箭手战术近才射,否则原地待命猎手和猎物

懂得给函数递上不同的"输入"、让一个函数灵活服务千军万马的英雄,才是云峰之上真正老练的程序员。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️🦌🏹

扣哒世界 编程知识 · 计算机科学 5(安息云峰):第二单元

扣哒世界教研团队