跳转至

Python自动评分器

此文件记录了 prairielearn/grader-python Docker 映像中包含的默认 Python 自动分级器。有关如何设置外部评分器的一般信息,请查看 外部评分 文档。

设置

info.json

问题应该首先设置启用外部评分,并在info.json设置中设置"gradingMethod": "External"。要使用本文档中详细介绍的特定 Python 自动评分器,应将 "externalGradingOptions" 字典中的 "image" 设置为 "prairielearn/grader-python"。不需要提供 entrypoint 属性。

完整的 info.json 文件应类似于:

info.json
{
  "uuid": "...",
  "title": "...",
  "topic": "...",
  "tags": ["..."],
  "type": "v3",
  "singleVariant": true,
  "gradingMethod": "External",
  "externalGradingOptions": {
    "image": "prairielearn/grader-python"
  }
}

用户变量

该问题必须指定将从学生代码中读取的变量用于对 (names_from_user) 进行评分。或者,问题还可以指定可从问题的设置代码 (names_for_user) 中供学生代码使用的变量。用户只能通过设置代码访问 names_for_user 中列出的变量或函数;用户代码中的测试用例只能访问 names_from_user 中列出的名称。

每个变量必须具有以下内容:

  • name:学生代码中变量的名称。
  • description:人类可读的变量描述。
  • type:人类可读的变量类型描述。这仅用于显示目的,不影响分级。

有两种方法可以指定这些变量。

===“question.html

The `<pl-external-grader-variables>` element can be used to both specify and display a table of variables.

```html title="question.html"
<pl-external-grader-variables params-name="names_for_user">
  <pl-variable name="n" type="integer">
    Dimensionality of $\mathbf{A}$ and $\mathbf{b}$.
  </pl-variable>
  <pl-variable name="A" type="numpy array (shape $n \times n$)"> Matrix $\mathbf{A}$. </pl-variable>
  <pl-variable name="b" type="numpy array (length $n$)"> Vector $\mathbf{b}$. </pl-variable>
</pl-external-grader-variables>

<pl-external-grader-variables params-name="names_from_user">
  <pl-variable name="x" type="numpy array (length $n$)">
    Solution to $\mathbf{Ax}=\mathbf{b}$.
  </pl-variable>
</pl-external-grader-variables>
```

===“server.py

Using `<pl-external-grader-variables>` in `question.html` is the recommended way to specify user variables. However, if you need to dynamically generate the lists of variables or have more advanced needs, you can also do so directly in `server.py` by assigning to `data["params"]["names_for_user"]` and `data["params"]["names_from_user"]`. The following example is equivalent to the previous `question.html` example:

```python title="server.py"
def generate(data):
    data["params"]["names_for_user"] = [
        {"name": "n", "description": r"Dimensionality of $\mathbf{A}$ and $\mathbf{b}$.", "type": "integer"},
        {"name": "A", "description": r"Matrix $\mathbf{A}$.", "type": "numpy array"},
        {"name": "b", "description": r"Vector $\mathbf{b}$.", "type": "numpy array"}
    ]
    data["params"]["names_from_user"] = [
        {"name": "x", "description": r"Solution to $\mathbf{Ax}=\mathbf{b}$.", "type": "numpy array"}
    ]
```

You can still use `<pl-external-grader-variables>` to display the variables to students; just omit the nested `<pl-variable>` elements.

```html title="question.html"
<pl-external-grader-variables params-name="names_for_user"></pl-external-grader-variables>
<pl-external-grader-variables params-name="names_from_user"></pl-external-grader-variables>
```

question.html

问题应包含学生提交文件的方式,例如 <pl-file-editor><pl-file-upload> 或工作区。问题还应包含 <pl-external-grader-results> 元素以显示评分结果。这些分别放置在问题面板和提交面板中。还建议在提交面板中放置 <pl-file-preview> 元素,以便学生可以看到他们以前提交的代码。下面给出了一个示例问题标记:

question.html
<pl-question-panel>
  <pl-file-editor file-name="user_code.py" ace-mode="ace/mode/python"></pl-file-editor>
</pl-question-panel>

<pl-submission-panel>
  <pl-external-grader-results></pl-external-grader-results>
  <pl-file-preview></pl-file-preview>
</pl-submission-panel>

默认情况下,评分器将查找名为 user_code.py 的可评分文件,但这可以在测试套件中更改。

还可以使用 <pl-external-grader-variables> 元素向用户显示提供的和/或预期的变量。通过将 params-name 属性设置为 names_for_usernames_from_user,可以显示任意一组变量。

tests/setup_code.py

该文件在运行任何参考或学生代码之前执行。 names_for_user 中定义的任何变量都可以在学生代码中从此处访问,而参考答案可以不受限制地自由访问变量。对于学生代码和参考代码,此文件中的代码总共仅运行一次。如果您需要在每个学生和参考代码之前运行一些代码(例如,设置随机种子),您可以定义函数 def repeated_setup(),该函数将在每个学生和参考代码之前执行。 repeated_setup() 将始终在安装代码本身运行后运行。

