on perhaps unloading modules?
Hope Rouselle
hrouselle at jevedi.com
Sun Aug 22 05:30:38 EDT 2021
Hope Rouselle <hrouselle at jevedi.com> writes:
> Chris Angelico <rosuav at gmail.com> writes:
>
>> On Tue, Aug 17, 2021 at 4:02 AM Greg Ewing
>> <greg.ewing at canterbury.ac.nz> wrote:
>>> The second best way would be to not use import_module, but to
>>> exec() the student's code. That way you don't create an entry in
>>> sys.modules and don't have to worry about somehow unloading the
>>> module.
>>
>> I would agree with this. If you need to mess around with modules and
>> you don't want them to be cached, avoid the normal "import" mechanism,
>> and just exec yourself a module's worth of code.
>
> Sounds like a plan. Busy, haven't been able to try it out. But I will.
> Soon. Thank you!
Just to close off this thread, let me share a bit of what I wrote. The
result is a lot better. Thanks for all the help!
I exec the student's code into a dictionary g.
--8<---------------cut here---------------start------------->8---
def fs_read(fname):
with open(fname, "r") as f:
return f.read()
def get_student_module_exec(fname):
g = {}
try:
student_code = fs_read(fname)
student = exec(student_code, g)
except Exception as e:
return False, str(e)
return True, g
def get_student_module(fname):
return get_student_module_exec(fname)
--8<---------------cut here---------------end--------------->8---
And now write the test's key as if I were a student and named my test as
"test_key.py".
--8<---------------cut here---------------start------------->8---
def get_key():
okay, k = get_student_module("test_key.py")
if not okay:
# Stop everything.
...
return g
--8<---------------cut here---------------end--------------->8---
The procedure for grading a question consumes the student's code as a
dictionary /s/, grabs the key as /k/ and checks whether the procedures
are the same. So, suppose I want to check whether a certain function
/fn/ written in the student's dictionary-code /s/ matches the key's.
Then I invoke check_student_procedure(k, s, fn).
--8<---------------cut here---------------start------------->8---
def check_student_procedure(k, s, fn, args = [], wrap = identity):
return check_functions_equal(g[fn], s.get(fn, None), args, wrap)
--8<---------------cut here---------------end--------------->8---
For completeness, here's check_functions_equal.
--8<---------------cut here---------------start------------->8---
def check_functions_equal(fn_original, fn_candidate, args = [], wrap = identity):
flag, e = is_function_executable(fn_candidate, args)
if not flag:
return False, "runtime", e
# run original and student's code, then compare them
answer_correct = fn_original(*args)
answer_student = wrap(fn_candidate(*args))
if answer_correct != answer_student:
return False, None, str(answer_student)
return True, None, None
def identity(x):
return x
--8<---------------cut here---------------end--------------->8---
To explain my extra complication there: sometimes I'm permissive with
student's answers. Suppose a question requires a float as an answer but
in some cases the answer is a whole number --- such as 1.0. If the
student ends up producing an int, the student gets that case right: I
wrap the student's answer in a float() and the check turns out
successful.
I probably don't need to check whether a procedure is executable first,
but I decided to break the procedure into two such steps.
--8<---------------cut here---------------start------------->8---
def is_function_executable(f, args = []):
try:
f(*args)
except Exception as e:
return False, str(e)
return True, None
--8<---------------cut here---------------end--------------->8---
More information about the Python-list
mailing list