You know how in Python, if v
is a list or a dictionary, it's quite common to write functions that modify v
in place (instead of just returning the new value). I'm wondering if it is possible to write a checker that identifies such functions.
For simplicity, say you have a function f
, which only takes one argument - a
and returns in finite time (the return value is actually irrelevant). Assume also that for any input value v
, f(v)
always does the same thing (ie the logic inside of f
does not depend on any context or environment values - it's a pure computation on a
).
Is it possible to write a function m
, such that m(f, v)
returns True
if and only if f(v)
actually changes the original value of v
?
No; this is equivalent to the Halting problem . If such an m
did exist, then I could just write:
def f(a):
if m(f, a):
return a
else:
# Modify `a` somehow
return a
and we get a contradiction.
If you want to check that behavior you can write a simple blackbox test using deepcopy
of the original value something like:
def m(f, a):
original = copy.deepcopy(a)
f(a)
return original != a
def f(a):
a.append('a')
def k(a):
b = a
z = ['b']
m(f, z) # True
z = ['b']
m(k, z) # False
Of course, if the argument is a list
of dict
you have to deepcopy and compare the inner objects, but it's the same logic
a very rudimentary first shot at this that works for iterables only (and no: i do not claim this solves the halting problem or works in the general case...):
from collections.abc import Sequence
def changes(lst):
lst.append(0)
def no_changes(lst):
return
def tries_to_change(f, v):
if isinstance(v, Sequence):
v_immutable = tuple(v)
try:
f(v_immutable)
return False
except AttributeError:
return True
print(tries_to_change(f=changes, v=[1, 2, 3])) # True
print(tries_to_change(f=no_changes, v=[1, 2, 3])) # False
the idea is to cast the input to an immutable version of the same datastructure and see what happens. very crude!
and as mentioned by jbasko in the comments: this only prevents setting and deleting of elements; modifications of the elements themselves (eg if the argument is a list of a list; you could still change the 'inner' list) will go undetected.
minor update thanks to PM 2Ring 's comment: if the list contains mutable things this approach does not work (and the function returns None
[which then is consistent with the halting problem answer...]).
def tries_to_change(f, v):
if isinstance(v, Sequence):
# check if the sequence contains immutable elements only:
try:
set(v)
except TypeError:
# no idea what could happen to the elements in the list...
return None
v_immutable = tuple(v)
try:
f(v_immutable)
return False
except AttributeError:
return True
If you know that it will either always change v
or never change v
, then you could do something like this:
class Checker(dict):
def __init__(self):
super().__init__()
self.changed = False
def __setitem__(self, index, value):
super().__setitem__(index, value)
self.changed = True
# implementing the rest of the mutating methods, e.g. `update`
# is left as an exercise for the reader
def m(f, v):
'''return True if f modifies v. Otherwise, return False'''
c = Checker()
c.update(v)
f(c)
return c.changed
You might be able to use some code-path execution checking to see if all paths were executed, and/or do some weird AST hacking to remove any kind of conditionals... but you'd also have to make sure there wasn't any kind of stupid shenanigans like this:
def f(v):
'''Do terrible things in terrible ways.'''
q = v
if q.update({1:1}):
pass # because it will never hit here, but it *will* modify `v`
qux = [1,2]
qux.append({'derp': v})
qux[2]['derp'][42] = '...herring. A red one!'
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.