虞穎健 浙江省海寧市第一中學
倪俊杰 浙江省桐鄉市鳳鳴高級中學
新一輪的課程改革大幅增加了高中信息技術新教材程序設計相關內容的比重,大數據、物聯網、人工智能等內容進入了信息技術新版教材,新版教材的編程語言換成了在以上領域應用廣泛的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=g0gggggg')用測試輸入值替換學生提交代碼中變量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 = g0gggggg')
這一行代碼修改為:
re.sub('ns*=s*d+', f'n = g0gggggg', 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服務器,只需要一臺機房的普通計算機即可零配置快速部署使用,適用于在機房開展的程序設計教學,能有效提升課堂教學的效率。