python函数版ATM

最近系统的学习python函数知识点,感觉在面向对象之前,函数的功能确实强大。

最近使用函数写了ATM项目,虽然需求简单但也有很多知识点需要注意,这个项目把python基础的很多知识点都用上了。

前前后后,完整的写出整个项目的所有需求差不多也有四五次吧,每次写完都会有一些新的东西冒出来。

感觉是时候该总结一下,不然迷迷糊糊不知道学了些什么。

整个项目中肯定还有一些隐藏的bug,或者还有一些可以优化的地方。但是整体思路,我觉得还是不错的。

如果互联网面前的你看到这个项目,有啥意见或建议,不妨直说,欢迎留言讨论。

项目需求

# 编写ATM程序实现下述功能,数据来源于文件db.txt
1、注册功能:注册信息按照固定的格式存入文件db.txt
2、登录功能:用户名存在则登录&密码输错三次锁定账号,登录成功后记录下登录状态

# 下述操作,要求登录后才能操作
1、充值功能:用户输入充值钱数,db.txt中该账号钱数完成修改
2、转账功能:用户A向用户B转账1000元,db.txt中完成用户A账号减钱,用户B账号加钱
3、提现功能:用户输入提现金额,db.txt中该账号钱数减少
4、查询余额功能:输入账号查询余额

运行环境

# win10
# python3.8
# pycharm2019

项目知识点

  • 字典、列表等python基本数据类型
  • python流程控制
  • 字符编码、文件读写操作
  • 函数和函数对象
  • 名称空间和作用域
  • 装饰器

实现思路

# 既然是atm,就先定义一个函数atm(),atm函数作为主程序,是整个项目的切入点;然后在atm函数中调用相关功能(如,登录、注册、转账等);

# 单独定义登录、注册、查询余额、提现、转账、充值等函数,分别实现相关功能;

# 很多功能都会涉及到判断当前用户是否登录,所以可以定义一个单独的登录校验函数,判断用户是否登录;

# 可以定义一个全局变量来记录用户的登录状态;这样做的好处是程序中任何功能(函数)都可以访问到它,方便判断登录状态;

# 因为需要永久保存用户的登录、注册信息、金额等数据,所以需要将所有相关数据保存到硬盘文件中。
# 这就涉及到文件的读写操作,几乎每个功能都涉及读写操作,所以还需要定义读写相关的函数,方便多次调用;

# 为了方便操作更新用户信息,避免直接操控硬盘文件读写的麻烦,在程序运行开始后立马将文件db.txt内用户信息读入内存,保存在字典中;每个涉及金额操作函数(如:查询余额)都可以使用该字典,方便访问;

# 一旦某个用户的信息更新了,同步更新all_accouts字典和db.txt内用户数据;这样做的好处就可以将用户信息及时保存到硬盘,又可以通过操作字典方便下一次用户信息的修改。


程序流程图


项目框架

############################################ 工具类函数
def file2dict():
    pass

file2dict函数功能:将硬盘db.txt文件内用户信息读入内存字典中,也就是atm函数中的all_accounts中保存;
all_accounts字典中用户名做字典的key,value是一个列表,存放用户的密码、失败登录次数、余额
"""
all_accounts = {
	'egon': ['1234', 0, 100],
	'alex': ['1234', 0, 100],
	......
}
"""

def dict2file(all_accounts):
    pass
dict2file函数功能:当内存中字典all_accounts中某个用户的信息更新后,及时将数据同步保存到硬盘上;
此函数会将all_accounts中的所有数据按一定格式保存到'db.txt.swap'临时文件中,删除旧的'db.txt',再重命名'db.txt.swap'为'db.txt';这样做的目的是为了避免数据丢失。db.txt内每行数据格式,如:'egon:1234:0:100\n'


def append2file(new_user_info):
    pass
append2file函数功能:用户注册账号时,仅仅需要在'db.txt'文件末尾新增一行,用到此函数


def check_login(func):
    pass
check_login函数功能:判断当前操作的用户是否登录,可以设计成装饰器,装饰每一个func,如查询余额

############################################ 功能类函数
def login(all_accounts):
   pass

def logout(*args, **kwargs):
   pass

def register(all_accounts):
    pass

def check_balance(all_accounts):
   pass

def recharge(all_accounts):
  pass

def withdraw(all_accounts):
    pass

def transfer(all_accounts):
    pass

############################################ 全局变量
current_user = []
全局变量current_user记录当前登录用户信息,初始为空列表,用户登录成功后该列表内存放:用户名|密码|失败登录次数|余额
如:current_user = ['egon', '1234', 0, 100]
本质上该变量可以为任意数据类型,但最好的是可变数据类型;若是不可变类型(如str)则在功能类函数内使用它时必须使用global声明该变量是全局的;



