测试与错误排查

如果我们把本节所有的函数写在一个模块中并保存,运行这个模块,那么,有可能,程序就可以运行了:

============= RESTART: C:\Users\MSI\Desktop\BlackJ\functions.py =============
>>> blackjack()

庄家拿到了:['♣2', '♡5']
你拿到了:['♢3', '♣K']

再抽一张请输入h,停牌请输入s:h

你拿到了: ♣6

再抽一张请输入h,停牌请输入s:s

庄家拿到了: ♢7

庄家拿到了: ♣8

庄家的总点数为:22。庄家爆啦!

赢了耶…

不过更有可能的是。。。你的程序报错了。。。你可以根据报错的提示英文,自己寻找报错的原因,也可以把程序报错粘贴到评论区和大家一起讨论。细节上的错误,比如变量名是不是拼错了,需要大家自己去检查。本节,我们聊聊部分问题:

 

  • 缩进与程序逻辑

有的时候,虽然程序运行后并没有报错,但是整个程序的逻辑却没有按照我们预想的进行,这种情况就有可能是缩进错误造成的。

观察整个21点游戏程序,几乎每行代码都有缩进。例如,在上一节的主函数中,我们写过一段函数来询问玩家是否要再抽一张牌,还是停牌。假设我错误地将以下最后一行的缩进提前,会发生什么呢:

while answer == 'h':
    deal_card(deck, user)
    print('\n你拿到了:', user[-1])
        
    if evaluate(user) > 21:
        print('\n你的总点数为:', evaluate(user), '。爆掉啦!', sep ='')
        return
        
answer = input('\n再抽一张请输入h,停牌请输入s:')

嗯哼,while变成了一个无限循环——变量answer在while循环中不会发生改变,因此永远满足answer == ‘h’的条件。

假设除此以外的代码都写对了,运行模块时,并不会报错,而是一旦玩家选择了“再抽一张”,程序将会给玩家不停地发牌给玩家直到玩家爆牌为止:

>>> blackjack()

庄家拿到了:['♢4', '♡3']
你拿到了:['♣J', '♠10']

再抽一张请输入h,停牌请输入s:h   #只输入了一次h

你拿到了: ♡A   #被发了两张牌

你拿到了: ♠7

你的总点数为:28。爆掉啦!

 

21点主程序

为了写游戏的主程序,我把之前整理的游戏流程复制过来,并对照流程逐一解决:

1.首先,需要两个参与者————电脑和玩家、一副去掉大小王的牌,并将这副牌打乱(洗牌)。在之前的函数中,我已经决定用“列表”来装参与者获得的扑克牌,因此:

user = [] #新建空白列表,存放玩家的卡牌
computer = [] #新建空白列表,存放电脑的卡牌
deck = shuffled_deck() #利用写好的洗牌函数,将洗好的牌放入变量deck中

 

2.接着,从这副牌中拿出一张发给玩家、一张发给电脑,又一张发给玩家,又一张发给电脑,并在屏幕上公示玩家、电脑拿到的牌:

for i in range(2):
    deal_card(deck, user) #利用写好的发牌函数,发牌给玩家
    deal_card(deck, computer) #利用写好的发牌函数,发牌给电脑

print('\n庄家拿到了:', computer, '\n你拿到了:', user, sep ='')

这一步中,如果我将deal_card函数写四次,固然是可以的。但是试想如果不仅仅发两轮牌,而是需要把一副牌都发完的游戏,那用for循环就能使代码变得简练直观。
 

3.然后,询问玩家是再抽一张,还是停牌

  • 如果玩家再抽一张,则要判断此时玩家手上3张牌点数是否大于21
    • 如果大于21,玩家爆牌
    • 如果小于21,回到流程3
  • 如果玩家停牌,继续流程4
answer = input('\n再抽一张请输入h,停牌请输入s:') #利用变量answer储存玩家的选择
    
while answer == 'h': 
    #如果玩家选择再抽,则发牌给玩家并提示玩家拿到了什么牌
    deal_card(deck, user)
    print('\n你拿到了:', user[-1]) #user[-1]即用来储存玩家卡牌列表中最后(最新添加)的元素
        
    if evaluate(user) > 21:
        #如果玩家选择再抽,发牌后须检查玩家是否爆牌
        print('\n你的总点数为:', evaluate(user), '。爆掉啦!', sep ='')
        return #返回None,实际作用为结束游戏,防止下面的代码继续运行
        
    answer = input('\n再抽一张请输入h,停牌请输入s:') #如果玩家选择再抽,且再抽后没有爆牌,则再次询问玩家抽排还是停牌

