logo-teal.png

在Python中创建一个简单的API,您可以使用一个轻量级的Web框架,比如Flask。以下是一个示例,展示如何创建一个基本的API,返回一个JSON响应:

Flask构建一个API例子

from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/api/hello', methods=['GET'])
def hello():
    data = {
        'message': '你好,世界!'
    }
    return jsonify(data)

if __name__ == '__main__':
    app.run()

在这个例子中,我们使用Flask(__name__)定义一个Flask应用程序。然后,我们使用@app.route装饰器定义了一个路由/api/hello,指定HTTP方法为GET。当通过GET请求访问/api/hello端点时,将执行hello函数。

在hello函数内部,我们创建一个包含简单消息的字典data。然后,我们使用jsonify函数将字典转换为JSON响应。

最后,我们使用app.run()运行Flask应用程序。

为了测试API,您可以运行Python脚本,它将启动一个本地开发服务器。您可以在http://localhost:5000/api/hello上访问API。响应将是一个包含消息"你好,世界!"的JSON对象。

这是一个基本的示例,帮助您开始使用Flask创建API。您可以添加更多的路由、处理不同的HTTP方法,并根据具体需求执行各种操作。

使用FastAPI构建一个API,随机生成一个偶数

FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 3.6+ 并基于标准的 Python 类型提示。

from fastapi import FastAPI
from random import randint

app = FastAPI()

@app.get("/api/random_even")
async def generate_random_even():
    number = randint(0, 100)
    if number % 2 != 0:
        number += 1
    return {"random_even": number}

在这个例子中,我们从fastapi模块导入FastAPI,从random模块导入randint。我们使用FastAPI()创建一个FastAPI应用程序。

通过使用@app.get装饰器,我们定义了一个路由/api/random_even,处理该端点的GET请求。在generate_random_even函数内部,我们使用randint(0, 100)生成一个0到100之间的随机数。如果生成的数字不是偶数(即奇数),我们将其加1,使其变为偶数。

最后,我们返回一个包含生成的随机偶数的JSON响应。

要测试这个API,您可以运行Python脚本,FastAPI应用程序将启动一个本地开发服务器。您可以在http://localhost:8000/api/random_even上访问API。每次访问这个端点时,它都会生成一个新的随机偶数。

注意:在运行脚本之前,确保已通过pip install fastapi uvicorn安装了FastAPI及其所需的依赖项。然后,您可以使用uvicorn main:app --reload来启动应用程序,假设脚本的名称为main.py。

pip install fastapi uvicorn
uvicorn main:app --reload

浏览器访问该API

http://localhost:8000/api/random_even

指定ip和端口运行fastapi应用

uvicorn main:app --host 192.168.1.100 --port 8000
uvicorn main:app --host 0.0.0.0.0 --port 7878 ## 使用不同的端口

FastAPI传入参数

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}/items/{item_id}")
def get_user_item(user_id: int, item_id: str):
    return {"user_id": user_id, "item_id": item_id}

url指定参数,传入函数

根据函数的参数自动匹配url里参数?

from fastapi import FastAPI

app = FastAPI()

@app.get("/items")
def get_items(item_id: int, q: str):
    return {"item_id": item_id, "q": q}

如何构建自己的汇率API?

拓展?使用现有的api,作一些封装,再转发除去实现自己的api服务

3e904319-2068-47eb-8e93-3f35219bbce0.jpeg

前面汇率实时转换的小程序,使用了api key,因为是免费的,所以有调用次数的限制
找了一些免费、公开、无需key的API,把玩一下看看
这里列了不少免费的API
没有一一尝试,感兴趣可以自行测试一下,免费的,不能用也很正常:)

对于汇率转换小程序,可以改用上面列表里提到的第30个api
|30 |Crypto & Finance |Exchangerate |Currency exchange & crypto rates| https://api.exchangerate.host/latest|

使用requests库,访问该url,如法炮制:

import requests
import json
url = 'https://api.exchangerate.host/latest'
response = requests.get(url)
json_object = json.loads(response.text)
#print(json_object) 
if json_object["success"] == True:
    print(f"日期:{json_object['date']}")
    print(f"基准货币: {json_object['base']}")
    print(f"美元汇率: {json_object['rates']['USD']}")
    print(f"人民币汇率: {json_object['rates']['CNY']}")

对于一个API访问返回的数据,首先打印出来,然后用json格式化工具去查看,比如
json格式化与验证工具, 或者浏览器直接打开URL,现代浏览器会直接以JSON格式显示返回的数据

在Python里面访问这样的数据结构,使用字典数据访问形式:
json_object["result"] == "success" ## 判断结果是否成功

如下图所示,就可以比较清晰看出数据的结构:
格式化的json数据

print(f"基准货币: {json_object['base']}")
print(f"到美元的汇率: {json_object['rates']['USD']}")
print(f"到人民币汇率: {json_object['rates']['CNY']}")

以后就可以将这个API可以集成到自己的应用中...

