虞颖健 浙江省海宁市第一中学
倪俊杰 浙江省桐乡市凤鸣高级中学
新一轮的课程改革大幅增加了高中信息技术新教材程序设计相关内容的比重,大数据、物联网、人工智能等内容进入了信息技术新版教材,新版教材的编程语言换成了在以上领域应用广泛的Python。Python程序设计教学在新课程的教学中具有举足轻重的地位。程序设计测评系统能够即时检验学生编写的程序代码是否正确,教学需求也日益增强。相比传统的课堂教学模式,采用程序测评系统的程序设计课堂教学能够根据测评系统的实时反馈及时掌握学生的学习情况,进而能更好地调整课堂教学,提升课堂教学的有效性。
在线测评系统(Online Judge,简称OJ系统)是目前广泛使用的主流程序测评系统。OJ系统是一个在线判题系统,用户可以在线提交程序源代码,系统通过预先设计的测试数据来检验程序源代码的正确性。用户提交的程序在OJ系统下执行时将受到比较严格的限制,包括运行时间限制、内存使用限制和安全限制等。用户程序执行的结果将被OJ系统捕捉并保存,然后再转交给一个裁判程序。该裁判程序或者比较用户程序的输出数据和标准输出样例的差别,或者检验用户程序的输出数据是否满足一定的逻辑条件。国内较为著名的OJ系统有POJ(北京大学)、ZOJ(浙江大学)、HDOJ(杭州电子科技大学)。
传统的OJ系统经过多年的发展,虽然功能较为完善,但应用于中小学信息技术课堂教学,存在如下问题:①OJ系统用户提交的程序代码在服务器运行,用户较多情况下对服务器的性能有一定要求,需要配置专门的服务器,不符合大多数学校的实际情况。②OJ系统基于B/S架构,通常需要安装数据库和Web服务器并进行必要的配置和部署;OJ系统用户提交的程序代码在执行时有运行时间、内存使用、安全等的限制,需要在服务器上进行配置。使用OJ系统对信息教师的专业能力要求较高。③OJ系统检验的是用户提交程序代码的正确性,其中只有正确和错误两种结果,无法对程序设计填空的正确性进行检验,而目前不少程序设计考题都为填空题,这就需要测评系统具有程序设计填空码的评测功能。以上问题在一定程度上阻碍了OJ系统在中小学信息技术课堂教学中的推广与使用。
要想让广大中小学信息技术教师能够在课堂教学中使用程序测评系统,提升课堂教学的有效性,需要开发一款适用于当下信息技术课堂教学、使用门槛较低的程序测评系统,该系统需要满足如下需求:①系统无需高性能计算机,无需服务器,在机房学生机或者教师机上即可运行使用。②无需部署数据库与Web服务器,无需烦琐复杂的配置。③既能够检验提交的完整代码的正确性,也能够检验程序设计填空的正确性。
笔者根据浙江信息技术教学情况,设计并实现了Python程序设计测评系统——YYOJ系统。该系统有如下优点:①学生提交的程序代码的正确性在学生使用的本地机器上检验,对运行YYOJ系统数据库和Web服务器的计算机性能没有要求,YYOJ系统可以在机房任意一台计算机上运行,不需要配置专门的服务器,也无需对运行YYOJ系统计算机的内存、安全限制进行配置。②YYOJ系统不需要安装数据库和Web服务器并进行配置,它自带数据库和Web服务器的应用程序。③YYOJ系统除了可以检验学生提交的完整程序代码的正确性,还可以检验程序设计填空的正确性。此外,YYOJ系统对硬件设备要求较低,使用门槛不高且易于使用,特别适合浙江高中信息技术课堂教学。
YYOJ系统是基于B/S架构的Web应用程序,在开发过程中使用了Web框架、Web服务器、数据库等技术。
FastAPI是一个用于构建Web API的现代、高性能的WEB框架,它实现了ASGI规范。虽然FastAPI的主要用途是用于构建Web API,但是FastAPI也提供了对Jinja2、Mako等模板引擎的支持,能够像Flask、Django一样开发基于服务端渲染的Web应用程序。
Uvicorn是使用C和Python编写的ASGI Web服务器。使用Uvicorn部署实现了ASGI规范的Web应用程序。FastAPI实现了ASGI规范,所以,FastAPI开发的Web应用程序能够部署在Uvicorn中。Uvicorn与传统的Web服务器不同,传统的Web服务器如nginx、apache、IIS需要单独安装,Uvicorn不需要安装,并且可以通过Python编码的方式直接嵌入应用程序中运行。
Nuitka是Python编写的优化Python编译器,它能够把Python代码编译生成为可执行程序,而且通过Nuitka编译生成的可执行程序不需要安装Python环境也能够独立运行。Nuitka支持Python2与Python3,支持Windows、Linux与苹果等主流操作系统。Nuitka的工作原理是把Python代码编译成C代码,再把C代码编译成可执行文件,生成的文件不能像.pyc文件一样反编译,因此安全性高,而且因为编译成C代码,所以生成的可执行程序运行速度会更快。
Brython是用javascript编写的Python编译器和解析器。使用Brython,可以在浏览器中编译和运行Python代码。
SQLite是一个C语言库,它实现了一个小型、快速、自包含、高可靠性、功能齐全的SQL数据库引擎。SQLite内置于所有手机和大多数计算机中,并捆绑在人们每天使用的无数其他应用程序中。SQLite不像MySQL、Postgres那样需要开启服务器提供数据库服务,它是无服务器的数据库,可以通过编程语言调用API直接与SQLite数据库通信。
总的来说,YYOJ系统是使用FastAPI开发的Web应用程序,内嵌了Uvicorn和SQLite,Uvicorn用于承载Web应用程序,SQLite作为应用程序的数据存储,因此,省去了安装数据库和Web服务器以及配置的步骤。YYOJ的源码采用Nuitka编译并生成可执行程序,不需要安装Python环境,使用时把可执行程序拷贝到计算机上直接运行即可。判题代码在浏览器端通过Brython运行检验学生提交程序代码的正确性,提交的程序代码无需上传到服务器后再进行判题,减轻了服务器端的压力。
YYOJ系统是一个Python程序设计测评系统,教师在系统中录入Python程序设计的题目及其评判代码,学生登录系统根据题目要求编写或完善程序并提交,系统根据教师提供的评判代码对学生提交的程序进行评判,检验学生程序的正确性。YYOJ系统的功能主要包括学生界面功能、教师界面功能、第三方软件实现的管理功能。
①在线运行。学生可以输入Python代码并运行查看执行结果。该模块输入的Python代码在网页中运行,因此不需要Python编程环境,在浏览器的环境下就可以运行。开发该模块主要是考虑学生如果不在机房或者不在学校,YYOJ系统只要部署在学校的服务器上,学生通过自己的计算机、平板、手机登录YYOJ系统就可以练习Python代码,而无需安装Python编程环境。
②我要挑战。学生可以选择教师录入系统中的题目进行挑战练习,在学生提交Python代码后,系统根据教师的判题代码检验学生提交代码的正确性并反馈结果给学生。学生在课余时间可以使用该模块开展自主编程练习。
③练习反馈。学生登录之后才能使用。在“我要挑战”的基础上还添加了一项额外的功能:学生提交的Python代码检验的结果会保存到数据库中,教师可以在后台查看。
教师功能必须在教师登录后台之后才能使用,教师功能主要包括以下内容:
①题目查看。对系统中已经录入的题目提供分页查看功能。
②录入题目。提供录入题目的功能。录入的题目包括题目名称、需要批阅的代码、批阅代码三块内容。如果代码完全由学生编写,“需要批阅的代码”留空,如果代码是一部分由学生完善,如程序设计填空,则教师在“需要批阅的代码”中录入无需学生完善的代码部分并标记需要完善的部分。“批阅代码”是检验学生代码正确与否的评判代码。
③修改题目。提供修改录入的某个题目的功能。
④删除题目。提供删除某个题目的功能。
⑤查看做题情况。提供以班级为单位查看某个题目的评判结果的功能。
①教师管理。提供查看、添加、修改、删除教师账号的功能。
②班级管理。包括班级的查看、添加、修改、删除功能。
③学生管理。包括学生的查看、添加、修改、删除功能。
考虑到YYOJ系统的主要使用群体是信息技术教师,能够使用数据库管理软件来管理数据库中的数据,并且YYOJ使用的是无需服务器的SQLite数据库,所以以上三项功能并未在系统中实现,教师可以直接使用HeidiSQL或者DB Browser for SQLite这样的SQLite数据库管理工具来实现上述功能。
YYOJ系统通过Brython在浏览器中运行评判代码以检验学生提交代码的正确性。当学生点击提交按钮之后,Brython会将学生提交的代码传递给教师编写的评判代码,并在浏览器中运行评判代码检验学生提交的代码是否正确,最后仅将评判的结果返回给服务器并保存在数据库中。因为评判代码在浏览器中运行,所以减轻了服务器的压力。
YYOJ系统与传统的OJ系统不同,YYOJ系统除了支持完整代码正确性的评判,也支持程序设计填空正确性的评判。程序设计填空的答案通常不是唯一的,为了确保评判的正确性,教师需要编写评判代码,通过评判代码来评判程序设计填空的正确性。在学生提交代码之后,教师编写的评判代码在浏览器中运行评判学生提交代码的正确性,并通过AJAX请求将题目的编号、学生标志以及题目正确与否等信息传回服务端并保存在数据库中,教师在后台能够查看学生的答题情况。
评判代码的编写主要分为完整代码的正确性评判和程序设计填空的正确性评判。
评判完整代码的正确性的方式是检验某些变量的值或者程序的输出在程序运行后是否符合预期。
①根据变量值评判代码正确性。
某些问题会将结果存储到某个变量中,对于这样的问题,需要通过检验变量内存储的值来判断代码的正确性。
例1,描述:在列表s中存储[1, 10]范围内的所有偶数;
题目已有代码:无;
学生提交代码如图1所示。
图1
分析:该问题希望在列表s中存储[1,10]范围内的所有偶数,当程序运行完毕之后,列表s的值为[10,8,6,4,2],要评判提交的代码是否正确,只要判断列表s中是否仅包含2、4、6、8、10这5个偶数即可。
学生提交的代码作为文本字符串存储在变量src中提交给评判代码,评判代码通过exec函数运行学生提交的代码。exec是Python的内置函数,它能够动态地执行存储在字符串或文件中的 Python 语句。
exec(src)
通过上面的语句,学生提交的代码已经在评判代码中运行完毕,在评判代码运行的上下文环境中已经存在变量s且它的值为[10,8,6,4,2]。评判代码只需要判断s的值是否仅包含2、4、6、8、10这5个偶数即可。
在评判代码中引入变量correct,将其赋值为False,假设代码是错误的。通过if语句检验s的值是否仅包含2、4、6、8、10这5个偶数,如果代码正确,correct的值设置为True(如图2)。
图2
②根据输出评判代码正确性。
某些问题需要将结果通过print函数直接输出并显示,对于这样的问题,需要检验输出是否正确。
print函数的工作原理是将数据写入sys.stdout流中,stdout是标准输出流,默认情况下stdout会把数据显示在运行当前Python代码的终端命令行窗口中。
print("你好,世界!")
等价于
import sys
sys.stdout.write("你好,世界! ")
print函数的输出显示在屏幕上,没有办法对print函数的输出进行检验。要评判学生提交的代码是否正确需要对print函数的输出进行捕获并将其放入某个变量内。
如图3所示的代码自定了类MarkingOutput,改写了sys.stdout的默认行为,当执行print函数调用sys.stdout.write的时候,本来在屏幕上输出的数据不再输出到屏幕上,print函数的输出被添加到MarkingOutput的实例out的属性bufs列表中。上述代码默认包含在评判代码中,评判代码要判断提交代码的正确性只需要检查out实例的bufs列表即可。
图3
例2,描述:使用print输出三个变量a、b、c的最大值;
题目已有代码:a, b, c = 2, 4, 6;
学生提交代码如下页图4所示。
图4
分析:学生提交的代码作为文本字符串存储在变量src中提交给评判代码,通过exec执行学生提交的代码。
exec(src)
通过上面的语句,学生提交的代码已经在评判代码中运行完毕。
correct = False
if sys.stdout.bufs[0] == str(max(a, b, c)):
correct = True
在评判代码中引入变量correct,将其赋值为False,假设代码是错误的。本题最终结果仅仅输出的a、b、c三个变量中的最大值,输出的最大值保存在bufs列表的第一个元素中,要判断提交的代码是否正确,只需要判断bufs列表的第一个元素是不是a、b、c三个变量的最大值即可。
③程序结果随输入改变的正确性评判。
某些程序在运行时需要预先给定一个输入,程序代码运行的结果会随着输入的改变而改变,结果并不唯一。该类代码在评判的时候,需要提供多组测试的输入数据,一一检测其程序运行结果是否与预期相符合,如果全部符合则代码正确,否则代码错误。
例3,描述:将十进制自然数n(n是整型)转化为二进制数s(是字符串型)。
题目已有代码:
n = 13
s = ''
学生提交代码,如图5所示。
图5
分析:十进制自然数n转二进制数s,其结果会随着变量n的值的改变而改变,当变量n的值为13的时候,s的值为"1101",但是如果仅仅判断变量s的值为"1101"是无法达到判题效果的,提交的代码中n的值有可能为任意一个整数,如果不是13的情况下仍然判断s的值是否为"1101"将得出错误的判题结果。
此类问题的解决方法是提供一组测试输入值及其对应的结果,用提供的测试输入值来替代原来的输入值,运行代码检验对应的结果是否符合预期,如果提供的每个测试输入值得到的结果都符合预期,则可以认为提交代码是正确的。
对于例3,通过下面的代码随机生成10个测试输入值(如图6)。
图6
学生提交的代码作为文本字符串存储在变量src中,通过如图7所示的代码使用test_input中的每个测试输入来验证代码的正确性。
图7
其中,src_r=src.replace('n =13',f'n={d}')用测试输入值替换学生提交代码中变量n的输入值,exec(src_r)运行修改输入数据后的提交代码,s !=dtob(d)判断提交代码运行的结果是否与提供输入值得到的对应结果一致,只要其中一次检验结果不一致就可以确定提交代码错误,设置correct为False并退出循环。但以上代码有一个缺陷,如果学生修改了“n=13”这一行代码为“n=任意整数”,则上面的判题代码无法得出正确的结果。对于“n=任意整数”的情况,需要使用正则表达式来进行输入值的替换。
正则表达式,又称规则表达式(RegularExpression,在代码中常简写为regex、regexp或RE),是一种文本模式,包括普通字符(如a到z之间的字母)和特殊字符(称为“元字符”),是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串,通常被用来检索、替换那些符合某个模式(规则)的文本。
在Python中使用正则表达式需要引入正则表达式的内置库:
import re
然后将
src_r = src.replace('n = 13', f'n = {d}')
这一行代码修改为:
re.sub('ns*=s*d+', f'n = {d}', src)
其中的sub函数用于替换符合模式的字符串。第一个参数“ns*=s*d+”是要寻找的模式,其中的“s”表示空格,“d”表示数字,“*”表示匹配零到多个字符,“+”表示匹配一到多个字符,“ns*=s*d+”的含义是“n+包含零到多个空格+=+包含零到多个空格+包含一到多个数字”(+号表示连接),第二个参数是要替换为的字符串,第三个参数是需要替换字符串的文本。通过以上代码可知,不管n的值为何整数,都能将其替换为测试输入值。
评判程序设计填空的正确性的方法是先提取填空的程序段,再使用测试数据测试提取的程序段运行能否达到预期的结果。
例4,描述:二进制数b转换成十六进制数h(填空题)。
题目已有代码如图8所示。学生提交代码如图9所示。
图8
图9
分析:程序设计填空的题目需要教师预先提供题目非填空部分的代码,其中标注的“#第1题”“#第2题”是为了方便判题程序提取学生填空的程序代码。
在学生提交代码之后,判题程序会使用正则表达式提取其中填空的程序代码,并利用编写的判题代码判断学生填写的程序代码是否正确。图10所示是针对例4填空2编写的判题代码的核心代码。
图10
re.search函数的作用是找到与正则表达式匹配的字符串,第一个参数中的“(?P
该填空主要功能是将10~15的十进制数字转化成对应的16进制数的A~F。for循环中n的变化范围为10~15。通过exec将填空代码“chr(55+n)”执行结果赋值给变量c,if语句判断c是不是十进制数n对应的十六进制数的大写或小写字母,不是则程序错误,变量ec累加1,退出for循环。for循环退出后变量ec的值为0表示程序正确,设置变量b2_correct的值为True。
随着信息技术(信息科技)课程越来越得到重视,教师对程序设计在线测评系统的需求也愈发强烈。YYOJ系统是根据多年实际教学经验与广大一线信息技术教师的需求设计开发的,该程序设计评测系统除了支持常规的完整程序代码的评判,还支持程序设计填空题的评判,符合当下学考、高考教学的实际。该系统无需安装,无需数据库与Web服务器,只需要一台机房的普通计算机即可零配置快速部署使用,适用于在机房开展的程序设计教学,能有效提升课堂教学的效率。