观察上面这一段时要仔细分析其中逻辑,注意到第三行以下的所有代码均是while循环的缩进语块。只有当answer不再是’h‘或者玩家爆牌的时候,程序才会退出while循环。
 

4.现在,判断电脑的点数和是否小于17

  • 电脑点数和小于17,则电脑必须再抽一张牌,并回到流程4
  • 电脑点数和大于等于17但小于等于21,电脑必须停牌,继续流程5
  • 电脑点数和大于21,电脑爆牌
while evaluate(computer) < 17:
    #电脑牌点数和小于17则必须抽牌
    deal_card(deck, computer)
    print('\n庄家拿到了:', computer[-1])

    if evaluate(computer) > 21:
        #电脑抽牌后须判断其是否爆牌
        print('\n庄家的总点数为:', evaluate(computer), '。庄家爆啦!', sep ='')
        return

观察上面一段时也应注意思考什么时候这个while循环才会退出。
 

5.比较玩家和电脑手中点数和哪个更接近21

  • 电脑更接近21,电脑胜
  • 玩家更接近21,玩家胜
  • 双方均为21,但电脑拿到黑杰克,电脑胜
  • 双方均为21,但玩家拿到黑杰克,玩家胜
  • 平局
compare(computer, user) #所有的判断都写好在compare函数里面了

 

综上所述,我们把所有代码整理为一个函数,取名为blackjack:

def blackjack():
    'Python简版21点游戏主函数'

    user = []
    computer = []
    deck = shuffled_deck()

    for i in range(2):
        deal_card(deck, user)
        deal_card(deck, computer)

    print('\n庄家拿到了:', computer, '\n你拿到了:', user, sep ='')

    answer = input('\n再抽一张请输入h,停牌请输入s:')
    
    while answer == 'h':
        deal_card(deck, user)
        print('\n你拿到了:', user[-1])
        
        if evaluate(user) > 21:
            print('\n你的总点数为:', evaluate(user), '。爆掉啦!', sep ='')
            return
        
        answer = input('\n再抽一张请输入h,停牌请输入s:')

    while evaluate(computer) < 17:
        deal_card(deck, computer)
        print('\n庄家拿到了:', computer[-1])

        if evaluate(computer) > 21:
            print('\n庄家的总点数为:', evaluate(computer), '。庄家爆啦!', sep ='')
            return

    compare(computer, user)

至此,我们就完成简版21点游戏的所有程序。下面就进入测试和常见问题解决吧!
 

如果还有什么问题或者发现了文章的错误,欢迎给我留言!邮箱可以随便乱写~

得分比较与胜负判断

最后,我还需要一个能够比较参与者的得分并判断胜负情况的函数,这个函数比较简单,只需要根据游戏规则,书写if和elif组合的条件控制即可。

不过,为了使程序人性化,我们需要在分出胜负的时候,在屏幕上给玩家做出提示。例如:你赢啦!你输啦!你得了几分,电脑得了几分之类的死也死得明白。。。

注意,之前的函数当设计参与者时,我都用了变量player来代替,那是由于之前的函数每次只作用于一个参与者。不过对于得分比较来说,每次都必然涉及参与者双方。

我给这个函数取名compare:

def compare(computer, user):
    '对参与者的总分进行比较并判出胜负'
    computer_total = evaluate(computer) #在这个compare函数中,用到了上一节写的evaluate函数
    user_total = evaluate(user)

    #注意字符串'\n'表示换行,换行显示是为了让用户看得更清楚

    if computer_total > user_total:
        print('\n你输啦!\n你的总点数为:', user_total, '\n庄家总点数为:', computer_total, sep = '')

    elif computer_total < user_total:
        print('\n你赢啦!\n你的总点数为:', user_total, '\n庄家总点数为:', computer_total, sep = '')
        
    elif computer_total == user_total == 21 and 2 == len(computer) < len(user):
        print('\n你输啦!庄家拿到了黑杰克!')
        
    elif computer_total == user_total == 21 and 2 == len(user) < len(computer):
        print('\n你赢啦!你拿到了黑杰克!')
        
    else:
        print('\n平局!\n你的总点数为:', user_total, '\n庄家总点数为:', computer_total, sep = '')

 