一般性API访问小结:

  • 了解API基本调用方式
  • 了解API响应的数据结构
  • 将响应的数据专程json对象
  • 访问json对象特定字段,获取需要的数据
  • 根据实际问题实现自己程序逻辑,比如汇率转换的简单计算

练习

给定一个汇率数据API
编写一个完整的应用程序,输入一定数目的美元, 输出所有支持货币的转换金额,以及与美元的汇率
比如,程序实际运行可能如下所示:
请输入输入美元的数量:
日期, 你输入美元 $ xxxx
xxxx美元 换 yyyy 人民币; 汇率:7.2
xxxx美元 换 yyyy 日元; 汇率: 125
...
要求,尽可能输出所有支持的货币
提示:需要手工编写一个货币代码到中文文字描述的映射表,比如CNY表示人民币, USD表示美元等等(可以从网上获取相应的数据)

参考实现

获取货币代码与名称、国家地区的描述
import csv
import requests
from bs4 import BeautifulSoup

# 发起网页请求
url = "https://www.iban.hk/currency-codes"
response = requests.get(url)

# 解析HTML内容
soup = BeautifulSoup(response.text, 'html.parser')

# 找到<tbody>标签
tbody = soup.find('tbody')

# 提取表格数据
data = []
rows = tbody.find_all('tr')
for row in rows:
    cells = row.find_all('td')
    row_data = [cell.get_text(strip=True) for cell in cells]
    data.append(row_data)

# 将数据写入CSV文件
with open('currency_table_data.csv', 'w', newline='') as csvfile:
    writer = csv.writer(csvfile)
    writer.writerows(data)

将汇率web API封装成一个独立的模块

currency_exchange.py

import requests
import json
import datetime
import csv




class CurrencyExchange:
    def __init__(self, api_url="https://open.er-api.com/v6/latest/USD"):
        self.url = api_url
        self.base = 'USD'
        self.timestamp = None
        self.from_currency = None
        self.to_currency = None
        self.rates = None
        self.currency_code_mapping = None

    def get_currency_code_mapping(self, csv_file="currency_table_data.csv"):
        '''
        获取货币编码与名称的映射,预存在csv文件里
        '''
        self.currency_code_mapping = []
        with open(csv_file, 'r') as csvfile:
            csvreader = csv.reader(csvfile)
            for row in csvreader:
                self.currency_code_mapping.append(tuple(row))
        return self.currency_code_mapping
    
    def get_allrates(self):
        '''
        获取所有支持货币的汇率,相对于base货币,比如USD
        '''
        response = requests.get(self.url)
        json_object = json.loads(response.text)
        if json_object["result"] == "success":
            timestamp = int(json_object['time_last_update_unix'])
            self.timestamp = datetime.datetime.fromtimestamp(timestamp)
            self.rates = json_object["rates"]
            return self.timestamp, self.rates
        else:
            return 0, 0
    
    def get_rate(self, to_currency):
        response = requests.get(self.url)
        json_object = json.loads(response.text)
        if json_object["result"] == "success":
            if to_currency.upper() in json_object["rates"]:
                from_rate = float(json_object['rates'][self.base])
                to_rate = float(json_object['rates'][to_currency])
                from_to_rate = to_rate / from_rate
                timestamp = int(json_object['time_last_update_unix'])
                updated_time = datetime.datetime.fromtimestamp(timestamp)
                return updated_time, from_to_rate
            else:
                return 0,0
        else:
            return 0, 0
    
    def calc(self, usd_amount, to_currency):
        timestamp, rate = self.get_rate(to_currency)

        return timestamp, float(usd_amount) * float(rate)

使用自定义的类

# 引入一个自定义的模块,货币转换的一个类
from currency_exchange import CurrencyExchange

my_currency_exchange = CurrencyExchange()

# 默认该货币转换的类基础货币是美元,转换为浮点数,需要增加try except 异常处理
usd_amount = float(input("输入待转换的美元金额: "))
# 将用户的输入统一转换为大写,这样输入jpy,JPY,jPy,jPY,程序一样可以运行
to_currency = input("输入目标货币的代号: 比如 CNY, JPY, 等等...").upper()
# 查询一下货币代码与名称映射
list_of_currency_code_mapping = my_currency_exchange.get_currency_code_mapping()
to_currency_area = None
to_currency_name = None
to_currency_id = None

# 根据用户输入的货币代码,查表获取对应的国家名称,以及货币名称
for item in list_of_currency_code_mapping:
    if item[2] == to_currency:
        to_currency_area = item[0]
        to_currency_name = item[1]
        to_currency_id = item[3]
        # 给用户一个反馈
        print(f'你输入美元 ${usd_amount}, 目标货币: {to_currency_area} {to_currency_name}')
        break

# 调用自定义类货币转换的 calc方法: 负责将美元转成目标货币的金额
updated_timestamp, total = my_currency_exchange.calc(usd_amount, to_currency)

# 返回出错处理
if updated_timestamp == 0 and total == 0:
    print("查询失败or不支持该货币汇率")