############################################ 主函数atm
def atm():
    all_accounts = {}
   pass

############################################ 运行程序
if __name__ == '__main__':
    atm()

具体函数实现

atm函数

atm函数是整个程序的主函数,应该有两个功能:

  • 提供用户选择使用具体功能(登录、查询余额等)的接口
  • 为每个具体功能,即每个功能函数提供数据支撑,比如用户的信息

注:因为程序只有一个全局变量current_user,此处将它和atm函数放在一块

# 此处为全局变量,项目中的任何函数都可以访问到
current_user = []

def atm():
    # 功能1:
    # 功能函数集合,函数名放在字典的value中通过key直接找到函数;添加新功能时,只需新增字典的键值对,便于项目维护
    cmd_dict = {
        '1': ('登录', login),
        '2': ('注册', register),
        '3': ('查询余额', check_balance),
        '4': ('充值', recharge),
        '5': ('提现', withdraw),
        '6': ('转账', transfer),
        '7': ('退出', logout),
    }
    # 功能2: 
    # 调用工具类函数将'db.txt'内容读入字典
    all_accounts = file2dict()
    
    print('欢迎使用笨笨ATM'.center(50, '-'))
    while 1:
        # 展示具体功能列表,让用户选择
        for k, v in cmd_dict.items():
            print(f'({k}){v[0]}', end='\t')

        cmd = input('\n小主人,请选一个数字编号:').strip()
        if cmd not in cmd_dict:
            print('小主人,别闹听话')
            continue

        # 下面是输入合法的情况:
        #因为使用了函数对象,通过key直接找函数名,加括号就调用,避免使用if-else判断的繁琐,整个程序简洁优美;
        # 但是这样做,需要每一个功能函数的形参统一,这是它不足的地方;但这样的统一约束感觉却更pythonic
        func = cmd_dict.get(cmd)[1]
        func(all_accounts)

        
# 运行程序
if __name__ == '__main__':
    atm()

工具类函数

工具类函数有四个,三个是硬盘文件读写操作的,一个是登录状态检验的;

# 将硬盘文件用户信息读到内存字典中保存,atm函数中调用该函数一次性赋值给字典all_accounts
def file2dict():
    all_accounts = {}
    with open('db.txt', 'rt', encoding='utf8') as f:
        for line in f:
            if not line:	# 避免'db.txt'中有空行
                continue
            name, pawd, count, balance = line.strip().split(':')
            all_accounts[name] = [pawd, int(count), int(balance)]
    return all_accounts


# 此处形参将接收atm中字典all_accounts,每个功能类函数更新该字典后调用dict2file,将数据写到硬盘文件'db.txt'
def dict2file(all_accounts):
    import os
    with open('db.txt.swap', 'wt', encoding='utf8') as f:
        for k, v in all_accounts.items():
            user_info = f'{k}:{":".join(map(str, v))}\n'	# 如,'egon:1234:0:100'
            f.write(user_info)
    os.remove('db.txt')
    os.rename('db.txt.swap', 'db.txt')					


# 注册时将新的用户信息追加到硬盘文件,次处形参new_user_info接收注册函数中的一个元组实参:(name, password)
def append2file(new_user_info):
    with open('db.txt', 'at', encoding='utf8') as f:
        f.write(f'{":".join(new_user_info)}:0:0\n')


# 检查登录(装饰器)
def check_login(func):
    def inner(*args, **kwargs):
        if not current_user:		# 访问的是全局的current_user
            print('小主人,请先登录好吧')
            return
        else:
            func(*args, **kwargs)
    return inner

# check_login函数服务四个功能函数(查询、提现、充值、转账),用于判断用户是否登录;
# 因为能访问到全局变量current_user,所以可以很方便的利用装饰器的原理服务每一个功能类函数。

# 项目初期是将current_user定义在atm函数中,然后通过传参的方式给每个功能函数;
# 这样实现check_login函数功能也很简单,但达不到简化程序的目的,于是将current_user放在全局,利用装饰器实现check_login函数的功能

功能类函数

功能类函数有一些比较近似,这里挑典型的介绍:登录函数、转账函数等

登录:login(all_accounts)

def login(all_accounts):
    print('当前是登录界面'.center(50, '-'))
    tag = True
    while tag:
        name = input('请输入用户名:').strip()
        pawd = input('请输入密码:').strip()

        if name not in all_accounts:
            print('用户不存在,请核实')
            continue

        if all_accounts[name][1] == 3:
            print('该账号已冻结,请联系客服')
            continue

        if pawd == all_accounts[name][0]:
            # 登录成功,重置登录失败次数
            all_accounts[name][1] = 0
            # 保存当前用户信息
            current_user[:] = [name, pawd, 0, all_accounts[name][-1]]
            dict2file(all_accounts)
            print(f'登录成功,欢迎【{name}】')
            break

        # 下面是密码输错的情况
        all_accounts[name][1] += 1  # 登录错误次数累加
        s = f'密码错误,还有【{3-all_accounts[name][1]}】次登录机会'
        if all_accounts[name][1] == 3:
            s = f'密码输错三次,账号【{name}】已被冻结,解锁请联系客服'
            tag = False

        print(s)
        dict2file(all_accounts)
        
        
