0

I am observing following behavior since python passes object by reference?

class Person(object):
    pass

person = Person()

person.name = 'UI'
def test(person):
    person.name = 'Test'

test(person)
print(person.name)

>>> Test

I found copy.deepcopy() to deepcopy object to prevent modifying the passed object. Are there any other recommendations ?

import copy

class Person(object):
    pass

person = Person()

person.name = 'UI'
def test(person):
    person_copy = copy.deepcopy(person)
    person_copy.name = 'Test'

test(person)
print(person.name)

>>> UI


Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80
Deepanshu Arora
  • 375
  • 1
  • 5
  • 21
  • 1
    In this particular case, you can override the setter for name to return a copy of the object with the new value of the attribute name. In general, the way to ensure immutability is to ensure that there are no methods that mutate data in the class. – rdas Jun 16 '19 at 07:28
  • @rdas The problem I have is that I have to mutate multiple properties so overriding the setters will give me a new object every time – Deepanshu Arora Jun 16 '19 at 07:30
  • How about setting all attributes during construction ? Overriding the setters should not be a problem then... – ma3oun Jun 16 '19 at 07:42
  • Question is why you should want to do that? This is not "pythonic" way and you might break a lot of things (for example mocking). If you want to make something readonly make it readonly via `@property`. This is enough to prevent accidental mutation and still allows everyone, who is truly wishing to change to let them thru. – Radosław Cybulski Jun 16 '19 at 08:39
  • If you have to mutate properties then you don't want to ensure immutability. The recommendation here is keeping you requirements consistent. – Stop harming Monica Jun 16 '19 at 12:42

3 Answers3

1

I am observing following behavior since python passes object by reference?

Not really. it's a subtle question. you can look at python - How do I pass a variable by reference? - Stack Overflow

Personally, I don't fully agree with the accepted answer and recommend you google call by sharing. Then, you can make your own decision on this subtle question.

I found copy.deepcopy() to deepcopy object to prevent modifying the passed object. Are there any other recommendations ?

As far as I know, there no other better way, if you don't use third package.

LiuXiMin
  • 1,225
  • 8
  • 17
0

You can use the __setattr__ magic method to implement a base class that allows you to "freeze" an object after you're done with it.

This is not bullet-proof; you can still access __dict__ to mutate the object, and you can also unfreeze the object by unsetting _frozen, and if the attribute's value itself is mutable, this doesn't help much (x.things.append('x') would work for a list of things).

class Freezable:
    def freeze(self):
        self._frozen = True

    def __setattr__(self, key, value):
        if getattr(self, "_frozen", False):
            raise RuntimeError("%r is frozen" % self)
        super().__setattr__(key, value)


class Person(Freezable):
    def __init__(self, name):
        self.name = name


p = Person("x")
print(p.name)
p.name = "y"
print(p.name)
p.freeze()
p.name = "q"

outputs

x
y
Traceback (most recent call last):
  File "freezable.py", line 21, in <module>
    p.name = 'q'
RuntimeError: <__main__.Person object at 0x10f82f3c8> is frozen
AKX
  • 152,115
  • 15
  • 115
  • 172
0

There are no really 100% watertight way, but you can make it difficult to inadvertently mutate an object that you want to keep frozen; the recommended way for most people is probably to use a frozen DataClass, or a frozen attrs class

In his talk on DataClasses (2018), @RaymonHettinger mentions three approaches: one way, is with a metaclass, another, like in the fractions module is to give attributes a read only property; the DataClass module extends __setattr__ and __delattr__, and overrides __hash__:

-> use a metaclass.

Good resources include @DavidBeasley books and talks at python.

-> give attributes a read only property

class SimpleFrozenObject:
    def __init__(self, x=0):
        self._x = x
    @property
    def x(self):
        return self._x

f = SimpleFrozenObject()
f.x = 2  # raises AttributeError: can't set attribute

-> extend __setattr__ and __delattr__, and override `hash

class FrozenObject:

    ...

    def __setattr__(self, name, value):
        if type(self) is cls or name in (tuple of attributes to freeze,):
            raise FrozenInstanceError(f'cannot assign to field {name}')
        super(cls, self).__setattr__(name, value)

    def __delattr__(self, name):
        if type(self) is cls or name in (tuple of attributes to freeze,):
            raise FrozenInstanceError(f'cannot delete field {name}')
        super(cls, self).__delattr__(name, value)

    def __hash__(self):
        return hash((tuple of attributes to freeze,))

    ...

The library attrs also offers options to create immutable objects.

Reblochon Masque
  • 35,405
  • 10
  • 55
  • 80