else:
    if to_currency_name is not None:
        print(f"[更新日期:{updated_timestamp}] 美元 ${usd_amount} 可换 {total:.2f} {to_currency_area} {to_currency_name}")
    else:
        print(f"[更新日期:{updated_timestamp}] 美元 ${usd_amount} 可换 {total:.2f} {to_currency}")




其他好玩的API

Next:

下次我们自己写个简单的API看看?

image_XZsE558j_1688837139931_raw.jpg

Python requests 是一个常用的 HTTP 请求库,可以方便地向网站发送 HTTP 请求,并获取响应结果。requests 模块比 urllib 模块更简洁。

使用 requests 发送 HTTP 请求需要先导入 requests 模块:

import requests

导入后就可以发送 HTTP 请求,使用 requests 提供的方法向指定 URL 发送 HTTP 请求,例如:

# 导入 requests 包
import requests

# 发送请求
x = requests.get('https://www.dazuicheng.com/')

# 返回网页内容
print(x.text)

每次调用 requests 请求之后,会返回一个 response 对象,该对象包含了具体的响应信息,如状态码、响应头、响应内容等:

print(response.status_code)  # 获取响应状态码
print(response.headers)  # 获取响应头
print(response.content)  # 获取响应内容

更多响应信息如下:

属性或方法说明
apparent_encoding编码方式
close()关闭与服务器的连接
content返回响应的内容,以字节为单位
cookies返回一个 CookieJar 对象,包含了从服务器发回的 cookie
elapsed返回一个 timedelta 对象,包含了从发送请求到响应到达之间经过的时间量,可以用于测试响应速度。比如 r.elapsed.microseconds 表示响应到达需要多少微秒。
encoding解码 r.text 的编码方式
headers返回响应头,字典格式
history返回包含请求历史的响应对象列表(url)
is_permanent_redirect如果响应是永久重定向的 url,则返回 True,否则返回 False
is_redirect如果响应被重定向,则返回 True,否则返回 False
iter_content()迭代响应
iter_lines()迭代响应的行
json()返回结果的 JSON 对象 (结果需要以 JSON 格式编写的,否则会引发错误)
links返回响应的解析头链接
next返回重定向链中下一个请求的 PreparedRequest 对象
ok检查 "status_code" 的值,如果小于400,则返回 True,如果不小于 400,则返回 False
raise_for_status()如果发生错误,方法返回一个 HTTPError 对象
reason响应状态的描述,比如 "Not Found" 或 "OK"
request返回请求此响应的请求对象
status_code返回 http 的状态码,比如 404 和 200(200 是 OK,404 是 Not Found)
text返回响应的内容,unicode 类型数据
url返回响应的 URL

实例

# 导入 requests 包
import requests
import json

    
# 发送请求
x = requests.get('https://api.ipify.org?format=json')
    
# 返回 http 的状态码
print(x.status_code)
    
# 响应状态的描述
print(x.reason)
    
# 返回编码
print(x.apparent_encoding)

json_object = json.loads(x.text)
print(f"public IP: {json_object['ip']}")

实时汇率转换小程序

import requests
import time
import json
import datetime
import sys


supported_currency_name = {
    0: '欧元',
    1: '美元',
    2: '日元',
    3: '英镑',
    4: '人民币'
}

supported_currency_code = {
    0: 'EUR',
    1: 'USD',
    2: 'JPY',
    3: 'GBP',
    4: 'CNY'
}

def get_rate(from_currency, to_currency):
    url = 'https://v6.exchangerate-api.com/v6/b3c4901ef11a8bcb17276efc/latest/EUR'
    response = requests.get(url)
    json_object = json.loads(response.text)
    if json_object["result"] == "success":
        from_rate = float(json_object['conversion_rates'][from_currency])
        to_rate = float(json_object['conversion_rates'][to_currency])
        from_to_rate = to_rate / from_rate
        timestamp = int(json_object['time_last_update_unix'])
        query_time = datetime.datetime.fromtimestamp(timestamp)
        return query_time, from_to_rate
    else:
        return 0, 0

print("$$$$$$$$欢迎使用汇率转换程序¥¥¥¥¥¥¥¥¥¥")
print(f"支持的货币代码有:{supported_currency_code}")
from_currency_choice = int(input('输入原始货币编号数字:'))
from_currency_code = supported_currency_code[from_currency_choice]
from_currency_name = supported_currency_name[from_currency_choice]
print(f"你输入的初始货币:{from_currency_name}")
print(f"支持的货币代码有:{supported_currency_code}")
to_currency_choice = int(input('输入目标货币编号数字:'))
to_currency_code   = supported_currency_code[to_currency_choice]
to_currency_name = supported_currency_name[to_currency_choice]
print(f"你输入的目标货币:{to_currency_name}")
source_amount = int(input('输入待转换的货币金额: '))

query_time, rate = get_rate(from_currency_code, to_currency_code)
if query_time == 0 and rate == 0:
    print("获取汇率失败...")
    sys.exit()

target_amount = source_amount * rate

print(f"[{query_time}]: {source_amount} {from_currency_name} 可以换 {target_amount:.2f} {to_currency_name}")




image_rmPjGmaY_1688792209178_raw.jpg