FAQ
Q:这个函数的参数computer和user是哪来的?

A:和发牌函数与计分函数参数中的player一样,这两个都是我自己取的变量名称。实际使用compare函数时,可以使用其他的变量将其代替。如果对此感到费解,不用纠结,继续学习后面的小节会逐渐理解。

 
如果还有什么问题或者发现了文章的错误,欢迎给我留言!邮箱可以随便乱写~

计分

为了写计分函数,可能需要回顾上一章第四节第五节的内容,和数据类型章第六节关于字典(dict)的内容。

在游戏进行的过程中,我们需要多次对不同的参与者进行计分,以判断是否有超过21点的或是哪边点数更接近21。因此,计分函数也需要接收参数,来确定究竟是计算哪个参与者的分数。

由于各参与者的列表中的元素是扑克牌的牌面而不是单纯的数字,我首先建立一个字典(dict)来使牌面和其所代表的点数一一对应起来:

value_dict = {'A':11, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, '10':10, 'J':10, 'Q':10, 'K':10}

这其中只有一个问题,就是卡牌A,既可以代表11点,又可以代表1点。在字典中我决定先忽略代表1点的情况,放到后面来处理。

然后观察洗牌函数得到的列表:

>>> deck
['♣3', '♡9', '♠2', '♡A', '♠K', '♠8', '♣7', '♡8', '♣Q', '♢3', '♣2', '♢6', '♠J', '♡4', '♠3', '♡2', '♠7', '♢5', '♡3', '♢7', '♡6', '♣A', '♣10', '♢A', '♣5', '♣J', '♢10', '♡K', '♣4', '♠9', '♣8', '♠4', '♠10', '♣9', '♢9', '♢2', '♢4', '♣6', '♠A', '♣K', '♢Q', '♢J', '♡J', '♠5', '♠Q', '♢8', '♡Q', '♡7', '♡10', '♡5', '♠6', '♢K']

发现,每一张牌(表中每个元素)都是一个字符串。如果把该字符串的第一个字符(花色符号)忽略,就可以取得建立好的字典中的“键”,这一点我们可以用字符串切片来实现,例如:

>>> card = deck[1]
>>> card
'♡9'
>>> card[1:]
'9'

同时,因为卡牌A有特殊性,我还需要计算该参与者是否拿到了卡牌A,拿到了几张卡牌A。

同样,我使用变量player来替代参与者:

result = 0
A_num = 0

for card in player:
        
    if card[1:] == 'A': # 如果这张牌是A,那么A_num增加1
        A_num += 1 #注:x = x + 1可以简写为x += 1;同理x -= 1即 x = x - 1

    result += value_dict[card[1:]] #根据字典中的对应情况计算点数和

接着,处理卡牌A的问题。如果参与者手上的牌点数之和在A作为11点的情况下超过了21,那么参与者必须选择将A计为1点来防止爆牌。基于这一点,我打算作如下计算:

while result > 21 and A_num > 0: #如果参与者手里有A,且当A作11分计时总分大于21
    result -= 10 #将A改为1分计算,减去多算的10分
    A_num -= 1 #将改为1点计算的A去除

最后,给函数取个名字,叫做evaluate咯:

def evaluate(player):
    '计算参与者手中牌点数总和'
    value_dict = {'A':11, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, '10':10, 'J':10, 'Q':10, 'K':10}

    result = 0
    A_num = 0

    for card in player:
        
        if card[1:] == 'A':
            A_num += 1

        result += value_dict[card[1:]]

    while result > 21 and A_num > 0:
        result -= 10
        A_num -= 1

    return result

 

如果还有什么问题或者发现了文章的错误,欢迎给我留言!邮箱可以随便乱写~

发牌

为了写发牌函数,可能需要回顾数据类型章,第四节第九节的内容。

我认为,发牌函数有两个关键——牌从哪里来,牌到哪里去。

