函数与抽象

在前面的章节中我们已经学习了变量、数据类型、条件以及列表和迭代,这些已经足够写你想要的几乎所有程序了。但是随着想法越来越多,代码会越来越多,我们可以进一步提升,通过学习函数和抽象,能让我们面对更多代码的时候变得轻松,写的代码也会更加的简洁。接下来我们通过一个例子来学习函数的基础知识。

狗叫原始程序

分析一下如下的程序。

dog_name = '大宝'
dog_weight = 40
if dog_weight > 20:
    print(dog_name,"says 汪汪")
else:
    print(dog_name,"says 呜呜")

dog_name = '二宝'
dog_weight = 9
if dog_weight > 20:
    print(dog_name,"says 汪汪")
else:
    print(dog_name,"says 呜呜")

dog_name = '可乐'
dog_weight = 12
if dog_weight > 20:
    print(dog_name,"says 汪汪")
else:
    print(dog_name,"says 呜呜")

dog_name = '土豆'
dog_weight = 65
if dog_weight > 20:
    print(dog_name,"says 汪汪")
else:
    print(dog_name,"says 呜呜")

你会发现程序的功能很简单:不同的狗狗根据体重不同会发出不同的叫声,虽然程序没有什么bug,但是我们也能很明显的发现几个不舒服的地方:

  • 同样的代码不断重复,看起来很冗余
  • 这么多代码却没有做多少工作
  • 可以想象书写的过程很是枯燥乏味

而且,这样的代码会导致当我们想要修改的时候就比较麻烦了,比如说我们需要为不到2公斤的小狗增加一个叫声“嘤嘤”,需要对这个代码做哪些修改呢?很容易想到的是再后面再添加一种情况,但是因为有很多只狗狗,所以这段代码我们需要在很多个地方添加,这就很麻烦了,目前是4只狗狗,如果有40只,400只呢!显然我们需要找到一种更好的方法来优化这个程序!

狗叫函数

我们可以通过将一段代码组合在一起的方式来实现这个优化,如果学过Scratch的话很容易联想到这个功能叫做新建积木,同样的功能在Python中也是有的,叫做函数,其实Python和C++等代码编程语言中的函数跟Scratch中的新建积木是一个含义。在我们新建狗叫函数之前,其实我们很早就接触过Python中的其他函数了,最常见的就是print的了,下面我们以print为例来讲解函数是如何工作的。

print("我是一个函数")