想象一下,你有一个机器人,你希望告诉它在不同的情况下应该做什么。状态机就像是给机器人的一组指示,根据它当前的状态或情况来执行。

这样想一想:机器人可以有不同的状态,就像你可以有不同的心情或处境一样。例如,机器人可以处于“快乐”状态、“伤心”状态或“生气”状态。

现在,假设你希望机器人根据它的状态做不同的事情。如果机器人处于“快乐”状态,你希望它跳舞。如果它处于“伤心”状态,你希望它播放一首悲伤的曲调。如果它处于“生气”状态,你希望它跺脚。

为了实现这个目标,你可以创建一个状态机。它就像是一个流程图,展示了机器人可能的不同状态以及在每个状态下应该执行的操作。

在我们的例子中,状态机可能是这样的:

  • 如果机器人处于“快乐”状态,它应该跳舞。
  • 如果机器人处于“伤心”状态,它应该播放一首悲伤的曲调。
  • 如果机器人处于“生气”状态,它应该跺脚。

所以,每当机器人改变它的状态时,它会检查状态机,并根据当前的状态知道应该采取什么行动。如果它处于“快乐”状态,它会跳舞。如果它处于“伤心”状态,它会播放一首悲伤的曲调。如果它处于“生气”状态,它会跺脚。

状态机不仅仅用在机器人上,还可以用在计算机程序、游戏甚至是交通信号灯中。它们帮助我们组织和控制在不同情况下发生的事情。

交通信号灯

当我们讨论状态机的例子时,一个经典的示例是交通信号灯。让我们使用Python来实现一个简单的交通信号灯状态机:

class TrafficLight:
    def __init__(self):
        self.current_state = 'red'
    
    def change_state(self):
        if self.current_state == 'red':
            print("红灯,停车")
            self.current_state = 'green'
        elif self.current_state == 'green':
            print("绿灯,行走")
            self.current_state = 'yellow'
        elif self.current_state == 'yellow':
            print("黄灯,请准备停车")
            self.current_state = 'red'
        else:
            print("无效状态")
            
# 创建交通信号灯状态机
traffic_light = TrafficLight()

# 模拟信号灯状态变化
traffic_light.change_state()  # 输出:红灯,停车
traffic_light.change_state()  # 输出:绿灯,行走
traffic_light.change_state()  # 输出:黄灯,请准备停车
traffic_light.change_state()  # 输出:红灯,停车

增加等待时间

为了更加真实模拟现实交通灯,需要在信号灯转换之间增加等待时间,使用time.sleep

import time

在状态转换之前等待特定时间:

time.sleep(2)  # 停留2秒

增加打印提示

使用循环打印

  • 等待时间,秒为单位
    def print_delay(self, seconds):

      for second in range(seconds):
          self.print_delay_second(1)
    
  • 一秒打印一个等待提示...
    def print_delay_second(self, second=1):

      start_time = time.time()
      while time.time() - start_time < second:
          pass    
      print("waiting...")
    

状态机涉及的一些概念:

  • 状态
  • 转换
  • 输入
  • 输出

python里面有很多现成的实现好的状态机框架

体验一个简单的实现fysom

安装

pip install fysom

一个简单的例子:
finite-state-machine.jpg

from fysom import *

fsm = Fysom({'initial': 'awake',
             'final': 'red',
             'events': [
                 {'name': 'wakeup', 'src': 'sleeping', 'dst': 'awake'},
                 {'name': 'sleep',  'src': 'awake',   'dst': 'sleeping'}]})

print(fsm.current)   # awake
fsm.sleep()
print(fsm.current)   # sleeping
fsm.wakeup()
print(fsm.current)   # awake

拓展,练习

使用fsom模拟交通灯?,参考如下:
from fysom import Fysom
import time

def on_green_light():
    print("绿灯")
    time.sleep(5)
    fsm.trigger('change')  # 绿灯持续5秒后触发状态转换到下一个状态

def on_yellow_light():
    print("黄灯")
    time.sleep(2)
    fsm.trigger('change')  # 黄灯持续2秒后触发状态转换到下一个状态

def on_red_light():
    print("红灯")
    time.sleep(5)
    fsm.trigger('change')  # 红灯持续5秒后触发状态转换到下一个状态

fsm = Fysom({'initial': 'green',
             'events': [{'name': 'change', 'src': 'green', 'dst': 'yellow'},
                        {'name': 'change', 'src': 'yellow', 'dst': 'red'},
                        {'name': 'change', 'src': 'red', 'dst': 'green'}],
             'callbacks': {'on_green': on_green_light,
                           'on_yellow': on_yellow_light,
                           'on_red': on_red_light}})

while True:
    fsm.onchange()  # 执行状态转换
    time.sleep(1)  # 在状态转换之间添加1秒的延时等待

mFUjKKRNfbh4ySCFLEFH--3--6a5t3.jpg

之前编写的snake game比较简单,每次游戏结束需要重新运行程序才能继续玩

这次我们尝试使用StateMachine,状态机设计模式,改写该程序,增加几个状态:程序开始界面,游戏中状态、游戏结束界面以及游戏介绍界面

