第六单元:函数的定义与调用

Unit 6: Defining and Calling Functions
边际森林 Border Forest · 乡村巡游者 → 寂静市

6.1 为什么需要函数

Why We Need Functions

6.1.1 重复劳动的烦恼(The Pain of Repetition)

6.1.1.1 巡逻这件"无聊事" Patrolling is boring repetition

在"乡村巡游者"关卡里,英雄要在村庄的几个入口之间来回巡逻、清剿敌人。关卡一开始就点出了痛点:"在村庄中巡逻是很无聊的重复工作。" 每到一个巡逻点,你都要重复写"找敌人 → 判断 → 攻击"这同样的几行代码——点越多,重复得越多,代码又长又乱。

有没有办法只写一次这套逻辑,然后到处反复使用?有!这就是本单元的主角:函数(Function)。关卡说得很直白:"使用函数可以让重复工作轻松许多。你可以将很多行代码放到一行里!"

6.1.2 函数的三大好处(Three Benefits of Functions)

6.1.2.1 浓缩、复用、易读 Condense, reuse, readable

函数为什么这么重要?因为它一次性带来了三个好处:

好处含义带来的改变
浓缩代码把很多行代码压缩成一行调用主循环变得短小清爽
避免重复同一段逻辑只写一次,到处复用不用反复键入相同代码
提升可读性用有意义的名字代表一整套逻辑复杂逻辑变得一目了然

关卡里有一句很形象的话:"这是函数的第一大好处:它们能将大量代码浓缩到一行。" 想象一下,如果每次挥剑都要写 25 行代码,那将是怎样的灾难!

