VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > 简明python教程 >
  • 简明python教程之总结:ATM项目中函数字典自动添加的bug

本站最新发布   Python从入门到精通|Python基础教程
试听地址  
https://www.xin3721.com/eschool/pythonxin3721/


介绍#

本文以ATM项目为背景,介绍一个比较实用的编程技巧,使用装饰器将项目中的指定函数添加到字典中。

利用字典通过key访问value的特点,实现用户输入编号,通过字典直接获取并调用编号对应的功能函数。


Copy
# 实现的目标:让用户输入使用功能的编号,程序调用相应的函数。

基础版-if条件判断#


Copy
def login(): print('this is login function') def register(): print('this is register function') def transfer(): print('this is transfer function') # 主程序 def atm(): desc = '1: 登录\n2:注册\n3:转账' while 1: print(desc) cmd = input('请选择您要使用的功能编号:').strip() if cmd == '1': login() elif cmd == '2': register() elif cmd == '3': register() else: print('编号不存在,请重新选择') if __name__ == '__main__': atm() # 基础版本,每个数字字符对应不同的函数,通过if-elif-else判断用户的选择,然后调用。 # 优点:简单明亮,思路清晰 # 缺点:代码重复,程序臃肿

提高版-函数字典#


Copy
# 功能函数略....... # 主程序 def atm(): # 函数字典 cmd_func = { '1': ('登录', login), '2': ('注册', register), '3': ('转账', transfer), } while 1: for k, v in cmd_func.items(): print(f'({k}){v[0]}', end='\t') cmd = input('\n请选择您要使用的功能编号:').strip() if cmd not in cmd_func: print('编号不存在,请重新选择') continue # 编号存在字典中的情况 func = cmd_func.get(cmd)[1] # func是cmd_func字典中value的第二个元素 func() # 调用函数 if __name__ == '__main__': atm() # 提高版本,将每个功能函数放在一字典中,key是编号,值中包含有函数名,输入key就可以获取到值中的功能函数 # 优点:思路清晰,代码简洁,程序优美 # 缺点:需要手动创建一个函数字典

进阶版-自动生成函数字典#


Copy
# 装饰器 def auto(desc): from functools import wraps def wrapper(func): @wraps(func) def inner(*args, **kwargs): index = len(cmd_func) + 1 # 调用全局函数字典(初始为空),自动计算编号 cmd_func[str(index)] = (desc, func) # 将编号,描述信息、函数加到字典中 return inner return wrapper @auto('登录') def login(): print('this is login function') @auto('注册') def register(): print('this is register function') @auto('转账') def transfer(): print('this is transfer function') # 自动调用被装饰函数的函数,需要手动排除全局名称空间中不需要的名字 def auto_append(): my_func = [v for k, v in globals().items() if callable(v) if k not in ['atm', 'auto', 'auto_append']] for func in my_func: func() # 函数字典 cmd_func = {} # 主程序 def atm(): # 调用函数自动添加函数字典 auto_append() while 1: for k, v in cmd_func.items(): print(f'({k}){v[0]}', end='\t') cmd = input('\n请选择您要使用的功能编号:').strip() if cmd not in cmd_func: print('编号不存在,请重新选择') continue # 编号存在字典中的情况 func = cmd_func.get(cmd)[1] # func是cmd_func字典中value的第二个元素 func() # 调用函数 if __name__ == '__main__': atm() # 进阶版-编写装饰器,每个被装饰的函数调用,就会被添加到一个字典中,然后再写一个自动发现这些他们的函数,自动调用 # 优点:使用字典函数时不需要手动添加 # 缺点:需要字典是全局变量,cmd_func = {},手动排除不需要的函数,功能函数少时提现不出它的优势 # 上面的情况是全部函数都在一个文件中; # 如果功能函数在其他文件中,笔触采用导入的方式运行,这种情况使用自动添加函数字典就优势明显。

高级版-跨文件自动添加#

这里使用简化版的软件开发目录规范,将不同功能的函数放在不同的文件中,并且所有的文件在一个文件夹下。


Copy
ATM/ |-- run.py # 程序启动文件 |-- atm.py # 主函数文件,即atm()函数 |--funcs.py # 存放 login()、register()、transfer()函数的文件 |--tools.py # 存放装饰器函数auto()、auto_append()函数的文件

run.py


Copy
from atm import atm # 导入atm.py下的atm()函数 if __name__ == '__main__': atm() # 运行atm()函数

atm.py


Copy
from tools import auto_append # 从tools.py导入auto_append函数 # 函数字典,将login、register、transfer自动加到这个字典中 cmd_func = {} # 主程序 def atm(): auto_append() # 调用auto_append函数自动添加函数字典 while 1: for k, v in cmd_func.items(): print(f'({k}){v[0]}', end='\t') cmd = input('\n请选择您要使用的功能编号:').strip() if cmd not in cmd_func: print('编号不存在,请重新选择') continue # 编号存在字典中的情况 func = cmd_func.get(cmd)[1] # func是cmd_func字典中value的第二个元素 func() # 调用函数