根据前面提到的游戏的循环模式,每个游戏的状态都是可以使用该循环模式分别实现

主要就是设计一个简单的状态机:

class StateMachine():
    def __init__(self):
        self.current_state = None
        self.is_exit = False

    def change_state(self, state):
        self.current_state = state
        self.current_state.on_enter()

    def get_exit_status(self):
        return self.is_exit
    
    def update(self):
        if self.current_state:
            self.is_exit = self.current_state.handle_events()
            self.current_state.update()

    def render(self):
        if self.current_state:
            self.current_state.render()
  • 这个状态机比较简单,包含两个属性:

    self.current_state
    self.is_exit

  • 额外有几个方法:

    def change_state(self, state):
    def get_exit_status(self):
    def render(self):
    def update(self):

使用该状态机组织游戏,代码如下所示:

game_state_machine = StateMachine()
game_state_machine.change_state(MainMenuState())
    
while True:
    game_state_machine.update()
    game_state_machine.render()
    if game_state_machine.get_exit_status():
        break
创建状态机
初始状态切换到主菜单状态
进入循环: 更新状态机;渲染状态机,判断是否需要退出状态机
在更新状态机里面,根据输入进入不同状态,各个状态运行自己的更新,渲染函数

状态类设计:

class MainMenuState():
    def on_enter(self):
        print("进入主菜单界面")                

    def handle_events(self):
        print("处理输入"       

    def update(self):
        pass

    def render(self):
        print("渲染界面")

  • 完整代码如下:

    import pygame
    from snake import SnakeGame, Food, Snake
    import random

    class StateMachine():

     def __init__(self):
         self.current_state = None
         self.is_exit = False
    
     def change_state(self, state):
         self.current_state = state
         self.current_state.on_enter()
    
     def get_exit_status(self):
         return self.is_exit
     
     def update(self):
         if self.current_state:
             self.is_exit = self.current_state.handle_events()
             self.current_state.update()
    
     def render(self):
         if self.current_state:
             self.current_state.render()
    

    class MainMenuState():

     def on_enter(self):
         pygame.display.set_caption("Snake Game")
         self.screen = pygame.display.set_mode([SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH,SnakeGame.SCREEN_HEIGHT])
         main_image = pygame.image.load('snake_main.jpg')
         ## 缩放图像
         self.main_image = pygame.transform.scale(main_image, (SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH,SnakeGame.SCREEN_HEIGHT))
         self.font = pygame.font.Font(None, 60)
         self.font2 = pygame.font.Font("./AaWuHunTi-2.ttf", 60)
         self.game_title = "Smake Game Using Python"
         self.text = ""
         self.current_char = 0
         self.typing_speed = 100  # 打字速度,单位为毫秒
         self.typingsound = pygame.mixer.Sound('typing.wav')
         self.text_surface2 = self.font2.render("按空格键继续...", True, (200, 0,100))  ## 渲染文本
         self.text_rect2 = self.text_surface2.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, 3*SnakeGame.SCREEN_HEIGHT // 4))  ## 设置文本位置
         
        


    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return True
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        game_state_machine.change_state(GamePlayingState())
                    elif event.key == pygame.K_ESCAPE:
                        return True 
                    
        return False           

    def update(self):
        if self.current_char < len(self.game_title):
            self.text += self.game_title[self.current_char]
            self.current_char += 1
            self.typingsound.play()
            pygame.time.wait(self.typing_speed)

    def render(self):
        self.screen.fill((0, 0, 0))  ## 清空屏幕
        self.screen.blit(self.main_image, (0, 0))
        self.text_surface = self.font.render(self.text, True, (255, 0, 0))  ### 渲染文本
        self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, SnakeGame.SCREEN_HEIGHT // 2))  ## 设置文本位置
        self.screen.blit(self.text_surface, self.text_rect)  ### 绘制文本到屏幕

        self.screen.blit(self.text_surface2, self.text_rect2)  ### 绘制文本到屏幕        
        pygame.display.flip()

class GamePlayingState():
    def on_enter(self):
        self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT])
        
        score_board_image = pygame.image.load('score_board.png')
        ### 缩放图像
        cropped_image = score_board_image.subsurface(pygame.Rect(0, 0, 128, 128))
        self.score_board_image = pygame.transform.scale(cropped_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT))
        self.my_game = SnakeGame()
        self.clock = pygame.time.Clock()


    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    game_state_machine.change_state(MainMenuState()) 
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        game_state_machine.change_state(CommandInfoState())
                    elif event.key == pygame.K_ESCAPE:
                        game_state_machine.change_state(MainMenuState())
                    else:
                        self.my_game.handle_events(event) 

    def update(self):
        self.my_game.update()
        if self.my_game.get_is_gameover():
            game_state_machine.change_state(GameOverState())

    def render(self):
        self.screen.fill((0,0,0))
        self.screen.blit(self.score_board_image, (SnakeGame.SCREEN_WIDTH, 0)) 
        self.my_game.render()      
        pygame.display.flip()
        self.clock.tick(5)


