使用exec()运行学生代码时,变量与对象是否会引发环境干扰?
我是计算机科学导论课程的助教,布置了一个彩票猜数作业:学生需要通过命令行参数输入两个猜测数,匹配程序随机生成的两个数。我写了一段Python脚本批量测试学生的代码输出和用例,但教授提出担忧:单次运行学生代码产生的变量和对象会在脚本运行期间留存于环境中,可能导致副作用,影响后续其他学生代码的运行或掩盖问题。请问这个担忧确实会成为实际问题吗?
我的测试脚本代码如下:
import os import random import sys import re guessList = [(2,5), (7, 2), (3, 10), (8, 2), (9, 5), (3, 1), (8, 9)] for root, dirs, files in os.walk(".", topdown=False): # iterate through each folder in directory for name in dirs: student = os.path.join(root, name) student_name = re.search('([a-zA-Z]{2,}) ([a-zA-Z]{2,})', student).group() print("\nStudent:", student_name) print("-----------------------------------------------") # reset seed: nums generated: 10 1 7 8 10 1 4 8 8 5 3 1 9 8 random.seed(10) # loop through test cases for guess in guessList: # input sys.argv = ['', guess[0], guess[1] ] # run student's script try: exec(open(f"{student}/Lottery.py").read() ) except Exception as e: print("ERROR HAS OCCURED:") print(e) break print("-----------------------------------------------\n")
是的,教授的担忧完全合理,这确实会成为实际问题,具体风险点如下:
全局变量污染:
exec默认在当前脚本的全局命名空间执行学生代码。如果学生的Lottery.py定义了全局变量、修改了内置对象(比如给list添加自定义方法),这些变更会留存到后续测试环境中。比如前一个学生的代码修改了random模块的运行状态(即便你重置了seed,若学生代码手动调整seed或多次调用随机函数),下一个学生的随机数生成逻辑会被直接干扰。函数/类定义冲突:若多个学生的代码定义了同名函数或类,后运行的代码会覆盖之前的定义。比如学生A定义了
def check_guess(): ...,学生B也用了同名函数,测试学生B时实际执行的可能是学生A的函数,导致测试结果完全失真。系统级状态篡改:如果学生代码修改了
sys模块属性(比如sys.stdout)、全局异常处理钩子,会导致后续测试的输出、错误逻辑异常,甚至整个测试脚本崩溃。同学生用例间干扰:哪怕是同一个学生的多轮测试,前一次运行留存的全局变量(比如记录猜测次数的变量)也会导致后续用例逻辑错误。
解决方法
要彻底规避问题,需为每个学生/用例创建独立运行环境,推荐优先级如下:
1. 使用subprocess启动独立进程(最稳妥)
每个学生代码都在单独的Python进程中运行,完全隔离环境,还能精准模拟学生本地运行场景:
import subprocess import os import sys import re guessList = [(2,5), (7, 2), (3, 10), (8, 2), (9, 5), (3, 1), (8, 9)] for root, dirs, files in os.walk(".", topdown=False): for name in dirs: student = os.path.join(root, name) student_name = re.search('([a-zA-Z]{2,}) ([a-zA-Z]{2,})', student).group() print("\nStudent:", student_name) print("-----------------------------------------------") for guess in guessList: try: # 启动独立进程运行学生脚本,传递参数 result = subprocess.run( [sys.executable, f"{student}/Lottery.py", str(guess[0]), str(guess[1])], capture_output=True, text=True ) # 打印学生脚本的正常输出 if result.stdout: print(result.stdout.strip()) # 打印错误输出 if result.stderr: print("ERROR OUTPUT:", result.stderr.strip()) except Exception as e: print("ERROR HAS OCCURED:") print(e) break print("-----------------------------------------------\n")
2. 给exec指定独立命名空间(轻量但有局限)
如果不想启动新进程,可以每次运行时创建全新的命名空间,但无法完全隔离模块级状态(比如random、sys的全局修改):
# 在原脚本的exec处修改 for guess in guessList: sys.argv = ['', guess[0], guess[1]] try: # 创建独立的命名空间,避免污染全局环境 local_ns = {} global_ns = {} exec(open(f"{student}/Lottery.py").read(), global_ns, local_ns) except Exception as e: print("ERROR HAS OCCURED:") print(e) break
总结:优先选择subprocess方案,既能彻底消除环境污染风险,也能保证测试结果与学生实际运行场景一致。
内容的提问来源于stack exchange,提问作者j0llyw0lly

