练习
折纸问 题
一张厚度为N层的纸,每次对折都会使厚度变为原来的2倍,问第多少次对折后,其高度超过珠穆朗玛峰的高度(8848.86米)
题解
def fold_paper(thickness, height):
count = 0
while thickness < height:
thickness *= 2
count += 1
return count
print(fold_paper(0.0001, 8848.86))
打印时间
描述
请从00:00依次打印出一天的时间 示例:
- 23 : 52
- 23 : 53
- 23 : 54
题解
for 时钟 in range(24):
for 分钟 in range(60):
print(时钟, ':', 分钟)
九九乘法表
描述
要求使用循环代码打印一个九九乘法表出来.如下
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81
题解
#方法一
for i in range(1,10):
print()
for j in range(1,i+1):
print('%d*%d=%d' % (j,i,i*j),end=' ')
#方法二
i=1
while i<10: #控制行,1到9
j=1
while j <= i: #控制每行显示的数量,1到9
print("%d*%d=%d"%(j,i,i*j),end=' ') #输出
j+=1 #每行显示的数量加1
print("\n") #每一行结束换行
i+=1 #行数加1
复利的力量
小明从2026年1月1日每日定投100元,年化收益率为10%,考虑到交易日(仅排除周末),问小明在第几年可以实现100万的目标?
题解
from datetime import datetime, timedelta
import calendar
# 计算参数
daily_investment = 100 # 每日定投金额(元)
annual_return_rate = 0.10 # 年化收益率 10%
target_amount = 1000000 # 目标金额 100万元
# 起始日期:2026年1月1日
start_date = datetime(2026, 1, 1)
current_date = start_date
def count_trading_days(year):
"""统计某一年内的交易日数(仅排除周末)"""
days = 0
date_cursor = datetime(year, 1, 1)
end_date = datetime(year + 1, 1, 1)
while date_cursor < end_date:
if date_cursor.weekday() < 5:
days += 1
date_cursor += timedelta(days=1)
return days
trading_days_in_year = count_trading_days(start_date.year)
daily_return_rate = (1 + annual_return_rate) ** (1 / trading_days_in_year) - 1
# 模拟每日定投
total_amount = 0 # 当前总金额
trading_day_count = 0 # 交易日计数
current_year = start_date.year
trading_days_this_year = 0 # 当前年度的交易日数
last_year_end_amount = 0 # 上一年结束时的金额
last_year_trading_days = 0 # 上一年的交易日数
while total_amount < target_amount:
# 检查是否是工作日(周一到周五,weekday()返回0-6,0是周一,6是周日)
weekday = current_date.weekday() # 0=周一, 1=周二, ..., 4=周五, 5=周六, 6=周日
# 检查是否跨年(在处理交易日之前)
if current_date.year > current_year:
year_num = current_year - start_date.year + 1
print(f"第 {year_num} 年({current_year} 年)结束时,总金额: {last_year_end_amount:,.2f} 元,交易日数: {last_year_trading_days} 天")
current_year = current_date.year
trading_days_this_year = 0
last_year_end_amount = total_amount
last_year_trading_days = 0
trading_days_in_year = count_trading_days(current_year)
daily_return_rate = (1 + annual_return_rate) ** (1 / trading_days_in_year) - 1
if weekday < 5: # 周一到周五(0-4)是交易日
trading_day_count += 1
trading_days_this_year += 1
last_year_trading_days = trading_days_this_year
# 先计算昨日收益(复利)
total_amount = total_amount * (1 + daily_return_rate)
# 然后投入今日的定投金额
total_amount += daily_investment
last_year_end_amount = total_amount
# 检查是否达到目标(在交易日达到目标)
if total_amount >= target_amount:
break
# 移动到下一天
current_date += timedelta(days=1)
# 打印最后一年(如果还没打印)
if trading_days_this_year > 0:
year_num = current_year - start_date.year + 1
print(f"第 {year_num} 年({current_year} 年)结束时,总金额: {last_year_end_amount:,.2f} 元,交易日数: {trading_days_this_year} 天")
# 计算达到目标的实际日期
# 由于在交易日达到目标后立即break,current_date就是达到目标的日期
actual_date = current_date
year_num = actual_date.year - start_date.year + 1
print("-" * 60)
print("\n最终结果:")
print("=" * 60)
print(f"达到目标日期: {actual_date.strftime('%Y年%m月%d日')} ({calendar.day_name[actual_date.weekday()]})")
print(f"达到目标时间: 第 {year_num} 年({actual_date.year} 年)的第 {trading_days_this_year} 个交易日")
print(f"总金额: {total_amount:,.2f} 元")
print(f"累计交易日数: {trading_day_count} 天")
print(f"累计投资本金: {trading_day_count * daily_investment:,.2f} 元")
print(f"累计收益: {total_amount - trading_day_count * daily_investment:,.2f} 元")
print(f"收益率: {(total_amount - trading_day_count * daily_investment) / (trading_day_count * daily_investment) * 100:.2f}%")
print(f"实际经过天数: {(actual_date - start_date).days + 1} 天")
print("=" * 60)
字典排序
描述
将字典数组按字典的某个key排序
题解
# 方 法1:
sorted(d.cop(),key = lambda i:i[k])
# 方法2:
heappush(h,(i[k],i)) for i in d
单例模式
单例模式目的:让类创建的对象,在系统中只有 唯一的一个实例
特点:每一次执行 类名() 返回的对象,内存地址是相同的
方法一:使用 __new__ 方法
基本实现
class MusicPlayer(object):
# 记录第一个被创建对象的引用
instance = None
def __new__(cls, *args, **kwargs):
# 1. 判断类属性是否为空对象
if cls.instance is None:
# 调用父类方法为第一个对象分配空间
cls.instance = super().__new__(cls)
# 2. 返回类属性保存的对象引用
return cls.instance
# 测试单例效果
player1 = MusicPlayer()
player2 = MusicPlayer()
print(f"player1: {player1}")
print(f"player2: {player2}")
print(f"是否为同一对象: {player1 is player2}") # True
优化:只初始化一次
上述实现中,虽然返回的是同一个对象,但 __init__ 方法会被多次调用
class MusicPlayer(object):
# 记录第一个被创建对象的引用
instance = None
# 记录初始化执行状态
init_flag = False
def __new__(cls, *args, **kwargs):
if cls.instance is None:
cls.instance = super().__new__(cls)
return cls.instance
def __init__(self):
# 1. 判断是否执行过初始化动作
if MusicPlayer.init_flag:
return
# 2. 如果没有执行过就执行初始化动作
print("播放器初始化")
# 3. 修改初始化状态
MusicPlayer.init_flag = True
# 测试
player1 = MusicPlayer() # 输出: 播放器初始化
player2 = MusicPlayer() # 不会再次初始化
方法二:使用装饰器
def singleton(cls):
"""单例装饰器"""
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class MySingleton:
def __init__(self, param):
self.param = param
print(f"初始化参数: {param}")
# 使用示例
if __name__ == '__main__':
a = MySingleton(10) # 输出: 初始化参数: 10
b = MySingleton(20) # 不会再次初始化
print(f"a.param: {a.param}") # 10
print(f"b.param: {b.param}") # 10
print(f"是否为同一对象: {a is b}") # True
- 代码简洁优雅
- 可重用性强
- 不需要修改原类的内部结构
方法三:使用类方法
class Singleton(object):
def __init__(self, name):
self.name = name
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
cls._instance = cls(*args, **kwargs)
return cls._instance
# 使用示例
single_1 = Singleton.instance('第1次创建')
single_2 = Singleton.instance('第2次创建')
print(f"single_1.name: {single_1.name}") # 第1次创建
print(f"single_2.name: {single_2.name}") # 第1次创建
print(f"是否为同一对象: {single_1 is single_2}") # True
线程安全版本
上述实现在多线程环境下不安全,需要加锁保护
from threading import RLock
class Singleton(object):
_lock = RLock() # 可重入锁
def __init__(self, name):
self.name = name
@classmethod
def instance(cls, *args, **kwargs):
# 使用锁确保线程安全
with cls._lock:
if not hasattr(cls, "_instance"):
cls._instance = cls(*args, **kwargs)
return cls._instance
方法四:使用元类
class SingletonType(type):
def __call__(cls, *args, **kwargs):
# 创 建 cls 的对象时调用
if not hasattr(cls, "_instance"):
# 创建 cls 的对象
cls._instance = super(SingletonType, cls).__call__(*args, **kwargs)
return cls._instance
class Singleton(metaclass=SingletonType):
def __init__(self, name):
self.name = name
# 使用示例
single_1 = Singleton('第1次创建')
single_2 = Singleton('第2次创建')
print(f"single_1.name: {single_1.name}") # 第1次创建
print(f"single_2.name: {single_2.name}") # 第1次创建
print(f"是否为同一对象: {single_1 is single_2}") # True
Singleton是元类SingletonType的实例Singleton('参数')实际上是调用元类的__call__方法- 使用
super()避免递归调用
方法五:使用模块
这是 Python 官方推荐的单例实现方式,简单且天然线程安全
class Singleton:
def __init__(self, name):
self.name = name
def do_something(self):
print(f"{self.name} 正在工作...")
# 创建单例实例
singleton = Singleton('模块单例')
# file1.py
from my_singleton import singleton
print(f"file1 中的 singleton: {singleton}")
# file2.py
from my_singleton import singleton
print(f"file2 中的 singleton: {singleton}")
# 测试文件
import file1
import file2
print(f"是否为同一对象: {file1.singleton is file2.singleton}") # True
进度条打印
描述
字符串具有更丰富的格式化方法,结合转义字符、暂停模块,可以实现进度条打印。
效果如下:
从0%到100%,每0.5秒打印一次,每次在同一行打印
[# ] 0%
[##########] 100%
题解
# 模拟进度条
import time
for i in range(1,10):
print(f"[{'#'*i:<10}]",f"{i*10}%",'\r',end="",flush=True)
time.sleep(0.5)
后缀表达式
描述
后缀表达式,又称逆波兰式,指的是不包含括号,运算符放在两个运算对象的后面,所有的计算按运算符出现的顺序,严格从左向右进行(不再考虑运算符的优先规则)。
例如:后缀表达式为“2 3 + 4 × 5 -”计算过程如下: (1)从左至右扫描,将 2 和 3 压入堆栈; (2)遇到 + 运算符,因此弹出 3 和 2( 3 为栈顶元素,2 为次顶元素,注意与前缀表达式做比较),计算出 3+2 的值,得 5,再将 5 入栈; (3)将 4 入栈; (4)接下来是 × 运算符,因此弹出 4 和 5,计算出 4 × 5 = 20,将 20 入栈; (5)将 5 入栈; (6)最后是-运算符,计算出 20-5 的值,即 15,由此得出最终结果。
示例
listx = [15, 7, 1, 1, "+", "-", "/", 3, "*", 2, 1, 1, "+", "+", "-"]
题解
# 方法1-python人思维
while len(listx) > 1:
print(listx)
for i in range(len(listx)):
if str(listx[i]) in '+-*/':
if listx[i] == '+':
new = listx[i-2] + listx[i-1]
if listx[i] == '-':
new = listx[i-2] - listx[i-1]
if listx[i] == '*':
new = listx[i-2] * listx[i-1]
if listx[i] == '/':
new = listx[i-2] / listx[i-1]
del listx[i]
del listx[i-1]
listx[i-2] = new
break
print(listx)
# 方法2-利用pop 和 append 仿c语言栈操作
listy = []
for i in listx:
if str(i) not in "+-*/":
listy.append(i) # 入栈
else:
if i == "+":
new = listy.pop() + listy.pop() # 出栈
if i == "-":
new = listy.pop() - listy.pop()
if i == "*":
new = listy.pop() * listy.pop()
if i == "/":
new = listy.pop() / listy.pop()
listy.append(new)
print(listy)
电影演员
描述
小明拿到了一个电影+演员的数据名单,他想设计一个程序,要求: 1.输入演员名 2.如果演员出演了电影,则打印他+他出演的全部电影。程序结束 3.如果演员没有出演电影,则打印查无此人。程序继续
电影 = [
'妖猫传',['黄轩','染谷将太'],
'无问西东',['章子怡','王力宏','祖峰'],
'超时空同居',['雷佳音','佟丽娅','黄轩']]
题解
电影 = [
'妖猫传',['黄轩','染谷将太'],
'无问西东',['章子怡','王力宏','祖峰'],
'超时空同居',['雷佳音','佟丽娅','黄轩']]
# 如果查到了:打印出演员+【所有的】电影,循环结束
# 如果没查到,就 循环继续,并且打印【查无此人】
找到了吗 = 0
while True:
name = input('你要找的演员')
for i in 电影:
if name not in i :
a = i #暂存---for 是逐一提取数据,并赋值
else:
print(name,'出演了',a)
找到了吗 += 1
if 找到了吗 != 0 : # 不等于 0 就代表它找到了
break
print('【查无此人】') # 1号位
规整的打印考场号
描述
学校有440人参加考试,1号考场有80个座位,要求座位号为0101--0180 后面每个考场40个座位: 2号考场考试号要求为0201--0240 3号考场考试号要求为0301--0340 后续考场以此类推,请你打印出来这些考场号吧
题解
for i in range(1,440):
if i <= 80 :
print('01{:0>2d}'.format(i))
elif i <= 440:
if i%40 == 0:
print('{:0>2d}{:0>2d}'.format(i//40-1,40))
else:
print('{:0>2d}{:0>2d}'.format(i//40,i%40))
读取BMP文件
描述
不使用第三方模块的前提下,完成对24位bmp图像的图像数据分析与像素读取。 程序设计需要体现面向对象编程的特点,以创建类的形式编写。
参考资料:
以一张2*2的24位图的bmp格式图片为例
| Offset | Offset10 | Size | Hex value | Value | Description |
|---|---|---|---|---|---|
| BMP Header | |||||
| 0h | 0 | 2 | 42 4D | "BM" | ID field(42h, 4Dh) |
| 2h | 2 | 4 | 46 00 00 00 | 70 bytes(54+16) | BMP 文件的大小(54 字节标头+ 16 字节数据) |
| 6h | 6 | 2 | 00 00 | Unused | 特定应用 |
| 8h | 8 | 2 | 00 00 | Unused | 特定应用 |
| Ah | 10 | 4 | 36 00 00 00 | 54 bytes(14+40) | 可以找到像素阵列(位图数据)的偏移量 |
| DIB Header-Device Independent Bitmaps-设备无关位图 | |||||
| Eh | 14 | 4 | 28 00 00 00 | 40 bytes | DIB 头中的字节数(从此时开始) |
| 12h | 18 | 4 | 02 00 00 00 | 2 pixels(left to right order) | 位图的宽度(以像素为单位) |
| 16h | 22 | 4 | 02 00 00 00 | 2 pixels(bottom to top order) | 位图的高度(以像素为单位)。从下到上的像素顺序为正。 |
| 1Ah | 26 | 2 | 01 00 | 1 plane | 使用的颜色平面数量 |
| 1Ch | 28 | 2 | 18 00 | 24 bits | 每个像素的位数 |
| 1Eh | 30 | 4 | 00 00 00 00 | 0 | BI_RGB,未使用像素阵列压缩 |
| 22h | 34 | 4 | 10 00 00 00 | 16 bytes | 原始位图数据的大小(包括填充) |
| 26h | 38 | 4 | 13 0B 00 00 | 2835 pixels/metre horizontal | 图像的打印分辨率, |
| 2Ah | 42 | 4 | 13 0B 00 00 | 2835 pixels/metre vertical | 72 DPI × 39.3701 inches per metre yields 2834.6472 |
| 2Eh | 46 | 4 | 00 00 00 00 | 0 colors | 调色板中的颜色数量 |
| 32h | 50 | 4 | 00 00 00 00 | 0 important colors | 0 表示所有颜色都很重要 |
| Start of pixel array(bitmap data) | |||||
| 36h | 54 | 3 | 00 00 FF | 0 0 255 | Red, Pixel(x=0, y=1) |
| 39h | 57 | 3 | FF FF FF | 255 255 255 | White, Pixel(x=1, y=1) |
| 3Ch | 60 | 2 | 00 00 | 0 0 | Padding for 4 byte alignment(could be a value other than zero) |
| 3Eh | 62 | 3 | FF 00 00 | 255 0 0 | Blue, Pixel(x=0, y=0) |
| 41h | 65 | 3 | 00 FF 00 | 0 255 0 | Green, Pixel(x=1, y=0) |
| 44h | 68 | 2 | 00 00 | 0 0 | Padding for 4 byte alignment(could be a value other than zero) |
bit(位)比特是计算机运算的基础,属于二进制的范畴
byte字节是内存的基本单位
8 bit = 1 byte
# 参考知识
data = b'\xff' # b代表这是一个二进制数据,\x代表这是一个十六进制的数据
bin_data = bin(int.from_bytes(data))[2:] # -> 11111111
int(bin_data, 2) # -> 255
# 打开文件作为可编辑对象
with open("r.bmp", "rb") as f:
d = f.read()
data = bytearray(d)
# 试着把54到246的数据都改成0x00,即黑色。这样整张图片都变成黑色了
for i in range(54, 246):
data[i]= 0x00
# 保存文件
with open("black.bmp", "wb") as f:
f.write(data)
题解
class Readbmp:
"""
read bmp files
图片的格式说明:https://en.wikipedia.org/wiki/BMP_file_format#Example_1
"""
def __init__(self, pic_path) -> None:
self.pic_path = pic_path
self.read_color()
def read_color(self):
if self.pic_path.endswith(".bmp"):
self.read_bmp()
else:
print("不支持的格式")
def read_bmp(self):
bin_datas = []
"""read file data to bin"""
with open(self.pic_path, "rb") as f:
while True:
if len(bin_datas) == f.tell():
data = f.read(1)
bindata = bin(int.from_bytes(data))[2:]
if len(bindata) < 8:
bindata = (8 - len(bindata)) * "0" + bindata
bin_datas.append(bindata)
else:
bin_datas = bin_datas[:-1]
break
self.bin_pic_head = bin_datas[0:2] # ID field
self.bin_pic_size = bin_datas[2:6] # Size of the BMP file 文件大小
self.bin_pic_exe = bin_datas[6:10] # 特定应用,默认为0
self.bin_pic_address = bin_datas[10:14] # 图片信息开始地址
self.bin_pic_dib = bin_datas[14:18] # DIB 头中的字节数
self.bin_pic_w = bin_datas[18:22] # 图片像素宽度
self.bin_pic_h = bin_datas[22:26] # 图片像素高度
self.bin_pic_color_num = bin_datas[26:28] # 使用颜色平面数
self.bin_pic_color_long = bin_datas[28:30] # 每个像素位数
self.bin_pic_bi = bin_datas[30:34] # BI_RGB
self.bin_pic_big = bin_datas[34:38] # 原始图像数据大小
self.bin_pic_printpix = bin_datas[38:42] # 打印分辨率
self.bin_pic_dpi = bin_datas[42:46] # DPI
self.bin_pic_color_num = bin_datas[46:50] # 调色板中颜色数量
self.bin_pic_color_important = bin_datas[50:54] # 重要颜色数量
self.bin_pic_data = bin_datas[54:] # 图片数据
self.bin_to_pic()
# 将二进制数据转化成十进制数据
def bin_to_dec(self, bin_datas):
bin_data = ""
for i in reversed(bin_datas):
bin_data += i
return int(bin_data, 2)
# 将列表转为3个一组的二维列表
def change_data(self, data):
data_2d = []
x = []
for i in data:
x.append(int(i, 2))
if len(x) == 3:
data_2d.append(tuple(x))
x = []
return data_2d
# 处理图片数据
def bin_to_pic(self):
self.pic_head = chr(int(self.bin_pic_head[0], 2)) + chr(
int(self.bin_pic_head[1], 2)
)
self.pic_size = self.bin_to_dec(self.bin_pic_size)
self.pic_exe = self.bin_to_dec(self.bin_pic_exe)
self.pic_address = self.bin_to_dec(self.bin_pic_address)
self.pic_dib = self.bin_to_dec(self.bin_pic_dib)
self.pic_w = self.bin_to_dec(self.bin_pic_w)
self.pic_h = self.bin_to_dec(self.bin_pic_h)
self.pic_color_num = self.bin_to_dec(self.bin_pic_color_num)
self.pic_color_long = self.bin_to_dec(self.bin_pic_color_long)
self.pic_bi = self.bin_to_dec(self.bin_pic_bi)
self.pic_big = self.bin_to_dec(self.bin_pic_big)
self.pic_printpix = self.bin_to_dec(self.bin_pic_printpix)
self.pic_dpi = self.bin_to_dec(self.bin_pic_dpi)
self.pic_color_num = self.bin_to_dec(self.bin_pic_color_num)
self.pic_color_important = self.bin_to_dec(self.bin_pic_color_important)
self.pic_data = self.change_data(self.bin_pic_data)
# 打印图片信息
def show(self):
print(
"""
文件ID {}
图像大小(Byte) {}
特定应用 {}
图片信息开始地址 {}
DIB 头中的字节数 {}
图片像素宽度 {}
图片像素高度 {}
使用颜色平面数 {}
每个像素位数 {}
BI_RGB {}
原始图像数据大小(Byte) {}
打印分辨率 {}
DPI {}
调色板中颜色数量 {}
重要颜色数量 {}
图片数据 {} .... {}
""".format(
self.pic_head,
self.pic_size,
self.pic_exe,
self.pic_address,
self.pic_dib,
self.pic_w,
self.pic_h,
self.pic_color_num,
self.pic_color_long,
self.pic_bi,
self.pic_big,
self.pic_printpix,
self.pic_dpi,
self.pic_color_num,
self.pic_color_important,
self.pic_data[:5],
self.pic_data[-5:],
)
)
# 判断颜色
def color(self, color):
b, g, r = color[0], color[1], color[2]
if r == 0 and g == 0 and b == 0:
return "黑色"
elif r == 0 and g == 0 and b == 255:
return "蓝色"
elif r == 0 and g == 255 and b == 0:
return "绿色"
elif r == 255 and g == 0 and b == 0:
return "红色"
elif r == 255 and g == 255 and b == 255:
return "白色"
else:
return "其他颜色"
# 统计颜色
def count_color(self):
color_dict = {}
for i in self.pic_data:
if i in color_dict:
color_dict[i] += 1
else:
color_dict[i] = 1
return color_dict
# 判断颜色的比例
def color_percent(self):
color_dict = self.count_color()
color_percent_dict = {}
for i in color_dict:
color_percent_dict[self.color(i)] = int(
color_dict[i] / len(self.pic_data) * 100
)
for i in color_percent_dict:
print("{} 占比百分之 {}".format(i, color_percent_dict[i]))
p = Readbmp("r.bmp") # 另存为新文件
p.color_percent()
# 红色 占比百分之 100
"""
r.bmp是8*8的位图,其中有一个点是红色,其他都是黑色
"""
# 打开文件作为可编辑对象
with open("r.bmp", "rb") as f:
d = f.read()
data = bytearray(d)
# 试着把54到246的数据都改成0x00,即黑色。这样整张图片都变成黑色了(也可以只更改某个数据端)
for i in range(54, 246):
data[i]= 0x00
# 保存文件
with open("rn.bmp", "wb") as f:
f.write(data)
p = Readbmp("rn.bmp")
p.show()
p.color_percent()
# 黑色 占比百分之 100
21点游戏
描述
- 目标: 手牌点数尽可能接近 21 且不超过 21。超过则判负。
- 点数规则:
- A: 计 11,若总点数超过 21,可按需要降为 1(可多次降,直到不爆)。
- J/Q/K: 计 10。
- 其余数字牌按面值计数。
- 黑杰克: 首两张牌正好 21 点(如 A + 10/J/Q/K)。
- 庄家要牌规则: 小于 17 必须要牌,≥ 17 停牌。
- 比较规则(本项目实现):
- 玩家爆牌(>21)直接输;庄家爆牌(>21)玩家赢,返还 2 倍(含本金)。
- 双方均为 21:比较牌张数,玩家更少则赢 2.5 倍(含本金),否则输 0.5 倍。
- 仅玩家 21:赢 2.5 倍(含本金)。
- 仅庄家 21:输 0.5 倍。
- 双方点数相等但非 21:平局,退还本金(1 倍)。
- 其他情况:点数更大一方胜;玩家胜返还 2 倍(含本金)。
运行时交互改进:
-
开局发牌后,玩家操作前会展示庄家第一张牌,其他以
???隐藏,例如:['♠A', '???']。 -
你可以多次游玩多轮,直到玩家或庄家资金归零。
-
输入校验:下注与初始资金支持浮点数,非数字会提示重输。
项目涉及知识点
- 格式化字符串
- 三引号使用
- ANSI
- 布尔表达式
- float(input())
- try: except Exception as e:
- 二层的列表推导式
- 列表推导式
- __init__
- __str__
- __add__
- @staticmethod
- 算法(计算A是1还是11的)
- random.shuffle
- list.pop()
- len()
- dataclass
- while ( 多个条件 联合判断 )
- 装饰器
- 实例属性的调用
- str.lower()
- continue
- if elif else
题解
import random
from dataclasses import dataclass
from typing import List, Optional
from decimal import Decimal, InvalidOperation, getcontext
# 货币精度设置(两位小数)
getcontext().prec = 28
# 扑克牌花色与点数
NUMBERS: List[str] = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
COLORS: List[str] = ["♠", "♥", "♦", "♣"]
# 常量与魔法值收敛
DEALER_STAND_VALUE: int = 17
HOUSE_BANKROLL: Decimal = Decimal("100000000000000")
# 赔率(传入 win(amount) 的 amount 为实际发放金额:包含返还本金)
PAYOUT_NORMAL: Decimal = Decimal("2") # 正常胜:返还 2x 本金(含本金)
PAYOUT_BLACKJACK: Decimal = Decimal("2.5") # 黑杰克胜:返还 2.5x 本金(含本金)
PAYOUT_DEALER_BLACKJACK_LOSE: Decimal = Decimal("0.5") # 庄家 21 玩家输 0.5x
PAYOUT_TIE_PLAYER_FEWER_CARDS_WIN: Decimal = Decimal("2.5") # 双 21 且玩家更少牌
PAYOUT_TIE_PLAYER_MORE_OR_EQUAL_CARDS_LOSE: Decimal = Decimal(
"0.5"
) # 双 21 且玩家牌不更少
# 业务异常:用于替代库内 sys.exit
class GameOver(Exception):
def __init__(self, message: str):
super().__init__(message)
self.message = message
@dataclass(frozen=True)
class Card:
"""
单张扑克牌,包含点数与花色。
"""
number: str
color: str
def __str__(self) -> str:
return f"{self.color}{self.number}"
@staticmethod
def get_value(cards: List["Card"]) -> int:
"""
根据 21 点规则计算一组牌的点数。
A 优先按 11 计入,若超过 21 则降为 1(每有一张 A 可降一次)。
"""
numbers: List[str] = [card.number for card in cards]
value = 0
aces = 0
for num in numbers:
if num == "A":
aces += 1
value += 11
elif num in ["J", "Q", "K"]:
value += 10
else:
value += int(num)
while value > 21 and aces > 0:
value -= 10
aces -= 1
return value
class Deck:
def __init__(self) -> None:
self.reset()
def reset(self) -> None:
self.cards: List[Card] = [
Card(number, color) for number in NUMBERS for color in COLORS
]
random.shuffle(self.cards)
def deal_card(self) -> Card:
if len(self.cards) == 0:
self.reset()
return self.cards.pop()
class Player:
def __init__(self, name: str, money: Decimal):
self.name: str = name
self.money: Decimal = money
self.cards: List[Card] = []
self.bet_amount: Optional[Decimal] = None
def add_card(self, card: Card) -> None:
self.cards.append(card)
# 下注
def bet(self, amount: Decimal) -> bool:
if amount > self.money:
print(f"{self.name} 没有足够的资金")
return False
self.money -= amount
self.bet_amount = amount
return True
def print_money(func):
def wrapper(self, amount: Decimal):
func(self, amount)
if func.__name__ == "win":
print(
f"{self.name} 赢了{amount - self.bet_amount}元,当前余额{self.money}元"
)
elif func.__name__ == "lose":
print(
f"{self.name} 输了{amount + self.bet_amount}元,当前余额{self.money}元"
)
return None
return wrapper
# 赢钱
@print_money
def win(self, amount: Decimal) -> None:
self.money += amount
# 输钱
@print_money
def lose(self, amount: Decimal) -> None:
self.money = max(self.money - amount, Decimal("0"))
if self.money == 0:
# 不在库内退出进程,改为抛出业务异常
raise GameOver(f"{self.name} 没有足够的资金 游戏结束")
def show_hand_cards(self) -> List[str]:
return [str(card) for card in self.cards]
class Game:
def __init__(self, player: Player):
self.player = player
self.dealer = Player("庄家", HOUSE_BANKROLL)
self.deck = Deck()
# 下注
def bet(self) -> Decimal:
while True:
raw = input("请输入你的下注金额: ")
try:
amount = Decimal(raw)
except (InvalidOperation, ValueError):
print("请输入有效的金额")
continue
if amount <= Decimal("0"):
print("请输入大于0的金额")
continue
if self.player.bet(amount) is False:
continue
else:
return amount
# 发牌
def deal(self) -> None:
self.player.add_card(self.deck.deal_card())
self.player.add_card(self.deck.deal_card())
self.dealer.add_card(self.deck.deal_card())
self.dealer.add_card(self.deck.deal_card())
# 用户选择要牌
def player_hit(self) -> None:
print(f"玩家的牌是{self.player.show_hand_cards()}")
while True:
choice = input(f"是否要牌(y/n):").lower()
if choice == "y":
self.player.add_card(self.deck.deal_card())
player_value = Card.get_value(self.player.cards)
# 用光标控制序列(ANSI ESC)上移一行到行首
print(
f"\x1b[1F\x1b[2K\x1b[1F\x1b[2K"
+ f"玩家的牌是{self.player.show_hand_cards()}"
)
if player_value > 21:
break
elif choice == "n":
break
else:
print("请输入y或n")
continue
print()
# 庄家自动要牌
def dealer_hit(self) -> None:
dealer_value = Card.get_value(self.dealer.cards)
while True:
if dealer_value < DEALER_STAND_VALUE:
self.dealer.add_card(self.deck.deal_card())
dealer_value = Card.get_value(self.dealer.cards)
elif dealer_value >= DEALER_STAND_VALUE:
break
def compare(self, amount: Decimal) -> None:
"""
停牌-比较大小(规则对齐):
- 双 21:玩家牌更少赢 2.5x;否则输 0.5x
- 仅玩家 21:赢 2.5x
- 仅庄家 21:输 0.5x
- 任意一方爆牌:爆牌方输(庄家爆:玩家赢 2x;玩家爆:输 0)
- 点数相等(非 21):平局退还本金(1x)
- 其他:点数更大者胜(玩家胜赢 2x)
"""
print(f"庄家的牌是{self.dealer.show_hand_cards()}")
print(f"玩家的牌是{self.player.show_hand_cards()}")
player_value = Card.get_value(self.player.cards)
dealer_value = Card.get_value(self.dealer.cards)
if player_value > 21:
self.player.lose(Decimal("0"))
elif dealer_value > 21:
self.player.win(amount * PAYOUT_NORMAL)
# 当玩家和庄家都是21点时,比较牌的数量,玩家数量少赢,否则输
elif (player_value == 21) and (dealer_value == 21):
if len(self.player.cards) >= len(self.dealer.cards):
self.player.lose(amount * PAYOUT_TIE_PLAYER_MORE_OR_EQUAL_CARDS_LOSE)
elif len(self.player.cards) < len(self.dealer.cards):
self.player.win(amount * PAYOUT_TIE_PLAYER_FEWER_CARDS_WIN)
# 非21点且点数相同:平局,退还本金
elif player_value == dealer_value:
self.player.win(amount)
# 当玩家是21点时,赢1.5倍
elif player_value == 21:
self.player.win(amount * PAYOUT_BLACKJACK)
# 当庄家是21点时,输1.5倍
elif dealer_value == 21:
self.player.lose(amount * PAYOUT_DEALER_BLACKJACK_LOSE)
# 正常无倍数输赢
elif player_value > dealer_value:
self.player.win(amount * PAYOUT_NORMAL)
else:
self.player.lose(Decimal("0"))
def reset(self) -> None:
self.player.cards = []
self.dealer.cards = []
def start(self) -> None:
while True:
self.reset()
amount = self.bet()
self.deal()
self.dealer_hit()
# 开局时展示庄家的第一张牌,其他以 ??? 隐藏
dealer_preview = [str(self.dealer.cards[0])] + [
"???" for _ in range(len(self.dealer.cards) - 1)
]
print(f"庄家的牌是{dealer_preview}")
self.player_hit()
self.compare(amount)
if self.player.money == 0:
print("你破产了,游戏结束")
break
elif self.dealer.money == 0:
print("庄家破产了,你赢了")
break
else:
continue
def main() -> None:
print(f"""
{"=" * 20}
游戏规则:
1.目标是让手牌点数尽可能接近21点但不超过
2.A可以算作1或11点,J/Q/K算作10点
3.两张牌总和为21点称为'黑杰克',赔率1.5倍庄家小于17点必须要牌,大于等于17点停牌
4.点数相同为平局,退还赌注
{"=" * 20}
""")
user_name = input("请输入你的名字: ").replace(" ", "") or "玩家"
print(f'欢迎 "{user_name}" 来到21点游戏!')
while True:
raw_money = input("请输入你的初始资金: ")
try:
money = Decimal(raw_money)
except (InvalidOperation, ValueError):
print("请输入有效的金额")
continue
if money <= Decimal("0"):
print("请输入大于0的金额")
continue
break
print(f"你有{money}元")
game = Game(Player(user_name, money))
try:
game.start()
except GameOver as e:
print(e.message)
finally:
print(f"庄家: {game.dealer.show_hand_cards()}")
print(f"玩家: {game.player.show_hand_cards()}")
if __name__ == "__main__":
main()
Python cookbook
更多练习,请访问Python cookbook
如何成为某个领域的专家?选一个你愿意啃数年的主线:比如 Python 后端 / 数据分析 / AI 应用 / 工具链。
持续做 真正有交付结果的东西:开源项目、被人在用的小工具、线上服务、课程/文档。
如果有一天,你遇到了一些Python的BUG,可以去Python的官方仓库为它们提交PR,说明你已经走了很远的路了。