class GameOverState():
    def on_enter(self):
        self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT], pygame.NOFRAME)
        
        gameover_image = pygame.image.load('gameover.png')
        self.gameover_image = pygame.transform.scale(gameover_image, (SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT))
        self.music = pygame.mixer.music.load('gameover.mp3')
        pygame.mixer.music.play(-1)

    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.mixer.music.play()
                    game_state_machine.change_state(MainMenuState()) 
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        pygame.mixer.music.play()
                        game_state_machine.change_state(CommandInfoState())
                    elif event.key == pygame.K_ESCAPE:
                        pygame.mixer.music.play()
                        game_state_machine.change_state(MainMenuState())
                    else:
                        pass

    def update(self):
        pass

    def render(self):
        self.screen.fill((0,0,0))
        self.screen.blit(self.gameover_image, (0, 0))      
        pygame.display.flip()


class GameLevelState():
    def on_enter(self):
        self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT])
        main_image = pygame.image.load('main_menu.png')
        ## 缩放图像
        self.main_image = pygame.transform.scale(main_image, (SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT))
        score_board_image = pygame.image.load('score_board.png')
        ### 缩放图像
        
        cropped_image = score_board_image.subsurface(pygame.Rect(0, 0, 128, 128))
        self.score_board_image = pygame.transform.scale(cropped_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT))


    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    game_state_machine.change_state(MainMenuState()) 
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        game_state_machine.change_state(GamePlayingState())
                    elif event.key == pygame.K_ESCAPE:
                        game_state_machine.change_state(MainMenuState()) 

    def update(self):
        pass

    def render(self):
        self.screen.fill((0,0,0))
        self.screen.blit(self.main_image, (0, 0))
        self.screen.blit(self.score_board_image, (SnakeGame.SCREEN_WIDTH, 0))        
        pygame.display.flip()
        