要使用一个函数,首先要知道他的名字并且拼写要严格正确(区分大小写噢),然后函数后面要跟上一对小括号(注意是一对小括号),然后函数里面可能会填写一些内容,这些叫做函数的参数,参数可以不填也可以填1个或者多个。比如print函数就可以不填,直接使用print(),这样会输出一个换行,也可以输入1个参数,比如上面的print("我是一个函数'),也可以输入多个参数,比如print("我是一个参数","你懂了吗") 像这种可以直接使用的函数,叫做内置函数,是我们安装python的时候已经提前创建好的函数。如果内置的函数不能满足我们的需求,我们也可以创建自己的函数。

自定义函数

接下来我们来创建我们所需要的狗叫函数

def bark(name,weight):
    if(weight>20):
        print(name,"says 汪汪")
    else:
        print(name,"says 呜呜")

要创建一个函数,首先是一个def关键字,后面是一个名字,新建的这个函数的名字。 注意,关键字的意思是说这些单词是python中已经有固定用途的单词,不要把这些单词用作变量名、函数名等等,否则会有意料之外的bug。 举个例子:

print("hello")
def print():
    a = 1
print("hello")

回到函数的定义,在函数名字后面要跟上一对小括号,小括号里面要填写0个或多个参数。可以把这些参数想成是变量,其中包含你调用函数时传入的值。 然后是一个冒号,这个细节很重要,很多同学容易忽略,如果加上冒号的话,回车之后VSCode会自动识别出来这是一个函数,所以接下来的代码自动就加上了缩进,如果没有加冒号的话,这里的代码就没有缩进了,这也是大家写代码的时候判断自己是否加了冒号的一个小技巧,如果发现没有自动加上缩进,大概率说明忘了加冒号。 这里面的内容就是函数具体要重复使用的代码块了,我们一般把这些代码块叫做函数体。

函数的使用

定义好了函数之后,如果使用函数呢,刚才提到过print函数,我们直接输入函数的名字加上小括号就可以使用函数了,对于自己定义的函数也是一样的。

bark('大宝',21)
bark('二宝',11)

这样就可以直接调用狗叫函数了,当你有很多狗狗的时候也不用像之前那样复制很多遍代码了,是不是简化了很多呢。接下来我们来深入研究一下函数的执行过程。我们添加两行print语句,然后单步运行下我们的程序。

print("准备好了!")
#定义
def bark(name,weight):
    if(weight>20):
        print(name,"says 汪汪")
    else:
        print(name,"says 呜呜")
#调用
bark('大宝',21)
print("执行完毕!")

python解释器的执行的过程首先从第一行的print语句开始,则会打印出来一个“准备好了!” 下一步python解释器会看到bark函数的定义,在这里,python解释器并不执行函数中的代码;实际上,它知识创建一个名字bark,并保存函数参数和函数体,以备以后使用。解释器处理完函数定义之后,我们就能在需要时使用函数名bark来调用这个函数了。 这就是接下来的这两个语句,一个语句传入了大宝和21两个参数,一个语句传入了二宝和11两个参数,我们以大宝这一行为例,这两个参数传递到哪里了呢?传递到函数定义中的name和weight两个参数了,一般把这个叫做形式参数,可以把他当成全新的变量,传进来的大宝存到了name变量里,传进来的21存到了weight变量里。 接下来就会进入到函数体里面执行,记住并不是跳过函数还是要进入到函数体里面执行,首先是一个判断,weight大于20,所以执行print函数,输出大宝汪汪。 这样就完成了bark函数体重的代码。接下来去哪里呢?一个函数结束时,程序的控制会返回到之前调用函数的地方,解释器从那里继续执行。这个很重要,我们再重复一遍,一个函数结束时,程序的控制会返回到之前调用函数的地方,解释器会从那里继续执行。 在我们的程序中就是执行最后一行的print语句了,打印出来一个“执行完毕”,因为是最后一行代码,所以整个程序就顺利结束了。

函数测验

下面是更多的狗叫函数调用的例子,你觉得会输出什么结果呢?多说一句,程序报错也是一种结果噢。

  • bark("雪糕",20)
  • bark("大壮",-1)
  • bark("欢欢",0,0)
  • bark("豆豆","20")
  • bark("黑豹",21)
  • bark("大旺",10) 最后总结一下,我们把代码优化成函数就是抽象代码的一种方法,有了这个抽象,以后我们只要使用抽象之后的函数就可以方便地得到不同重量的狗狗对应的叫声了。

交通方式函数

我们通过另一个例子来练习一下函数的使用,这个例子叫交通方式,我们需要创建一个函数,它的功能是当我把距离告诉这个函数时,它能够告诉乘坐什么交通方式比较合适,我们的规则是:

  • 超过120公里时:坐飞机
  • 超过2公里时:坐汽车
  • 2公里内:步行 可以暂停思考并尝试一下,看自己能不能写出来这个函数。 接下来我们一起来定义并调用这个函数。 首先是定义
def how_shoud_I_get_there(miles):
    if miles>120 :
        print("坐飞机")
    elif miles>12 :
        print("坐汽车")
    else:
        print("走路")

然后是调用:

how_shoud_I_get_there(2)
how_shoud_I_get_there(15)
how_shoud_I_get_there(150)

我们可以进一步利用我们之前学过的input函数来让程序更加的智能:根据我们输入的公里数来判断合适的交通方式,注意要通过int函数把输入的字符串转换为数字

a = input("距离多远?")
print("你应该选择:")
how_shoud_I_get_there(int(a))

通过这个例子我们进一步巩固了函数的定义和使用,接下来讲解几个关于函数的常见问题:

  1. 函数名有什么规则 给函数起名字的规则跟变量名的规则是一样的,开头不能是数字或其他特殊符号,只能是字母或者下划线,另外就是不能是关键词。另外如果函数名字比较长包含多个单词的话,一般会在单词之间用下划线连接起来,就像交通方式这个函数那样。
  2. 可以像函数传递什么类型呢? 可以给函数传递我们学过的任何数据类型:数字、字符、布尔,之后我们学到列表等数据结构的时候也是可以往函数中传递的。
  3. 函数中可以调用其他函数吗? 可以的,比如我们的交通方式函数中就用到了内置的print函数,当然我们也可以定义一个函数1,再定义一个函数2,然后在函数2中调用函数1,这也允许的,当然是否这样做取决于实际的需要。

函数返回值

到目前为止,我们只是向函数传递值,也就是调用函数并且向他的参数传递值,我们其实也是可以从函数得到值的,这就是函数的返回值,需要用到一个新的语句return语句。 我们以bark函数为例,把它改造一下:

def get_bark(weight):
    if(weight>20):
        return "says 汪汪"
    else:
        return "says 呜呜"
get_bark(28) #汪汪
a = get_bark(20)  #a=嘤嘤
print(a)    #print(嘤嘤)
print(get_bark(15)) #print(嘤嘤)

接下来让我们做几个关于return的练习题,计算以下几个函数调用的返回值

def make_greeting(name):
    return 'Hi ' + name + '!'
def compute(x, y):
    total = x + y
    if(total > 10):
        total = 10
    return total
def allow_access(person):
    if person == '小帅':
        answer = True
    else:
        answer = False
    return answer

make_greeting('小帅')
compute(2,3)
compute(6,6)
allow_access('小美')
allow_access('小帅')
#记得加上print
print(make_greeting('小帅'))
print(compute(2,3))
print(compute(11,3))
print(allow_access('小美'))
print(allow_access('小帅'))

最后一点,return之后程序就会返回到调用程序的地方继续执行,所以return后面一般是不写代码的,如果写了也不会被执行到。

重构设计头像代码

接下来让我们完成一个新的任务:假设有一家公司正在开发一些代码,想要帮助他们的用户选择头像。你知道的,头像可以是真实的或者想象的,他们的代码所做的是询问用户对头发颜色、眼睛颜色、性别等有什么偏好,然后根据用户反馈的这些偏好为用户生成一个合适的头像。 不过,对于这个比较简单的任务来说,他们目前的代码有点儿太复杂了。代码如下,注意,他们希望能为用户提供方面,所以提供了一些默认值,用户可以输入一个值,或者也可以直接按回车键接受默认值。

hair = input("你想要什么颜色的头发 [棕色]?")
if hair == '':
    hair = '棕色'
print('你选择的是:', hair)
hair_length = input("你想要长发还是短发 [短发]?")
if hair_length == '':
    hair_length = '短发'
print('你选择的是:', hair_length)
eyes = input("你想要什么颜色的眼睛 [蓝色]?")
if eyes == '':
    eyes = '蓝色'
print('你选择的是:', eyes)
gender = input("你想选择什么性别的角色 [女生]]?")
if gender == '':
    gender = '女生'
print('你选择的是:', gender)
has_glasses = input("戴眼镜吗 [不]?")
if has_glasses == '':
    has_glasses = '不'
print('你选择的是:', has_glasses)
has_beard = input("有胡须吗 [不]?")
if has_beard == '':
    has_beard = '不'
print('你选择的是:', has_beard)

在这段代码中,我们会根据头像的每个属性提示用户输入相应的选择,每个问题还包括了一个默认选择,比如这里的棕色头发;如果用户只是按回车键,就把变量赋值为默认值,否则,我们就使用用户输入的值。接下来,还会输出用户的各个选择。对于每个属性反复进行这样的操作。 运行这段代码,看它是如何工作的,掌握它的运行逻辑,然后才能更好的优化。

抽象头像代码

为了能够代码更简洁,我们需要找出代码中共性的东西,然后把它们抽象到一个函数中。分析代码发现以下几点:

  • 对于每个属性,我们都会提示用户输入并得到他们的响应。
  • 每次提示用户输入时,我们会问一个不同的问题。
  • 而且有一个不同的默认值,如棕色、短发、蓝色。
  • 每个属性会赋值给一个变量并且我们会打印这个变量展示给用户。
  • 对于每个属性,我们会做同样的事情。 总结一下,在每段代码中有两个地方有变化,就是问题和默认值。这些将成为我们的参数,因为每次调用函数的时候它们都不同。下面就从这里开始:
def get_attribute(question, default)

我们定义了一个新的函数,名字叫get_attribute,它有两个参数,question对应要问的问题,如你想要什么颜色的头发,另一个参数default对应默认值,如棕色。

编写get_attribute函数体

现在来完成函数体。根据原先的代码,首先需要创建一个字符串作为问题,来提示用户并得到他们的输入。 通过将query和default参数组合在一起,并且添加上之前的中括号和问号来格式化这个字符串,通过input函数提示用户并得到他们的输入,然后存到answer这个变量中。

def get_attribute(query, default):
    question = query + '[' + default + ']? '
    answer = input(question)

然后,我们需要像之前的代码一样,查看用户是否是直接按下了回车键选择默认值(这种情况下,answer是一个空字符串)。然后输出用户的选择。 最后还有一件事要做:我们需要把answer返回给调用get_attribute函数的地方。怎么做呢?用我们之前刚刚学到的return语句。

def get_attribute(query, default):
    question = query + '[' + default + ']? '
    answer = input(question)
    if(answer == ''):
        answer = default
    print('你选择的是', answer)
    return  answer

调用get_attribute函数

定义好get_attribute函数之后,针对每个属性,我们只需要写出适当的get_attribute()函数调用就可以了。

def get_attribute(query, default):
    question = query + '[' + default + ']? '
    answer = input(question)
    if(answer == ''):
        answer = default
    print('你选择的是', answer)
    return  answer
    
hair = get_attribute("你想要什么颜色的头发", "棕色")
hair_length = get_attribute("你想要长发还是短发", "短发")
eyes = get_attribute("你想要什么颜色的眼睛", "蓝色")
gender = get_attribute("你想选择什么性别的角色", "女生")
has_glasses = get_attribute("戴眼镜吗", "不")
has_beard = get_attribute("有胡须吗", "不")

最后总结一下什么是重构:要调整代码使它更简洁、更可读、更结构化,这是优秀的程序员经常要完成的一个活动。通常这个活动称为重构代码。

全局变量与局部变量

在学习函数之后,我们需要对变量做更深入的学习,在此之前,关于变量我们学过了声明变量/定义变量、设置变量的值以及改变变量的值/赋值。引入函数之后,我们需要学习更多变量的知识,尤其是局部变量和全局变量的知识,对于之前提到的形式参数,可以把它当做局部变量来理解。

理解变量作用域

通过一个例子来演示一下局部变量和全局变量并学习下变量作用域的知识。

def have_a_drink(param):
    msg = '喝 ' + param
    print(msg)
    param = '橙子'
drink = '水'
have_a_drink(drink)
print('饮料是:', drink)

思考一下这段代码中各个不同类型的变量,哪些是局部变量,哪些是全局变量。 通过单步执行来跟踪一下这段代码的执行过程,对各个变量的变化和类型进行更深入的思考。

在函数中使用全局变量

如果要在函数中使用全局变量,首先要用global关键字让Python知道我们要使用一个全局变量,并且我们还可以在函数中修改一个全局变量的值。

def have_a_drink(param):
    global drink
    #print(drink)
    drink = '橙子'
    msg = '喝 ' + param
    print(msg)
    param = '橙子'
drink = '水'
have_a_drink(drink)
print('饮料是:', drink)

看起来很简单:要在函数中使用一个全局变量,只需要使用global关键字明确你的意图,然后就可以随意使用了。不过要当心,很多程序员都在这方面犯过错误。如果你没有使用global关键字,仍然可以摘函数中读取全局变量的值,不过如果你想改变这个全局变量的值,可能会有两种结果:如果这是你再函数中第一次使用这个变量,Python会认为这是一个局部变量而不是全局变量;或者如果你已经读取过这个值,在这种情况下Python会抛出一个UnboundLocalError.只要你看到这个错误,就应该查看是不是不小心混用了局部和全局变量。

偷钱练习题:

以下代码模拟的是一个小偷到银行偷钱的过程,请问银行的余额最终是多少?小偷真的偷到钱了吗?

balance = 10500                     #balance:余额
def steal(balance, amount):         #steal:偷  amout:数量
    if(amount < balance):
        balance = balance - amount
    return amount
proceeds = steal(balance, 1250)     #proceed:结果
print('小毛贼,你偷了', proceeds)
print('还剩',balance)

balance虽然是一个全局变量,但是在steal函数中还有一个形参,也就是局部变量balance,所以函数中修改balance的时候其实修改的是局部变量balance,对于全部变量的balance也就是余额其实是没有影响的,所以银行余额还是跟之前一样的。

函数默认值与关键字

如果函数有多个参数的话,在调用的时候一定要注意参数的顺序,如果没有按照正确的顺序传入参数的话,可能会有意料之外的bug出现。比如有一个需要高度和速度的飞行函数,而我们在调用的时候弄错了参数的顺序,你能想象让飞机的高度是500而速度是3000米吗?

默认值如何工作

为了缓解这种可能存在的问题,我们可以使用函数中的默认值和关键字机制。下面是一个打招呼函数的例子:

def greet(name,message = '吃了吗?'):
    print('Hi', name + '.', message)

在这个函数中,name是一个普通的形参,在等待相应的实参;message是一个具有默认值的形参,它就不用太关心是否有实参传过来了,因为不传的话它可以使用默认值。 通过以下两个例子验证一下:

greet('小胖')
greet('小虎', '今天感觉怎么样?')

先列出必要参数

如果你的函数有一个带默认值的参数了,你再添加新的不带默认值的参数时,一定要把这个必要的参数放在前面,否则就会出现错误。比如:

def greet(name, message='多喝水!', emotion):
    print('Hi', name + '.', message, emotion)

运行这个程序时,python会报错,指出一个必要参数在一个默认参数后面,为什么不能这么做呢?简单地讲,因为如果这样做的话,当调用函数时只提供了两个参数时,python解释器就不知道到底应该把这两个参数传递给前两个还是旁边的两个了。 所以对我们来讲,要记住一点:要先列出所有必要参数,然后才是有默认值的参数。所以,修正上面的代码,可以这么做:

def greet(name, emotion, message='多喝水!'):
    print('Hi', name + '.', message, emotion)

用关键字使用实参

到目前为止,每次我们调用一个函数时,都是按位置提供实参。也就是说,第一个实参对应第一个形参,第二个实参对应第二个形参,依此类推。如果愿意,也可以使用参数名作为关键字,以一种不同的顺序指定实参。 比如:

greet(message='去哪里玩啦?',name = '小胖',emotion = '挺高兴呀')

也可以混合使用位置参数和关键字参数:

greet('小虎', message='Yo!', emotion=':)')

接下来让我们通过一个接地气的例子来验证大家是不是牢牢掌握了关键字和默认值是如何工作的,分析以下代码并确定最后的输出结果,把你的答案先写下来,待会儿我们运行下程序看是不是跟你想的一样。

def make_klm(kouwei='酸辣', kaochang=True,jidan=True, xiangcai=True ):
    peifang = kouwei + '烤冷面 '
    if kaochang:
        peifang = peifang + '加烤肠 '
    if jidan:
        peifang = peifang + '加鸡蛋'
    if not xiangcai:
        peifang = peifang + '不要'
    peifang = peifang + '加香菜.'
    return peifang
klm = make_klm()
print('来一份烤冷面,配方为:', klm)
klm = make_klm(xiangcai=False)
print('来一份烤冷面,配方为:', klm)
klm = make_klm(kaochang=False, xiangcai=False)
print('来一份烤冷面,配方为:', klm)
klm = make_klm(kouwei='酸甜辣', kaochang=False, jidan=False)
print('来一份烤冷面,配方为:', klm)

我们来分析下这个程序,程序的命名其实是不规范的,尽量都是用英语单词噢,当然如果实在是单词过于复杂,可以适当用拼音来代替。

Last Updated:
Contributors: houlinsen