6.1.3 你早就在用函数(You've Been Using Functions)

6.1.3.1 attack 其实就是一个函数 attack is already a function

其实,你一直都在用函数,只是没意识到。关卡揭晓了真相:当你写 hero.attack(enemy) 时,"这是在调用(calling 或者说 invoking)一个函数,名为 attack。"

💡 attack 背后藏着多少代码?

调用 attack 时实际运行的代码"很长很复杂"。它要判断:"我有武器吗?我离得够近吗?挥一次要多久?我该打吗?造成多少伤害?"——所有这些细节,全被打包浓缩进了一个简单的词:attack。这正是函数的魔力:把复杂藏起来,只露出一个好用的名字。

本单元最大的跃迁是:你将从"只会调用别人写好的函数",升级到"定义自己的函数"

6.2 什么是函数

What is a Function

6.2.1 函数的定义(Definition)

6.2.1.1 一段可以重复使用的代码 A reusable block of code

CodeCombat 函数示意:把一组动作打包起来 - codecombat.cn
图1:函数就是把一组动作打包、贴上名字,之后随时一键调用

函数(Function)一段被命名、可以重复使用的代码。你把若干行代码"打包"在一起,给这个包起个名字;以后任何时候想运行这段代码,只要喊一声这个名字就行。关卡说:"函数是代码的重要组成部分。"

6.2.2 函数名与函数体(Name and Body)

6.2.2.1 函数由两部分组成 A function has two parts

关卡明确指出:"定义一个函数,它含有两部分:函数名和函数体。"

组成部分英文作用
函数名Function Name函数的名称,以后用它来调用,例如 attack
函数体Function Body调用函数时实际执行的那些代码

有时函数还会带参数(Parameter)——比如 attack(enemy) 括号里的 enemy。不过本单元先专注于不带参数的函数,参数留到以后的单元再深入。

6.2.3 黑箱比喻(The Black Box)

6.2.3.1 像一台贴了标签的机器 Like a labeled machine

💡 函数 = 贴了标签的机器

把函数想象成一台机器:机器外面贴着名字(函数名),机器内部装着一整套零件和工序(函数体)。你不需要知道内部怎么运转,只要按下写着名字的按钮,机器就会乖乖完成整套工作。findAndAttackEnemy() 就是这样一台"找敌人并攻击"的机器——按一下,它就把找敌人、判断、攻击全做完。

6.3 定义函数

Defining a Function

6.3.1 def 关键字(The def Keyword)

6.3.1.1 用 def 开启一个定义 Start a definition with def

在 Python 里,定义函数要用关键字 def(define 的缩写,意为"定义")。基本格式是:

def 函数名():
    # 函数体:调用时要执行的代码(缩进)
    要做的事情

它由四个部分组成:关键字 def、一个函数名、一对圆括号 ()冒号 :,以及下面缩进的函数体。是不是有点眼熟?它的"冒号 + 缩进"骨架和 ifwhile 一脉相承。

6.3.2 语法解剖(Anatomy)

6.3.2.1 一行行看清楚每个部件 Examine each part

def findAndAttackEnemy ( ) : 关键字 def "我要定义" 函数名 以后用它来调用 括号 () 放参数的地方 冒号 : 下面要开始函数体了

以"乡村巡游者"里的函数为例,把"找敌人并攻击"这套逻辑完整地装进函数体:

# 这定义了 findAndAttackEnemy 函数
def findAndAttackEnemy():
    enemy = hero.findNearestEnemy()
    if enemy:
        hero.attack(enemy)
注意:定义函数只是把代码"存"起来,并不会立刻运行里面的内容。就像写好一份菜谱,菜谱本身不会自动做出菜来——要等你照着做(调用)时才会执行。

6.3.3 命名规则(Naming)

6.3.3.1 取个好名字,代码会"说话" Good names make code self-explaining

函数名可以自己起,但好名字能让代码自我解释。看本单元这些名字就知道它们各自负责什么:findAndAttackEnemy(找到并攻击)、findAndCleaveEnemy(找到并劈斩)、cleaveOrAttack(劈斩或攻击)、pickUpCoin(捡硬币)、checkAndAttack(检查并攻击)。

命名建议:动词 + 名词描述函数"做什么事",比如 attackEnemy。常用驼峰命名法(第一个词小写,后面每个词首字母大写)。名字里不能有空格,也不能用数字开头。

6.4 调用函数

Calling a Function

6.4.1 如何调用(How to Call)

6.4.1.1 写下名字加括号 Write the name with parentheses

定义好函数后,调用(call / invoke)它非常简单——写下函数名,后面跟一对圆括号即可。每写一次,函数体里的代码就完整执行一遍。

while True:
    hero.moveXY(35, 34)      # 走到第一个巡逻点
    findAndAttackEnemy()     # 调用!一行顶好几行

    hero.moveXY(60, 31)      # 走到右侧入口
    findAndAttackEnemy()     # 再调用一次,逻辑复用
核心威力:同一套"找敌人并攻击"的逻辑只写了一次(在 def 里),却能在巡逻路上反复调用。主循环瞬间变得清爽好读——这正是函数浓缩与复用的价值。

6.4.2 定义 ≠ 调用(Define vs. Call)

6.4.2.1 写菜谱和照着做是两回事 Writing a recipe vs. cooking it

这是新手最容易绕晕的地方:定义函数和调用函数是两个完全不同的动作。 关卡反复强调:"你需要编写函数的定义,这样你就可以在主循环中调用它了。"

动作写法发生了什么
定义def findAndAttackEnemy(): + 函数体把代码存起来,暂不执行
调用findAndAttackEnemy()立即执行函数体里的代码
常见误区:只写了 def 定义却忘了在循环里调用——函数永远不会运行,英雄会原地发呆。比如"造币厂"关卡就提醒你:写好 attackEnemy 函数后,记得取消 attackEnemy() 那行的注释,否则它不会生效。

6.4.3 调用时不要加 hero(No hero Prefix)

6.4.3.1 自己定义的函数,直接喊名字 Your own functions need no hero.

关卡特别强调了一个关键区别:"调用函数时不要加 hero,因为函数由你定义,而不是英雄。"

hero.attack(enemy)        # attack 是英雄自带的 → 要加 hero.
findAndAttackEnemy()     # 这是你定义的 → 不加 hero,直接喊名字
怎么区分?attackmoveXYcleave 这些是英雄的本领,调用要写 hero. 开头;而 findAndAttackEnemy 这种你自己 def 出来的函数,是"全局"的,直接写名字加括号即可,不带 hero.

6.5 实战关卡精讲

Level Strategy & Analysis

这一节聚焦每一关如何用函数解决问题。看懂"为什么这样封装",你就能自己写出完整解法。

6.5.1 乡村巡游者(Village Rover)

6.5.1.1 思路:把"找+打"封装成一个函数 Strategy: wrap find+attack into one function

CodeCombat 乡村巡游者关卡 - codecombat.cn
图2:乡村巡游者——定义一个巡逻函数,在各入口反复调用

这是你第一次定义自己的函数。地图上有几个固定巡逻点,每个点都要做同样的"找敌人并攻击"。难点不在逻辑,而在于意识到"该把重复的部分抽出来"。策略就是把这套逻辑装进 findAndAttackEnemy(),然后在主循环里"移动 → 调用"地反复使用。

def findAndAttackEnemy():   # ① 先定义
    enemy = hero.findNearestEnemy()
    if enemy:
        hero.attack(enemy)

while True:                  # ② 再在循环里调用
    hero.moveXY(35, 34)
    findAndAttackEnemy()
    # 到右侧入口再调一次……
注意顺序:def 定义要写在调用之前(通常放在文件顶部)。函数定义部分不在 while 循环里——它独立存在,循环里只负责"调用"。

6.5.2 村庄守卫(Village Guard)

6.5.2.1 思路:定义多个函数,各司其职 Strategy: define multiple functions

CodeCombat 村庄守卫关卡 - codecombat.cn
图3:村庄守卫——同时定义"攻击"和"劈斩"两个函数,配合御敌

敌人成群来袭,单靠普通攻击不够了,需要再加一个使用 cleave 的函数。这一关教你同时定义并使用多个函数:findAndAttackEnemy 负责普通攻击,findAndCleaveEnemy 负责劈斩。后者里要做两层检查——敌人存在,且 cleave 就绪。

def findAndCleaveEnemy():
    enemy = hero.findNearestEnemy()
    if enemy:                       # 检查 1:敌人存在
        if hero.isReady('cleave'):  # 检查 2:劈斩就绪
            hero.cleave(enemy)
封装的好处显现:主循环里只需在每个巡逻点依次调用 findAndCleaveEnemy()findAndAttackEnemy(),逻辑清晰得像"读说明书"。这就是把复杂藏进函数、让主流程保持简洁的威力。

6.5.3 村庄守护神(Village Champion)

6.5.3.1 思路:在函数里融合嵌套 if/else Strategy: combine nesting inside a function

CodeCombat 村庄守护神关卡 - codecombat.cn
图4:村庄守护神——把上一单元的嵌套判断,整个封装进一个函数

这一关让你"只能靠自己"定义函数,而且要把第五单元学的嵌套 if/else 融进函数体。思路是写一个 cleaveOrAttack() 函数:有敌人时,如果 cleave 就绪就劈斩,否则就普通攻击——一个函数把"该用哪种攻击"的决策全部包办。

def cleaveOrAttack():
    enemy = hero.findNearestEnemy()
    if enemy:
        if hero.isReady('cleave'):
            hero.cleave(enemy)
        else:                       # 劈斩没好 → 普通攻击
            hero.attack(enemy)
能力叠加:函数 + 嵌套 if/else,两个单元的本领合体了。把复杂的决策逻辑封进函数后,主循环只剩"移动 + 调用 cleaveOrAttack()",无论有几个巡逻点都一样简洁。

6.5.4 造币厂(The Mint)

6.5.4.1 思路:照着示例,从零写一个函数 Strategy: write a function from scratch

CodeCombat 造币厂关卡 - codecombat.cn
图5:造币厂——既要捡硬币,又要在差役偷走前干掉它们

这一关要你"从头开始创建一个函数"。关卡给了 pickUpCoin(捡硬币)作示范,要你照葫芦画瓢,自己写一个 attackEnemy(攻击敌人)。pickUpCoin 里还用到了上一单元学的 pos:找到硬币后移动到它的 coin.pos.x, coin.pos.y 去捡。

def pickUpCoin():             # 关卡给的示例
    coin = hero.findNearestItem()
    if coin:
        hero.moveXY(coin.pos.x, coin.pos.y)

def attackEnemy():            # 照着写的新函数
    enemy = hero.findNearestEnemy()
    if enemy:
        hero.attack(enemy)
别忘了这一步:主循环里要同时调用两个函数——先 attackEnemy() 清场,再 pickUpCoin() 捡钱。记得把示例里 attackEnemy() 那行的注释取消掉,否则函数白定义了!

6.5.5 寂静市(概念挑战 · Silent City)

6.5.5.1 思路:把全部逻辑塞进一个函数 Strategy: put all logic in one function

这是一个概念挑战,要求很巧妙:必须定义一个函数,并且 while True 循环里只能有一条语句。 这等于强制你把"看到敌人就打、否则回中心"的全部逻辑都封装进一个函数,让主循环干净到只剩一行调用。

def checkAndAttack():
    enemy = hero.findNearestEnemy()
    if enemy:
        hero.attack(enemy)
    else:
        hero.moveXY(30, 26)   # 没敌人 → 回中心标记

while True:
    checkAndAttack()             # 循环里只有这一条语句!
融会贯通:循环里只剩一行,逻辑却毫不缺失——全藏进了函数。这道题完美展示了函数的终极意义:把复杂封装起来,让主程序读起来像一句简单的话。

6.6 函数的常见错误

Common Mistakes with Functions

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

6.6.1.1 定义和调用环节的典型问题 Typical issues when defining and calling

CodeCombat 函数常见错误示意 - codecombat.cn
图6:定义了却忘记调用,是新手最常见的失误
易错点清单:
  • 只定义、不调用——写了 def 却没在循环里喊它的名字,函数永远不运行。
  • 调用时多加了 hero.——自己定义的函数直接写名字,不要加 hero.
  • 忘写括号——调用必须带 (),只写函数名不加括号是错的。
  • 忘写冒号 :——def 函数名() 后面少了冒号。
  • 函数体没缩进——属于函数的代码必须统一向右缩进。
  • 定义写在调用之后——要先 def 好,再去调用。

本单元小结 ✨

恭喜你完成边际森林第六单元!你已经从"使用现成函数"进化到"创造自己的函数"。你的代码从此可以浓缩、复用、变得清晰易读。来回顾一下核心概念:

核心概念关键点对应关卡
函数一段被命名、可复用的代码全部
三大好处浓缩、避免重复、易读全部
函数名 / 函数体名字用来调用,体是实际代码全部
def 定义def 名字(): + 缩进函数体乡村巡游者
调用函数写名字 + ()全部
定义 ≠ 调用定义只存代码,调用才执行造币厂
不加 hero.自定义函数直接喊名字村庄守卫
多个函数各司其职,配合使用村庄守卫、造币厂
函数 + 嵌套把复杂判断封进函数村庄守护神
极简主循环循环里只剩一行调用寂静市

会封装、懂复用的英雄,才是真正高效优雅的英雄。
继续守护边际森林吧,勇敢的程序员!🌲🛡️⚙️

CodeCombat 编程知识 · 边际森林:第六单元

CodeCombat教研团队