class CommandInfoState():
    def on_enter(self):
        self.x = random.randint(0, 240)
        self.y = random.randint(0, 240)
        self.screen = pygame.display.set_mode([SnakeGame.SCORE_BOARD_WIDTH+SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT])
        score_board_image = pygame.image.load('score_board.png')
        ### 缩放图像
        cropped_image = score_board_image.subsurface(pygame.Rect(128, 128, 128, 128))
        self.command_background_image = pygame.transform.scale(cropped_image, (SnakeGame.SCORE_BOARD_WIDTH + SnakeGame.SCREEN_WIDTH, SnakeGame.SCREEN_HEIGHT))
        self.font = pygame.font.Font(None, 60)
        self.text0 = "snake game using python"
        self.text1 = "using arrow key to control snake"
        self.text2 = "Esc to return & Space to proceed"

        succ_snake_image = pygame.image.load('succ1.png')
        ### 缩放图像
        self.succ_snake_image = pygame.transform.scale(succ_snake_image, (240,240))

        self.snakehisssound = pygame.mixer.Sound('snake_hiss.wav')
     

    def handle_events(self):
        for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.snakehisssound.stop()
                    game_state_machine.change_state(MainMenuState()) 
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_SPACE:
                        self.snakehisssound.stop()
                        game_state_machine.change_state(MainMenuState())
                    elif event.key == pygame.K_ESCAPE:
                        self.snakehisssound.stop()
                        game_state_machine.change_state(MainMenuState()) 

    def update(self):
        self.x = random.randint(0, 600)
        self.y = random.randint(0, 240)
        self.snakehisssound.play()
        pygame.time.wait(300)


    def render(self):
        self.screen.fill((0,0,0))
        self.screen.blit(self.command_background_image, (0, 0))
        self.screen.blit(self.succ_snake_image, (self.x, self.y))

        self.text_surface = self.font.render(self.text0, True, (0, 0, 255))  ### 渲染文本
        self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, SnakeGame.SCREEN_HEIGHT // 4))  ### 设置文本位置
        self.screen.blit(self.text_surface, self.text_rect)  ### 绘制文本到屏幕 

        self.text_surface = self.font.render(self.text1, True, (0, 0, 255))  ### 渲染文本
        self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, SnakeGame.SCREEN_HEIGHT // 2))  ### 设置文本位置
        self.screen.blit(self.text_surface, self.text_rect)  ### 绘制文本到屏幕


        self.text_surface = self.font.render(self.text2, True, (0, 0, 255))  ### 渲染文本
        self.text_rect = self.text_surface.get_rect(center=((SnakeGame.SCREEN_WIDTH+SnakeGame.SCORE_BOARD_WIDTH) // 2, 3*SnakeGame.SCREEN_HEIGHT // 4))  ### 设置文本位置
        self.screen.blit(self.text_surface, self.text_rect)  ### 绘制文本到屏幕

                
        pygame.display.flip()

pygame.init()
icon_image = pygame.image.load("snake.png")

### 设置图标
pygame.display.set_icon(icon_image)

game_state_machine = StateMachine()
game_state_machine.change_state(MainMenuState())

while True:
    game_state_machine.update()
    game_state_machine.render()
    if game_state_machine.get_exit_status():
        break

pygame.quit()

使用循环模式重新改写snake game

主要修改,将游戏输入处理、状态更新,以及渲染重绘分离

import pygame
import random
class Snake:
    def __init__(self, screen, cols, rows, cell_size = 30, color=(0, 0, 0)):
        self.body = [(3, 0), (2, 0), (1, 0)]
        self.direction = "RIGHT"
        self.cell_size = cell_size
        self.screen = screen
        self.color = color
        self.cols = cols
        self.rows = rows

    def move(self, direction):
        head = self.body[0]
        if direction == "RIGHT":
            new_head = (head[0] + 1, head[1])
        elif direction == "LEFT":
            new_head = (head[0] - 1, head[1])
        elif direction == "UP":
            new_head = (head[0], head[1] - 1)
        elif direction == "DOWN":
            new_head = (head[0], head[1] + 1)
        self.body.insert(0, new_head)
        self.body.pop()
        self.direction = direction


    def draw(self):
        head = self.body[0]

        for cell in self.body:
            x = cell[0] * self.cell_size
            y = cell[1] * self.cell_size
            pygame.draw.rect(self.screen, self.color, [x, y, self.cell_size, self.cell_size])
        head_x = head[0] * self.cell_size
        head_y = head[1] * self.cell_size

        if self.direction == "LEFT" or self.direction == "RIGHT":
            eye1_pos = head_x + self.cell_size // 2, head_y + self.cell_size // 4
            eye2_pos = head_x + self.cell_size // 2, head_y + 3* self.cell_size // 4
        elif self.direction == "UP" or self.direction == "DOWN":
            eye1_pos = head_x + self.cell_size // 4, head_y + self.cell_size // 2
            eye2_pos = head_x + 3* self.cell_size // 4, head_y + self.cell_size // 2
        else:
            pass
        pygame.draw.circle(self.screen, (255, 165, 0), eye1_pos, 4)
        pygame.draw.circle(self.screen, (255, 165, 0), eye2_pos, 4)

        
    def check_collide(self, food):
        # Check if the Snake collides with the Food
        if self.body[0] == food.position:
            return 0
        # Check if the Snake collides with the wall
        if self.body[0][0] < 0 or self.body[0][0] >= self.cols or self.body[0][3] < 0 or self.body[0][4] >= self.rows:
            return 1
        # Check if the Snake collides with its own body
        for i in range(1, len(self.body)):
            if self.body[0] == self.body[i]:
                return 1
        return 2

    def grow(self):
        self.body.append(self.body[-1])


class Food:
    def __init__(self, screen, cols, rows, cell_size = 30, color=(255, 0, 0)):
        self.position = (random.randint(0, cols - 1), random.randint(0, rows - 1))
        self.color = color
        self.cell_size = cell_size
        self.cols = cols
        self.rows = rows
        self.screen = screen

    def generate_food(self, color=(255, 0, 0)):
        self.position = (random.randint(0, self.cols - 1), random.randint(0, self.rows - 1))
        self.color = color

    def draw(self):
        x = self.position[0] * self.cell_size
        y = self.position[1] * self.cell_size
        pygame.draw.rect(self.screen, self.color, [x, y, self.cell_size, self.cell_size])

   
class SnakeGame:
    # Define some colors
    BLACK = (0, 0, 0)
    WHITE = (255, 255, 255)
    GREEN = (0, 255, 0)
    RED = (255, 0, 0)

    # Set the dimensions of the screen
    SCREEN_WIDTH = 600
    SCREEN_HEIGHT = 600

    # Set the dimensions of the board and grid
    BOARD_ROWS = 20
    BOARD_COLS = 20
    GRID_SIZE = 30
    SCORE_BOARD_WIDTH = 200
    def __init__(self):

        # Initialize Pygame
        pygame.init()

        # Set the size of the screen
        self.screen = pygame.display.set_mode([SnakeGame.SCREEN_WIDTH + SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT])
        self.snake = Snake(self.screen, SnakeGame.BOARD_COLS, SnakeGame.BOARD_COLS, SnakeGame.GRID_SIZE,SnakeGame.WHITE)
        self.food = Food(self.screen, SnakeGame.BOARD_COLS, SnakeGame.BOARD_COLS, SnakeGame.GRID_SIZE,SnakeGame.RED)
        self.score = 0

        # 创建字体对象
        self.score_font = pygame.font.SysFont('Arial', 30)
        self.score_text = 'Score'
        self.high_score = 0
        self.is_gameover = False
        self.is_gamepause = False
        #font.render('Hello, world!', True, (255, 255, 255))

        # Set the caption of the screen
        pygame.display.set_caption("Snake Game")

        # 加载图像
        score_board_image = pygame.image.load('score_board.png')
        # 缩放图像
        self.score_board_image = pygame.transform.scale(score_board_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCREEN_HEIGHT))

        snake_icon_image = pygame.image.load('succ1.png')
        self.snake_icon_image = pygame.transform.scale(snake_icon_image, (SnakeGame.SCORE_BOARD_WIDTH, SnakeGame.SCORE_BOARD_WIDTH))
        self.eatsound = pygame.mixer.Sound('eat.wav')
        self.direction = "RIGHT"
        #self.music = pygame.mixer.music.load('sample.mp3')

        # 播放声音和音乐
        #self.eatsound.play()
        #pygame.mixer.music.play()

    def draw(self):
        for row in range(SnakeGame.BOARD_ROWS):
            for col in range(SnakeGame.BOARD_COLS):
                x = col * SnakeGame.GRID_SIZE
                y = row * SnakeGame.GRID_SIZE
                pygame.draw.rect(self.screen, SnakeGame.WHITE, [x, y, SnakeGame.GRID_SIZE, SnakeGame.GRID_SIZE], 1)
    def draw_score(self):
        # 显示图像
        self.screen.blit(self.score_board_image, (SnakeGame.SCREEN_WIDTH, 0))
        self.screen.blit(self.snake_icon_image, (SnakeGame.SCREEN_WIDTH, 0))
        
        high_score_text = self.score_font.render(f'Top: {self.high_score}', True, (0, 0, 0))
        self.screen.blit(high_score_text, (SnakeGame.SCREEN_WIDTH, SnakeGame.SCORE_BOARD_WIDTH+10))


        score_text = self.score_font.render(f'{self.score_text}: {self.score}', True, (0, 0, 0))
        self.screen.blit(score_text, (SnakeGame.SCREEN_WIDTH, SnakeGame.SCORE_BOARD_WIDTH + 50))

        

    # 加载历史最高分
    def load_high_score(self):
        with open("high_score.txt", "r") as f:
            lines = f.readlines()
            for line in lines:
                print(line)
            f.close()
        try:
            self.high_score = int(lines[0])
        except:
            print('invalid score')
        
            
    # 保存当前最高分
    def save_high_score(self):
        with open("high_score.txt", "w") as f:
            f.write(f"{self.high_score}")

    def get_is_gameover(self):
        return self.is_gameover

    def handle_events(self, event):
        if event.type == pygame.QUIT:
            self.is_gameover = True
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT and self.direction != "LEFT":
                self.direction = "RIGHT"
            elif event.key == pygame.K_LEFT and self.direction != "RIGHT":
                self.direction = "LEFT"
            elif event.key == pygame.K_UP and self.direction != "DOWN":
                self.direction = "UP"
            elif event.key == pygame.K_DOWN and self.direction != "UP":
                self.direction = "DOWN"
            elif event.key == pygame.K_ESCAPE:
                self.is_gameover = True
            elif event.key == pygame.K_p:
                if self.is_gamepause:
                    self.is_gamepause = False
                else:
                    self.is_gamepause = True
    def update(self):
        # Move the Snake
        self.snake.move(self.direction)
        ret = self.snake.check_collide(self.food)
        #print(f'当前分数: {self.score}')
        if ret == 0: # 碰到事物
            self.eatsound.play()
            while True:
                self.food.generate_food()
                if self.food.position not in self.snake.body:
                    break
            self.snake.grow()
            self.score +=10
        elif ret == 1: # hit wall or itself
            self.is_gameover = True
        else: # 继续移动
            pass
    def render(self):
        # Draw the board, Snake, and Food
        self.draw()
        self.snake.draw()
        self.food.draw()
        self.draw_score()
    
    def play(self):
        # Set the clock for the game
        clock = pygame.time.Clock()
        done = False
        direction = "RIGHT"
        # Start the game loop
        self.load_high_score()
        pause = False
        while not done:
            # Handle events
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    done = True
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_RIGHT and direction != "LEFT":
                        direction = "RIGHT"
                    elif event.key == pygame.K_LEFT and direction != "RIGHT":
                        direction = "LEFT"
                    elif event.key == pygame.K_UP and direction != "DOWN":
                        direction = "UP"
                    elif event.key == pygame.K_DOWN and direction != "UP":
                        direction = "DOWN"
                    elif event.key == pygame.K_ESCAPE:
                        done = True
                    elif event.key == pygame.K_p:
                        if pause:
                            pause = False
                        else:
                            pause = True
            if pause:
                continue

            # Move the Snake
            self.snake.move(direction)
            ret = self.snake.check_collide(self.food)
            #print(f'当前分数: {self.score}')
            if ret == 0: # collide with food
                self.eatsound.play()
                while True:
                    self.food.generate_food()
                    if self.food.position not in self.snake.body:
                        break
                self.snake.grow()
                self.score +=10
            elif ret == 1: # hit wall or itself
                done = True
            else: # nothing happened
                pass

            # Clear the screen
            self.screen.fill(SnakeGame.BLACK)

            # Draw the board, Snake, and Food
            if not done:
                self.draw()
                self.snake.draw()
                self.food.draw()
                self.draw_score()

            # Update the screen
            pygame.display.flip()

            # Set the frame rate of the game
            clock.tick(5)

        pygame.quit()
        if int(self.score) > self.high_score:
            self.high_score = int(self.score)
            self.save_high_score()
if __name__ == "__main__":
    my_game = SnakeGame()
    my_game.play()









StateMachine.zip