牌从刚才洗好的牌里面来,这个简单。同时,我觉得发牌是从牌堆的一侧顺序发的,而不是从牌堆中间随机抽一张发的——换言之,刚才洗好的那个deck列表,要么从头开始抽出元素发给玩家,要么从尾巴开始抽出,不应该是从中间抽。同时,发出去的牌就不应该再存在于deck列表中了。回顾列表的方法,我觉得最有效的就是pop(),既能从列表尾抽出元素、也能返回这个被抽出的元素、还能把该元素从原列表中删除:

card = deck.pop()

这样一来,我们就将deck列表最后一个元素赋值给了变量card,同时将这个元素从deck中删除。
 

21点简化版的参与者有两个——玩家和电脑。因此,二者都该有一个专属的列表来储存发给他们的牌。牌的去向也就应该是这两个列表。同时,要做到只写一个函数,就可以发牌给不同的人,意味着这个函数需要将发牌这一相同规则,作用于不同的对象——发牌函数需要参数。

好啦,将上述内容整理为一个函数,取名叫deal_card:

def deal_card(deck, player):
    '给参与者发牌'
    card = deck.pop()
    player.append(card)

 

FAQ:

Q:为什么deal_card函数需要两个参数,第一个deck参数是做什么用的?

A:我们先这样理解:在洗牌函数中,我们返回了洗好的列表deck,此时我们需要一个媒介,将洗牌函数中的deck引到发牌函数中来才能使用。在本次实战结束后,我们会学习命名空间,届时将详细讲为什么洗牌函数中的deck,不能直接被发牌函数调用。

 

Q:player这个变量是怎么来的?

A:player只是一个名称,指的是用来存储玩家或者电脑手中的牌的列表。也就是说,在21点游戏程序的其他地方,我们需要定义两个列表,用来替换发牌函数中的player(思考为什么不能再发牌函数中定义这两个列表?)

 

如果还有什么问题或者发现了文章的错误,欢迎给我留言!邮箱可以随便乱写~

洗牌

为了写洗牌函数,可能需要回顾命令式编程的第二节第五节内容。

首先介绍一个小知识:我们说过Python3的编码支持很多种语言,其实它也支持很多种特殊符号,包括扑克牌花色的符号,它们的编码分别是:

>>> classes = ['\u2660', '\u2661', '\u2662', '\u2663']
>>> for c in classes:
	print(c)
	
♠
♡
♢
♣

关于编码更详细的内容以后会涉及,现在复制粘贴用就好,当然你可以坚持用汉字来代替花色…

去掉大小王的扑克牌共有52张,是由四中花色和13种牌面组合而成的。我先建立两个列表,用来储存所有的花色和牌面:

classes = ['\u2660', '\u2661', '\u2662', '\u2663']
ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']

接着,我打算建立一个空列表来存放52张牌:

deck = []

下面,我准备利用for循环嵌套,生成52张扑克牌,并全部放到deck里面:

for r in ranks:
    for c in classes:
        deck.append(c + r)

接着,我需要把deck这个列表中元素的顺序随机打乱:

import random
random.shuffle(deck)

好啦,最后,我把上述所有的东西整理在一个函数里面,函数取名叫:shuffled_deck吧:

def shuffled_deck():
    '生成包含随机顺序52张牌的列表' 
    deck = []
    classes = ['\u2660', '\u2661', '\u2662', '\u2663']
    ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']

    for c in classes:
        
        for r in ranks:
            deck.append(c + r)
    
    import random
    random.shuffle(deck)

    return deck #最后还加上这个函数返回的内容,即生成好的deck列表

保存,运行试试:

============= RESTART: C:\Users\MSI\Desktop\BlackJ\functions.py =============
>>> deck = shuffled_deck()
>>> deck
['♣3', '♡9', '♠2', '♡A', '♠K', '♠8', '♣7', '♡8', '♣Q', '♢3', '♣2', '♢6', '♠J', '♡4', '♠3', '♡2', '♠7', '♢5', '♡3', '♢7', '♡6', '♣A', '♣10', '♢A', '♣5', '♣J', '♢10', '♡K', '♣4', '♠9', '♣8', '♠4', '♠10', '♣9', '♢9', '♢2', '♢4', '♣6', '♠A', '♣K', '♢Q', '♢J', '♡J', '♠5', '♠Q', '♢8', '♡Q', '♡7', '♡10', '♡5', '♠6', '♢K']

 

如果还有什么问题或者发现了文章的错误,欢迎给我留言!邮箱可以随便乱写~

理清思路