data中的服务器参数可以通过该文件中的data来访问。

tests/test.py

每个编码问题的测试用例被定义为 test.py 文件中包含的 Test 类的方法。该课程将从 PLTestCasePLTestCaseWithPlot 开始,具体取决于您是否需要学生提供绘图:

tests/test.py
from pl_unit_test import PLTestCase, PLTestCaseWithPlot

# No plot grading
class Test(PLTestCase):
  pass

# Plot grading enabled
class Test(PLTestCaseWithPlot):
  pass

这些类本身扩展了 unittest.TestCase,因此其中的任何功能也可用。

每个测试用例都是类中的一个单独的方法,这些函数的名称必须以 test_ 为前缀,但后面的任何名称都是任意的。测试用例由 unittest 库按排序顺序运行,因此可以使用的一种约定是为测试提供数字名称以确保其运行顺序。

向测试用例添加名称和点值是通过 python 装饰器完成的:@points(val)@name("name of the test case")。这些将控制向学生显示的案例名称和授予的分数。定义的一个例子:

tests/test.py
from pl_unit_test import PLTestCase
from pl_helpers import name, points

class Test(PLTestCase):
  @points(10)
  @name("Check basic math")
  def test_0(self):
    assert 1 == 1

在测试用例实现中,学生答案变量和参考答案变量可以分别作为元组 self.stself.ref 的子项进行访问。有各种辅助函数来检查不同类型变量的正确性,这些函数在 code_feedback.py 中定义。这些内容取自 RELATE 评分器,因此对于那些具有 RELATE 经验的人来说可能会很熟悉。

在测试用例结束时,使用 feedback.set_score() 设置答案的正确性。该函数采用 0 到 1 (inclusive) 之间的浮点数,0 表示完全正确,1 表示完全正确。默认情况下,如果没有给出分数,测试用例将被标记为不正确。

测试用例的整体结构应该类似于:

tests/test.py
from pl_unit_test import PLTestCase
from pl_helpers import name, points
from code_feedback import Feedback

class Test(PLTestCase):
  @points(10)
  @name("name of the test case")
  def test_0(self):
    if Feedback.check_scalar("name of the variable", self.ref.variable_name, self.st.variable_names):
        Feedback.set_score(1)
    else:
        Feedback.set_score(0)

请注意,Feedback.set_score() 用于将测试用例的正确性设置在 01 之间,然后将其乘以测试用例授予的点数。例如,如果测试用例价值 10 分,并且运行 Feedback.set_score(0.5),则学生将获得 5 分。

可以使用 self.data 从测试用例内访问 data 中的服务器参数。

请参阅代码反馈文档,获取可以帮助您检查不同类型变量的正确性的函数列表。

前导和尾随代码

如果可选文件 tests/leading_code.py 和/或 tests/trailing_code.py 存在,自动评分器将在评分前自动将内容添加到用户提交的内容中。如果使用其他输入方法,例如,这可能很有用。对于 Parson 的问题提供 python 导入或其他必须运行的代码。

