is there any way to prevent side effects in python?
Asked Answered
H

7

16

Is there any way to prevent side effects in python? For example, the following function has a side effect, is there any keyword or any other way to have the python complain about it?

def func_with_side_affect(a):
    a.append('foo')
Hyperform answered 13/12, 2013 at 14:42 Comment(7)
I don't think you can do this. Somewhat related post: https://mcmap.net/q/349207/-why-no-39-const-39-in-python-closedShakta
How would you possibly detect a side-effect? The difference between a side-effect and just an effect is pretty much purely contextual. How would you even start to devise a system that would automatically determine such a thing?Ejaculatory
@Silas: Side effect has a pretty precise meaning in CS: anything but the return value really, see en.wikipedia.org/wiki/Side_effect_%28computer_science%29Ibnrushd
@Ibnrushd By that definition, append() itself has a side-effect.Ejaculatory
@SilasRay Indeed it has, and that's why OP gives a use of a.append as example for a side effect. What's your point?Headboard
The function with a side effect was the function that called append in this example, not append itself. The point I was trying to make is it's pretty much impossible to differentiate from what seems, at least in my reading of the question, to be an "acceptable" side effect and a side effect we want to detect and warn about.Ejaculatory
@SilasRay func_with_side_affect has side effects because it uses append which has side effects. I don't know why OP included the function definition around it, but this does not affect the question. I don't read any distinction between "acceptable" and "unacceptable" side effects.Headboard
H
12

Python is really not set up to enforce prevention of side-effects. As some others have mentioned, you can try to deepcopy the data or use immutable types, but these still have corner cases that are tricky to catch, and it's just a ton more effort than it's worth.

Using a functional style in Python normally involves the programmer simply designing their functions to be functional. In other words, whenever you write a function, you write it in such a way that it doesn't mutate the arguments.

If you're calling someone else's function, then you have to make sure the data you are passing in either cannot be mutated, or you have to keep around a safe, untouched copy of the data yourself, that you keep away from that untrusted function.

Huoh answered 13/12, 2013 at 15:14 Comment(2)
thanks, now that i know that is is impossible, i can stop trying to make it work and just live with it.Hyperform
@yigalirani python gives you access to all the code, monkey-patch offending function and you're all set!Coates
V
3

No, but with you example, you could use immutable types, and pass tuple as an a argument. Side effects can not affect immutable types, for example you can not append to tuple, you could only create other tuple by extending given.

UPD: But still, your function could change objects which is referenced by your immutable object (as it was pointed out in comments), write to files and do some other IO.

Valvulitis answered 13/12, 2013 at 14:50 Comment(2)
Doesn't reinforce not being able to mutate any mutable objects in the tuple though...Pulmotor
@JonClements Yes, it is better to specify that tuple should contain objects of immutable types.Valvulitis
S
3

Sorry really late to the party. You can use effect library to isolate side-effects in your python code. As others have said in Python you have to explicitly write functional style code but this library really encourages towards it.

Smitt answered 30/9, 2016 at 0:49 Comment(0)
L
2

You'll have to make a copy of the list first. Something like this:

def func_without_side_affect(a):
    b = a[:]
    b.append('foo')
    return b

This shorter version might work for you too:

def func_without_side_affect(a):
    return a[:] + ['foo']

If you have nested lists or other things like that, you'll probably want to look at copy.deepcopy to make the copy instead of the [:] slice operator.

Lurid answered 13/12, 2013 at 15:22 Comment(0)
C
1

About the only way to enforce that would be to overwrite the function specification to deepcopy any arguments before they are passed to the original function. You could to that with a function decorator.

That way, the function has no way to actually change the originally passed arguments. This however has the "sideeffect" of a considerable slowdown as the deepcopy operation is rather costly in terms of memory (and garbage-collection) usage as well as CPU consumption.

I'd rather recommend you properly test your code to ensure that no accidental changes happen or use a language that uses full copy-by-value semantics (or has only immutable variables).

As another workaround, you could make your passed objects basically immutable by adding this to your classes:

 """An immutable class with a single attribute 'value'."""
 def __setattr__(self, *args):
     raise TypeError("can't modify immutable instance")
 __delattr__ = __setattr__
 def __init__(self, value):
     # we can no longer use self.value = value to store the instance data
     # so we must explicitly call the superclass
     super(Immutable, self).__setattr__('value', value)