funcs.py


Copy
from tools import auto # 从tools.py导入auto装饰器 @auto('登录') # 装饰login def login(): print('this is login function') @auto('注册') # 装饰register def register(): print('this is register function') @auto('转账') # 装饰transfer def transfer(): print('this is transfer function')

tools.py


Copy
from functools import wraps def auto(desc): # 装饰器 from atm import cmd_func # 函数内导入,避免循环导入问题 def wrapper(func): @wraps(func) def inner(*args, **kwargs): index = len(cmd_func) + 1 # 访问atm.py文件的cmd_func字典 cmd_func[str(index)] = (desc, func) # 将函数自定添加到该字典中 return inner return wrapper def auto_append(): # 在当前局部名称空间中导入需要添加到字典里面的函数 from funcs import login, register, transfer my_funcs = locals() for func in my_funcs.values(): func() # 自动执行,即执行装饰器内的inner函数,自动添加到字典

运行run.py,程序结果界面如下,很完美对吧。通过上述操作,我们实现了自动将功能函数添加到字典中。

不过值得注意的有如下几点:

  • 我们是将字典cmd_func放在了atm.py文件的全局名称空间中,函数装饰器auto在使用它的时候需要从atm.py文件中导入cmd_func。此处需要注意的是循环导入问题。这种情况下解决循环导入问题有两中方案。方案1:在auto函数内导入from atm import cmd_func方案2:tools.py文件顶部导入 import atm,auto函数内使用atm.cmd_func

  • 如果将字典cmd_func放在一个独立的py文件中,而不是放在atm.py文件中,那就很方便了,只要在需要它的位置导入并引用就好了,不会造成循环导入问题。

  • 函数auto_append内导入login、register、transfer函数,局部名称空间的名字。


补充#

说到这里就可以结束了,因为上面的操作很流畅的实现了我们目的。这种操作在功能数量多,跨文件导入时,非常实用。

在刚要结束的时候,我突然想试试,如果不通过run.py文件调用atm函数,而是直接运行atm.py文件,调用atm函数,是不是也可以?于是,在atm.py文件末尾增加if __name__ == '__main__': atm()


Copy
from tools import auto_append # 从tools.py导入auto_append函数 # 函数字典,将login、register、transfer自动加到这个字典中 cmd_func = {} # 主程序 def atm(): auto_append() # 调用auto_append函数自动添加函数字典 while 1: for k, v in cmd_func.items(): print(f'({k}){v[0]}', end='\t') cmd = input('\n请选择您要使用的功能编号:').strip() if cmd not in cmd_func: print('编号不存在,请重新选择') continue # 编号存在字典中的情况 func = cmd_func.get(cmd)[1] # func是cmd_func字典中value的第二个元素 func() # 调用函数 if __name__ == '__main__': # 增加了这个判断,不影响该文件被导入执行 atm()

执行atm.py文件,调用atm函数,结果。。。。。字典居然是空的

也就是说,我的这些操作,就是啥也没做。但我明明做了呀。

这个问题,我研究了半天愣是没想明白。后来跟着debug调试,后来发现其实原因很简单。

使用run.py文件导入atm函数这种方式执行atm函数时

被导入的模块atm已经在导入模块的时候会执行atm.py文件,执行完毕后atm.py模块名称空间里面就有了一个名字cmd_func这个字典。

后面执行atm函数内部的auto_append函数,会进入装饰器auto内部,装饰器内部遇到from atm import cmd_func时会再次导入atm模块,但因为atm模块已经被加载到内存了,所以此时不会再执行atm.py文件,而是直接在内存中找到了cmd_func这个名字。后面装饰器内部使用的都是这个相同的字典,即atm.py文件内的字典。

直接执行atm.py文件,调用atm函数的方式情况却不同。

执行atm.py文件,程序走到cmd_func = {}处,程序会为这个字典开辟一个内存空间。

后面执行atm函数,进入atm函数内部,遇到auto_append(),就会跳转到auto_append函数内部。

auto_append内部因为导入login,会进入funcs.py文件内,由于装饰器的存在,程序又会进入装饰器auto函数内。

auto内就比较关键了,因为要使用cmd_func字典,程序需要再次从atm.py文件导入这个字典。

现在,内存中是没有atm这个模块的(因为还在执行这个文件)。根据导入模块时的搜索路径(内存、内置、sys.path),此时就会再次执行atm.py文件,遇到cmd_func={},就会再次在内存中开辟一块空间存这个字典。

所以我们在装饰器中使用的字典是第二次生成的字典,而atm函数内部使用的字典却是第一次生成的那个字典。他们在不同的作用域里面。

所以,这个问题的关键在于:两种方式导入atm.py这个模块的先后顺序不同,造成第一种方式整个程序使用的是一个字典;而第二方式开辟了两个字典,我们操作的是后来的第二个字典,而程序使用的却是第一个

核心知识点是模块导入时的搜索顺序:内存、内置、sys.path路径

作者: the3times


相关教程