Is it possible to make wrapper object for numbers, e.g. float, to make it mutable?

Question:

In Python 3 everything is supposed to be an object, even numbers, but they’re immutable.

Is it possible to create wrapper object for numbers, e.g. float, such that it would behave exactly as ordinary numbers except it must be mutable?

I’ve wondered whether it would be feasible using built-in type function by creating anonymous wrapper object deriving from float, but changing it behaviour to be mutable.

>>> f = lambda x : type('', (float,), dict())(x)
>>> a = f(9)
>>> a
9.0

What parameters must I change f to make number a be mutable?

How I verify if a number is mutable:

I must be able to create such function f that would create from integer value a float value and after shallow copy it would behave in the following manner:

>>> list_1 = [f(i) for i in [1, 2, 3, 4]]
>>> list_1
[1.0, 2.0, 3.0, 4.0]
>>> list_2 = copy.copy(list_1)
>>> list_1[0] *= 100
>>> list_1
[100.0, 2.0, 3.0, 4.0]
>>> list_2
[100.0, 2.0, 3.0, 4.0]

Modification of the first list, have changed both of them.

Maybe I must add some fields to dict() or add additional base class that would enforce mutability?

Asked By: Adam Przedniczek

||

Answers:

Values are immutable. They’re platonic forms. An expression like 5 := 3 is nonsensical. What are mutable are locations, usually referred to as addresses or pointers. Python doesn’t have those, but we can fake it by using a container type like a list, which is really a location that references other locations.

Here’s a partial implementation of a mutable numerical type by using a list to store a location where we will keep the value of the number and change the value in that location when it should change, and because all copies of a mutable number will share that location, all copies will see the change

import copy

# Convenience to work with both normal and mutable numbers
def _get_value(obj):
    try:
        return obj.value[0]
    except:
        return obj

class mutable_number(object):
    def __init__(self, value):
        # Mutable storage because `list` defines a location
        self.value = [value]

    # Define the comparison interface
    def __eq__(self, other):
        return _get_value(self) == _get_value(other)

    def __ne__(self, other):
        return _get_value(self) != _get_value(other)

    # Define the numerical operator interface, returning new instances
    # of mutable_number
    def __add__(self, other):
        return mutable_number(self.value[0] + _get_value(other))

    def __mul__(self, other):
        return mutable_number(self.value[0] * _get_value(other))

    # In-place operations alter the shared location
    def __iadd__(self, other):
        self.value[0] += _get_value(other)
        return self

    def __imul__(self, other):
        self.value[0] *= _get_value(other)
        return self

    # Define the copy interface
    def __copy__(self):
        new = mutable_number(0)
        new.value = self.value
        return new

    def __repr__(self):
        return repr(self.value[0])

x = mutable_number(1)
y = copy.copy(x)
y *= 5
print x

list_1 = [mutable_number(i) for i in [1, 2, 3, 4]]
list_2 = copy.copy(list_1)
list_1[0] *= 100
print list_1
print list_2

Please let me know if anything is unclear, and I can add more documentation

Answered By: mobiusklein

Maybe a wrapper like the following would be what you need.

# To solve scope issues. See
# <https://stackoverflow.com/questions/3431676/creating-functions-in-a-loop>
# for details.
def make_new_method(self, method_name):
    def new_method(self, *a, **aa):
        method = getattr(self._type, method_name)
        return method(self._heart, *a, **aa)
    return new_method


class Mutable():
    def __init__(self, obj):
        self._heart = obj
        self._type = type(obj)

        # Remap each of the heart object's attributes to be used on the
        # wrapper object.
        for attr_name in dir(self._type):
            # Do not overwrite existing Mutable methods, only set new ones
            # from its heart.
            if attr_name not in dir(Mutable):
                # Methods need to be treated differently.
                if callable(getattr(self._type, attr_name)):
                    new_attr = make_new_method(self, attr_name)
                    setattr(Mutable, attr_name, new_attr)
                else:
                    attr_value = getattr(self._heart, attr_name)
                    setattr(self, attr_name, attr_value)

    def set(self, new_value):
        if self._type is type(new_value):
            self._heart = new_value
        else:
            self.__init__(new_value)

    def __repr__(self):
        return f'Mutable({repr(self._heart)})'

When you write mutable3 = Mutable(3) it stores the value you want to be mutable (3, in this case) in the atribute _heart of an instance of the Mutable class and remaps its methods to be used directly with the instance itself. It allows, for example, doing

In [1]: a = list(map(Mutable, range(3)))

In [2]: a
Out[2]: [Mutable(0), Mutable(1), Mutable(2)]

In [3]: b = a[1]

In [4]: b.set('hello')

In [5]: a
Out[5]: [Mutable(0), Mutable('hello'), Mutable(2)]

It sounds to me like creating "symlinks" between variables, a behaviour we would achieve using pointers in a lower level language like C.

Answered By: Pedro Ilidio