(Code copied from the Wikipedia article about Immutable object)

Crystallize answered 13/12, 2013 at 14:52 Comment(0)
P
1

Since any Python code can do IO, any Python code could launch intercontinental ballistic missiles (and I'd consider launching ICBMs to be a fairly catastrophic side effect for most purposes).

The only way to avoid side effects is to not use Python code in the first place but rather data - i.e. you end up creating a domain specific language which disallows side effects, and a Python interpreter which executes programs of that language.

Phonogram answered 13/12, 2013 at 14:58 Comment(2)
Wh.. whh.... what? If it can launch ICBMs then it must be Skynet trying to get rid of the human enemy... The Terminators with a Python code base seem somewhat less scary though - so that's a plus :)Pulmotor
This is the most realistic elaboration on "no". import antigravity.Shakta
G
0

It would be very difficult to do for the general case, but for some practical cases you could do something like this:

def call_function_checking_for_modification(f, *args, **kwargs):
    myargs = [deepcopy(x) for x in args]
    mykwargs = dict((x, deepcopy(kwargs[x])) for x in kwargs)

    retval = f(*args, **kwargs)

    for arg, myarg in izip(args, myargs):
        if arg != myarg: 
            raise ValueError, 'Argument was modified during function call!'
    for kwkey in kwargs:
        if kwargs[kwkey] != mykwargs[kwkey]:
            raise ValueError, 'Argument was modified during function call!'

    return retval

But, obviously, there are a few issues with this. For trivial things (i.e. all the inputs are simple types), then this isn't very useful anyways - those will likely be immutable, and in any case they are easy (well, relatively) to detect than complex types.

For complex types though, the deepcopy will be expensive, and there's no guarantee that the == operator will actually work correctly. (and simple copy isn't good enough... imagine a list, where one element changes value... a simple copy will just store a reference, and so the original value with change too).

In general, though, this is not that useful, since if you are already worried about side effects with calling this functions, you can just guard against them more intelligently (by storing your own copy if needed, auditing the destination function, etc), and if it's your function you are worried about causing side effects, you will have audited it to make sure.

Something like the above could be wrapped in a decorator though; with the expensive parts gated by a global variable (if _debug == True:, something like that), it could maybe be useful in projects where lots of people are editing the same code, though, i guess...

Edit: This only works for environments where a more 'strict' form of 'side effects' is expected. In many programming languages, you can make the available of side effects much more explicit - in C++ for instance, everything is by value unless explicitly a pointer or reference, and even then you can declare incoming references as const so that it can't be modified. There, 'side effects' can throw errors at compile time. (of course there are way to get some anyways).

The above enforces that any modified values are in the return value/tuple. If you are in python 3 (i'm not yet) I think you could specify decoration in the function declaration itself to specify attributes of function arguments, including whether they would be allowed to be modified, and include that in the above function to allow some arguments explicitly to be mutable.

Note that I think you could probably also do something like this:

class ImmutableObject(object):
    def __init__(self, inobj):
        self._inited = False
        self._inobj = inobj
        self._inited = True

    def __repr__(self):
        return self._inobj.__repr__()

    def __str__(self):
        return self._inobj.__str__()

    def __getitem__(self, key):
        return ImmutableObject(self._inobj.__getitem__(key))

    def __iter__(self):
        return self.__iter__()

    def __setitem__(self, key, value):
        raise AttributeError, 'Object is read-only'

    def __getattr__(self, key):
        x = getattr(self._inobj, key)
        if callable(x):
              return x
        else:
              return ImmutableObject(x)

    def __setattr__(self, attr, value):
        if attr not in  ['_inobj', '_inited'] and self._inited == True:
            raise AttributeError, 'Object is read-only'
        object.__setattr__(self, attr, value)

(Probably not a complete implementation, haven't tested much, but a start). Works like this:

a = [1,2,3]
b = [a,3,4,5]
print c
[[1, 2, 3], 3, 4, 5]
c[0][1:] = [7,8]
AttributeError: Object is read-only

It would let you protect a specific object from modification if you didn't trust the downstream function, while still being relatively lightweight. Still requires explicit wrapping of the object though. You could probably build a decorator to do this semi-automatically though for all arguments. Make sure to skip the ones that are callable.

Gleiwitz answered 13/12, 2013 at 15:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.