# 登录成功后,需要更新全局变量:current_user[:] = [name, pawd, 0, all_accounts[name][-1]]
# 此处通过切片赋值的方式,修改的是全局变量current_user
# 需要注意如果写成:current_user = [name, pawd, 0, all_accounts[name][-1]],这是定义了一个局部的变量而没有更新全局的current_user,会有bug

# 登录失败时,全局current_user还是空的,不能累加该列表中的失败次数,如current_user[2] += 1,这样会报错索引越界

# 登录成后需要重置用户登录失败次数,这表示用户连续登录失败三次锁定账号,只要登录成功则登录失败次数就清零
# 不管登录成功还是登录失败都要及时将数据保存到硬盘就是避免下次该用户登录时因为登录失败次数的问题出bug

# 待优化的地方:
# 比如用户进入登录界面后,发现没有账号想要退出去注册时,只能关闭程序重新进入;
# 这种情况下可以在账号名不存在是让用户选择继续登录还是去注册,在此处调用register()函数

退出函数不需要使用任何形参,但为了atm函数中统一调用的关系,此处写成可变长度的形参形式

def logout(*args, **kwargs):
    choice = input('小主人,要离开了吗(y/n):').strip()
    if choice == 'y':
        print('欢迎再次再来玩,拜拜')
        exit()

转账:transfer(all_accounts)

@check_login
def transfer(all_accounts):
    print('当前是转账界面'.center(50, '-'))
    while 1:
        other = input('请输入对方账号:').strip()
        
        if other not in all_accounts:
            print('对方账号不存在,请核实后再转账')
            continue
            
        money = input('请输入转账金额(整数):').strip()
        if not money.isdigit():
            print('小主人,钱的事要认真')
            continue
            
        money  = int(money)
        if money <= 0:
            print('小主人,别闹了')
            continue
        if money > current_user[-1]:
            print('抱歉,小主人你的口袋余额不足,无法转账')
            continue

        confirm_password = input('请输入账号密码:').strip()
        if confirm_password != current_user[1]:
            print('密码错误,交易取消')
            break
        # 下面是可以转账的情况
        print(f'小主人,您将给【{other}】,转账【¥{money}元】')
        is_confirm = input('确认此次转账交易(确认请按y/取消按任意键):').strip().lower()
        if is_confirm == 'y':
            current_user[-1] -= money
            all_accounts[current_user[0]][-1] -= money
            all_accounts[other][-1] += money
            dict2file(all_accounts)
            print('转账成功')
            break
        else:
            return

# 因为是转账,所以该函数内判断较多,严谨起见;
# 该函数因为使用了all_accounts字典,多以很容易判断转账对方账号other是否存在;
# 用户输入符合转账要求时,需要同时扣除自己账号的余额,增加对方账号的余额,更新all_accounts并回写到文件'db.txt'

总结

  • ATM的项目关键是要注意函数相互调用和函数间共享数据(current_user & all_accounts)
  • 项目整体思路清晰,需要定义多个工具类函数,方便重复调用,维护代码的简洁清晰
  • 为了避免在功能函数中直接操控文件读写,程序运行之处将用户信息读到内存字典中,借助字典的优势方便数据更新
  • 利用函数对象的概念,将函数存放在字典中,简化atm函数中功能类函数的if-else判断选择问题
  • 利用装饰器,实现某些功能类函数的登录状态检验需求

全局变量curent_user列表

当前项目中,current_user这个全局列表记录用户登录的信息,存放了当前用户的用户名、密码、登录失败次数和余额;其实就当前项目需求来说,该列表只要存放用户的名字即可;存放该用户其他信息是多余的(因为所有功能类函数只需通过all_accounts字典和current_user中的用户名即可完成各自的功能)。所以就目前需求来看可以将各个功能类函数中有关current_user的代码去掉也无妨(check_balance函数因为写的比较简单,目前只使用了current_user查询余额,也可以修改为使用all_accounts字典和current_user中的用户名完成查询余额操作)


项目源文件

链接:https://pan.baidu.com/s/1bRPeJ_49UrTGo0lfFuv4jQ
提取码:cc0m

posted @ 2020-03-21 21:55  the3times  阅读(661)  评论(0编辑  收藏  举报