一个Python程序,实际上是由很多的模块、函数有机组合在一起而成的,我给大家画了张图(手动滑稽

那要开始写一个Python程序,首先我们应该确定这个程序大致的流程是如何的,其中需要实现那些功能;接着再逐一去开发需要实现的功能;最后再把它们整合在一起,做一些细节的调整。

本实战接下来的所有小节,都是我的个人思路,大家完全可以反驳并提出更好的方法。

我先根据上一节说的游戏规则,整理一下“Python21点简化版”应该有什么样的流程

  1. 首先,需要两个参与者————电脑和玩家、一副去掉大小王的牌,并将这副牌打乱(洗牌)
  2. 接着,从这副牌中拿出一张发给玩家、一张发给电脑,又一张发给玩家,又一张发给电脑,并在屏幕上公示玩家、电脑拿到的牌。
  3. 然后,询问玩家是再抽一张,还是停牌
    • 如果玩家再抽一张,则要判断此时玩家手上3张牌点数是否大于21
      • 如果大于21,玩家爆牌
      • 如果小于21,回到流程3
    • 如果玩家停牌,继续流程4
  4. 现在,判断电脑的点数和是否小于17
    • 电脑点数和小于17,则电脑必须再抽一张牌,并回到流程4
    • 电脑点数和大于等于17但小于等于21,电脑必须停牌,继续流程5
    • 电脑点数和大于21,电脑爆牌
  5. 比较玩家和电脑手中点数和哪个更接近21
    • 电脑更接近21,电脑胜
    • 玩家更接近21,玩家胜
    • 双方均为21,但电脑拿到黑杰克,电脑胜
    • 双方均为21,但玩家拿到黑杰克,玩家胜
    • 平局

 

这个流程要想走完,我觉得会需要以下功能

  1. 洗牌:上一章第五节的练习题中我们生成了一个有52张牌的列表,现在我需要写一个函数,先生成这个列表,再将其顺序打乱
  2. 发牌:玩家或电脑抽牌,实际上就是程序给他们发牌的过程。需要从洗好的牌中拿出一张,“发给“玩家或者电脑
  3. 计算点数和:我们需要一个函数来计算当前玩家或者电脑手上的牌点数和是多少
  4. 胜负判定:我们需要一个函数根据我们胜负规则来判断谁输谁赢

 

差不多了~ 下面,我就来逐一实现这些功能,并将它们整合在一起形成最终的程序。

 

如果还有什么问题或者发现了文章的错误,欢迎给我留言!邮箱可以随便乱写~

介绍与规则

  • 介绍

21点也称为黑杰克(Black Jack),是起源于法国的一种纸牌游戏。如果你去过海外的赌场,你会看到很多人坐在一起玩这个。这个游戏闲家和庄家获胜的概率比较均衡,学起来也容易(当然精通很难)。

21点不同的地区规则有些许不同,在这次实战中,我们要做的就是利用目前学过的Python知识,来写一个简化版的21点纸牌游戏~

21点的具体规则大家可以去网上查,也可以单击此处玩21点小游戏。下面要介绍的是为了方便我们实战而简化的规则。

 

  • 规则

游戏形式:玩家对抗电脑

玩家数量:1人

游戏目标:使手中的牌点数之和尽可能接近但不超过21。

游戏流程:将一副标准扑克牌去除大小王后,剩下52张。洗牌后,程序首先给玩家和电脑各发2张牌,并公示。玩家可根据手中的牌点数之和选择再抽一张牌或停牌。玩家选择停牌后,电脑手中牌点数和若小于17则必须抽牌,大于等17则必须停牌。双方都停牌后比较双方牌点数之和,更接近21的一方获胜。

点数计算:纸牌2-10点数即为牌面点数;纸牌J, Q, K均计为10点;纸牌A可计为1点或11点,由玩家自由选择。

胜负判定:任何时候任何一方手中牌点数超过21则称为“爆牌”,直接判负。双方停牌后以点数之和接近21的一方获胜。

特殊情况:任意一方若抽到一张A和一张10点牌(10、J、Q、K)则被称为抽到”黑杰克(Black Jack)“,黑杰克大于其他和为21点的组合。

 

  • 栗子1

小明抽到【梅花7, 方片10】
电脑抽到【梅花6,红桃J】

小明选择再抽一张牌,并抽到:方片5

此时小明牌点数为:7 + 10 + 5 = 22 > 21

小明爆牌,电脑胜

 

  • 栗子2

小明抽到【梅花9, 方片10】
电脑抽到【梅花6,红桃J】

小明选择停牌

此时电脑牌点数为:6 + 10 = 16 < 17

电脑的点数若小于17则必须再抽一张,电脑抽到:黑桃3

此时电脑牌点数为:6 + 10 + 3 = 19 > 17

电脑点数大于等于17时必须停牌

双方点数均为19,平局。

 

  • 栗子3

小明抽到【梅花9, 方片10】
电脑抽到【梅花A,红桃J】

小明选择再抽一张,获得:红心A

小明可以选择A作为1点或11点,本例中,作为11点则小明爆牌,因此小明目前手中的牌点数为:9 + 10 + 1 = 20

小明再次选择抽一张牌,获得:黑桃A

小明可以选择A作为1点或11点,本例中,作为11点则小明爆牌,因此小明目前手中的牌点数为:9 + 10 + 1 + 1 = 21

小明选择停牌

此时电脑牌点数为:11 + 10 = 21 > 17

电脑点数大于等于17时必须停牌

双方点数均为21,但是电脑是黑杰克,电脑胜。

 

小明卒

 

如果还有什么问题或者发现了文章的错误,欢迎给我留言!邮箱可以随便乱写~

自定义函数(下)*

  • 关于return和print()

上一节中,我们学习了用return来返回一个结果。初学时,可能搞不清return和print()到底该用哪一个。我们首先要理解返回一个值的意思:

上一节中,我们定义了my_abs函数:

def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x

现在,我们定义另一个函数my_abs2:

def my_abs2(x):
    if x >= 0:
        print(x)
    else:
        print(-x)

我们将二者保存后运行:

>>> my_abs(-666)
666
>>> my_abs2(-666)
666

好像是一样的嘛?不过,我们再进一步:

>>> my_abs(-666) + 1
667
>>> my_abs2(-666) + 1
666
Traceback (most recent call last):
  File "<pyshell#6>", line 1, in 
    my_abs2(-666) + 1
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'

注意到,第二个函数,首先打印出了666,然后报了一个错。报错提示说:Python不支持一个叫做NoneType的东西和整数(int)进行运算。

在Python中,有一种特殊的数据类型称为NoneType,这类数据只有一个值None,它不支持任何运算,也没有任何方法可以调用。简单来说,这种数据类型就意味着啥也没有。

函数my_abs2()的定义中没有return命令,Python默认该函数返回的结果是None;None无法和整数1进行运算。

因此,我们要根据程序后期是否需要用到函数返回的结果等条件,来判断选择return还是print()。

 

  • 还是关于return和print()

我们对上述两个函数的定义修改如下:

def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x
    print('没了')

def my_abs2(x):
    if x >= 0:
        print(x)
    else:
        print(-x)
    print('没了')

保存并执行:

>>> my_abs(-666)
666
>>> my_abs2(-666)
666
没了

在Python中,return语句在返回结果的同时,将退出函数。print()则没有这个功能。

 

  • 函数一定要传入参数嘛?

目前为止我们使用的函数都要求传入参数——函数名称后面的括号里都要填点什么。如果一个函数的法则,不需要作用于任何对象,那么这个函数可以不要参数。例如,我想讲一个生成1-100随机数的功能封装进一个函数random_num,我可以做如下定义:

>>> def random_num():
	import random
	return random.randrange(1, 101)

>>> random_num()
63
>>> random_num()
13

 

  • 文档字符串(docstring)

我们在交互界面输入help(),括号内填入任意的Python内建函数名称,会得到关于该函数的一般说明:

>>> help(len)
Help on built-in function len in module builtins:

len(obj, /)
    Return the number of items in a container.

Python的函数允许其作者在定义的第一行使用一个字符串,当该函数名称被help()调用时,显示这个字符串作为该函数的说明。比如,我们还是用我们的绝对值函数:

def my_abs(x):
    '返回参数x的绝对值'
    if x >= 0:
        return x
    else:
        return -x

保存并运行,然后我们利用help(my_abs)就可以查看到我们写的说明了:

>>> help(my_abs)
Help on function my_abs in module __main__:

my_abs(x)
    返回参数x的绝对值

这种开头写的字符串,我们给他起个名字叫文档字符串(docstring),这是个好习惯~ 可以让别人大致明白你这个函数是干嘛的。

 

  • 反复调用函数*

本章第8节我们改写过一个摄氏度与华氏度转换的程序:

try:
    temp_C = eval(input('请输入现在的摄氏温度:'))
    temp_F = temp_C * 1.8 + 32
    print('当前温度为华氏', temp_F, '度', sep = '')

except:
    print('请用阿拉伯数字输入温度')

你会发现,当用户运行这个模块时,如果用户真的输入了汉字,在得到错误提示后,程序就结束了。用户需要重新运行该模块才能更正输入。我们把这个模块用函数的方法改写一下:

def temp(): #这个函数不需要参数
    try:
        temp_C = eval(input('请输入现在的摄氏温度:'))
        temp_F = temp_C * 1.8 + 32
        print('当前温度为华氏', temp_F, '度', sep = '')

    except:
        print('请用阿拉伯数字输入温度')
        return temp() # 当用户输入错误,先打印提示信息,再重新运行这个函数

temp() #调用该函数

保存这个模块并运行:

=================== RESTART: C:\Users\MSI\Desktop\demo.py ===================
请输入现在的摄氏温度:一度
请用阿拉伯数字输入温度
请输入现在的摄氏温度:亮度
请用阿拉伯数字输入温度
请输入现在的摄氏温度:3
当前温度为华氏37.4度
>>> 

瞬间就更人性化了呢!

 

  • 本节练习

改写本章第八节中的练习题答案,使得用户任何时候输入错误都可以继续游戏。

=================== RESTART: C:/Users/MSI/Desktop/demo.py ===================
谜底是1-100间的整数,请输入您的答案...:二十
请输入纯阿拉伯数字啦...

谜底是1-100间的整数,请输入您的答案...:就不
请输入纯阿拉伯数字啦...

谜底是1-100间的整数,请输入您的答案...:咬我呀
请输入纯阿拉伯数字啦...

谜底是1-100间的整数,请输入您的答案...:55
您猜大啦

看答案

import random

answer = random.randrange(1, 101)

def guess_num():   
    '该函数用于猜数字游戏中对用户输入的数字与答案作比较,并给出相应提示'
    try:
        num = eval(input('谜底是1-100间的整数,请输入您的答案...:'))
    
    except:
        print('请输入纯阿拉伯数字啦...\n') #\n是换一行,方便用户看清
        return guess_num()

    while num != answer:
        if num > answer:
            print('您猜大啦\n')
            return guess_num()
        elif num < answer:
            print('您猜小啦\n')
            return guess_num()

    print('猜对啦!答案是:', answer, sep = '')

guess_num()

酱哦


 

如果还有什么问题或者发现了文章的错误,欢迎给我留言!邮箱可以随便乱写~

自定义函数(上)

 

上学时我们就学过数学函数:给定数集A,对其施加一个固定法则f,得到数集B,那么f(A) = B就是一个函数关系式,简称函数。

回到Python中,目前学习、使用过的函数——你比如abs(), max(), min(), print(), len(), eval()——实际上也是对给定的对象,施加一个固定的法则,然后返回一个结果,而abs、print、max这些单词,则是该函数的名称。让我们记住这四个加粗的关键词,来学习如何自定义一个函数。

 

  • 举个栗子

Python中函数定义的一般格式为:

def <函数名称>(<对象>):
    <法则>
    return <结果>

观察上述语句,发现首先有一个def,这是英文define(定义)的缩写,在Python中专门用来定义;另外有一个return,这是Python函数中用以返回结果的命令。没太看懂没关系,来看一个栗子:

回顾本章第二节

在Python中,函数的定义被写在一个个后缀为.py的文件中。这些.py文件,被称为Python的模块(modules)。 之所以输入abs(-1)我们可以得到1这个结果,是因为Python加载了一个模块,在这个模块里,发明Python的大神已经写好了abs()遵守:当x >= 0, |x| = x;当x < 0, |x| = -x这个规则。

现在我们来自己写一个可以计算绝对值的函数。首先给这个函数取个名称,就叫做my_abs吧。想要计算绝对值,my_abs需要将绝对值计算法则施加给一个对象(设为x,当然也可以设为别的你喜欢的变量名),最后返回计算的结果。

通过学习条件、循环等章节,不难写出:

def my_abs(x):
    if x >= 0:
        return x
    else:
        return -x

现在把这个定义保存在一个.py文件中,然后按F5运行,就可以使用刚定义的函数了:

=================== RESTART: C:\Users\MSI\Desktop\demo.py ===================
>>> my_abs(-666)
666

 

  • 函数的参数

目前为止,我们使用函数已经很6了:输入函数名称、后面跟一个括号、括号内则填入函数的法则需要作用的对象——这个过程,我们称为给函数传入参数。我们在定义my_abs()时规定,该函数的法则只作用于一个对象x,因此,我们在实际使用的时候,括号内也只能传入一个参数,填0个,2个或者更多,都会报错:

=================== RESTART: C:\Users\MSI\Desktop\demo.py ===================
>>> my_abs(-666, 233)
Traceback (most recent call last):
 File "<pyshell#1>", line 1, in <module>
 my_abs(-666, 233)
TypeError: my_abs() takes 1 positional argument but 2 were given

当然,传入参数个数的限制、需要传入参数与否都取决于具体函数的定义。比如我们熟悉的print()函数,就可以用逗号分隔填入很多个参数并打印。关于函数的参数,我们会在后面更深入的学习。

 

  • IDLE工作目录

刚才我们定义my_abs()的时候,是在纯文本编辑器里面完成的。当然,定义函数的过程可以直接在IDLE的交互界面下完成:

>>> def my_abs(x):
	if x >= 0:
		return x
	else:
		return -x
#注意,函数定义写完后,要按两次回车才能退出定义语块。

	
>>> my_abs(-666)
666

优点是即写即用;缺点是写的难度有点大,中间如果输错了按了回车,就得重头写了,另外这样定义的函数不能保存,关了IDLE就没了。

不过,即便my_abs()的定义已经保存在demo.py文件中,如果我们关掉IDLE,再打开:

>>> my_abs(-666)
Traceback (most recent call last):
  File "<pyshell#19>", line 1, in 
    my_abs(-666)
NameError: name 'my_abs' is not defined

不见了耶…

之前我们讲过,要使用Python模块,首先要加载它们。试一下:

>>> import demo
Traceback (most recent call last):
 File "<pyshell#0>", line 1, in <module>
 import demo
ModuleNotFoundError: No module named 'demo'

???白写了?

其实,这是由于IDLE的默认工作目录和文件demo.py的目录不一致造成的。以我的电脑为例,IDLE默认是在安装Python时一个名为Python36-32的文件目录工作的,然鹅demo.py文件我保存在了桌面上,因此Python找不到import demo的demo模块。

解决方案也就很明确了,要么改变IDLE的工作目录,要么改变demo.py的保存位置。

在这里,不推荐大家去找自己电脑的Python36-32文件夹,因为如果不小心覆盖或者删除了里面重要的模块就不好了;IDLE的工作目录的修改,这一点我们将在之后涉及。

目前来说,我们要么在交互界面中直接定义函数使用;要么将函数保存在.py文件中运行后使用。

 

  • 封装

现在我们知道了,一个函数之所以能够实现某个功能,是因为该函数的作者对函数进行了定义。例如一个简单的计算绝对值的函数,刚才我们就写了5行代码来定义它。可想而知,更为复杂的功能其定义复杂程度也会倍增。但实际上,我们使用某个函数的时候,只需要知道他的功能是什么,而不需要搞懂其详细的定义。例如,我们知道print()能将括号中的内容打印出来,但是我们并不知道print()函数是如何定义的——这种将复杂的定义“藏”在函数定义中的过程称为封装(encapsulation)。封装的优势很多,例如用户可以直接通过调用函数来实现功能,而不需了解其详细定义;封装后的函数可以反复多次调用等等。

 

  • 本节练习

在IDLE的交互界面里面直接写一个函数average(),该函数接收两个参数,并返回二者的平均值。写完后用几组数字试验一下。

看答案

>>> def average(num_1, num_2):
	return (num_1 + num_2) / 2 #该行括号是为了优先运算括号内加法

>>> average(1, 2)
1.5
>>> average(47.8, -92)
-22.1

酱哦


 

如果还有什么问题或者发现了文章的错误,欢迎给我留言!邮箱可以随便乱写~