使用__name__ == "__main__" { #using-name-main }

默认情况下,学生的代码不会在 __name__ 设置为 "__main__" 的情况下执行,而作为 Python 脚本执行的 Python 脚本通常会出现这种情况。这允许学生使用如下代码在本地环境中测试和调试他们的代码,而不影响自动评分器功能:

Student handout they are editing
if __name__ == "__main__":
    # Student's own debugging code

另一方面,如果问题要求学生编写检查 __name__ 值的代码,则可以通过添加包含以下内容的 tests/leading_code.py 文件 来实现:

tests/leading_code.py
__name__ = "__main__"

多次迭代

通过设置 total_iters 类变量,测试套件可以运行多次迭代。为了防止特定的测试用例多次运行,您可以向其添加 @not_repeated 装饰器。

代码反馈

代码反馈库包含用于检查各种数据类型的正确性的内置函数。以下是它们的非详尽列表,有关更完整的参考,请参阅自动生成的代码文档GitHub 上的源文件。请注意,所有函数都会对用户输入执行某种健全性检查,并且如果学生未定义输入变量,则不会失败。

  • check_numpy_array_features(name, ref, data) 检查 numpy 数组是否具有与参考解决方案相同的形状和数据类型。是否_不_根据参考检查值。
  • check_numpy_array_allclose(name, ref, data, rtol=1e-05, atol=1e-08) 检查 numpy 数组是否具有与参考解决方案相同的形状和数据类型,还使用指定的 rtolatol 检查值以查看它们是否接近。
  • check_list(name, ref, data, entry_type=None) 检查列表的长度是否与参考解的长度相同。如果 entry_type 不是 None,则可以选择检查每个元素是否具有该类型。是否_不_根据参考检查值。
  • check_dict(name, ref, data, target_keys=None) 检查学生字典是否具有相对于参考字典的所有正确键值映射。
  • check_tuple(name, ref, data) 检查元组是否具有与参考解决方案相同的长度和值。
  • check_scalar(name, ref, data, rtol=1e-5, atol=1e-8) 使用指定的 rtolatol 检查标量值是否接近参考解。
  • call_user(f, *args, **kwargs) 使用特定的 argskwargs 调用用户定义的函数。
  • check_dataframe(name, ref, data, subset_columns=None) 检查 Pandas DataFrame 是否具有与参考解决方案相同的内容。可以选择通过向 subset_columns 提供列名称列表来检查列的子集。

大多数这些函数都有 accuracy_criticalreport_failure 关键字参数。如果 accuracy_critical 设置为 True(默认为 False),则如果此检查失败,评分作业将停止(类似于断言)。如果 report_failure 为 true (default),则如果此检查失败,将向学生显示反馈。显示反馈时使用 name 参数。

一般提示和陷阱

请注意,Feedback.check_xx 函数的第一个参数是正在检查的变量的名称,如果学生错误地回答了此问题,这将显示在评分者反馈中。

请注意不要调换学生参数和参考参数的顺序。学生的答案需要接受更严格的类型检查,过去也曾出现过因格式不良的学生答案而导致评分器崩溃的情况。

禁止库函数

注意,由于Python是一种高度动态的语言,因此对Python有足够了解的同学可以绕过以下方法。为了更好地保证哪些函数被使用或不被使用,请考虑使用更高级的静态分析技术,这些技术超出了此自动评分器提供的范围。您还可以通过手动评分进行手工验证。

人们可以在设置代码中挂钩库函数,以禁止学生访问某些函数。此示例取自 demo/autograder/python/numpy 问题。

可以将库函数设置为Feedback.not_allowed

numpy.linalg.inv = Feedback.not_allowed
numpy.linalg.pinv = Feedback.not_allowed

现在,每当学生尝试使用这些函数时,他们的代码都会引发异常。

概述

setup_code.pyStudent AnswerReference Answer(ans.py)Test Cases(test.py) names_for_userAll Variablesnames_from_user(self.st)All Variables(self.ref)

特定于课程的库

有些课程可能会使用多个问题中通用的库。对于此类问题,可以将这些库和类保存在课程的 serverFilesCourse 目录中。此目录中的任何文件都会自动添加到 Python PATH 中,因此可以根据需要将它们导入到上述任何文件中。但是,如果使用此选项,问题的 info.json 文件应指示这些文件应添加到评分容器中,如下所示:

info.json
{
  "externalGradingOptions": {
    "image": "prairielearn/grader-python",
    "serverFilesCourse": ["course_lib.py"]
  }
}

上述过程适用于特定于特定课程的小型实用程序库。对于较大的库,包括从 Python 包索引 安装的库(即通过 pip install 或同等版本),强烈建议您创建您自己的自定义评分器图像,因为这将提供更好的性能并改善学生体验。

serverFilesCourse 静态数据的使用示例

Python 自动评分器能够从 serverFilesCourse 检索信息。当存在可以在多个问题之间共享的文件和其他静态数据时,可以使用此功能。例如,假设有一个名为 chem.json 的数据文件在多个问题中使用。该文件可以保存在课程根目录中的 serverFilesCourse 目录中,例如 serverFilesCourse/compounds/chem.json

要从自动评分器访问 serverFilesCourse,请在问题 info.json 中指定文件或其包含目录。例如,要将 compounds 目录复制到自动评分器,请使用:

info.json
{
  "externalGradingOptions": {
    "image": "prairielearn/grader-python",
    "serverFilesCourse": ["compounds"]
  }
}

然后,test.py 中的测试可以使用如下代码加载 chem.json 文件:

import json

with open("/grade/serverFilesCourse/compounds/chem.json", "r") as f:
    list_of_compounds = json.load(f)

安全

在评分作业开始时,脚本 run.shroot 权限运行。这将初始化目录并将评分文件复制到正确的位置,删除自身,并使用 uuidgen 生成评分结果的秘密文件名,该文件名通过命令行参数传递到评分代码中。然后,实际的评分脚本将以用户 ag 的身份运行 (pltest.py)。

评分 Python 脚本会将所有敏感文件加载到内存中(设置代码、答案代码、测试用例),并在运行任何学生代码之前将其从磁盘中删除。 argv 中的命令行参数也会被擦除。因此,用户无法访问特定于问题的代码,而只能访问公开的通用评分器逻辑。

评分后,结果将写入上面生成的秘密文件名中。如果该文件不存在或文件名不匹配,则评分作业将失败,学生将不会获得分数。这主要是为了防止评分器代码崩溃,但理论上也可以防止狡猾的学生编写自己的结果文件。

评分作业现在将返回到 run.sh 脚本中的 root,并将所有输出复制到正确的位置,正如外部评分框架所期望的那样。