简体   繁体   中英

Check that Python function does not modify argument?

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM