Search code examples
pythonpython-2.7structmutablelvalue

Python 2.7 - clean syntax for lvalue modification


It is very common to have struct-like types that are not expected to be modified by distant copyholders.

A string is a basic example, but that's an easy case because it's excusably immutable -- Python is unusual in even allowing things like method calls on literal strings.

The problem is that (in most languages) we frequently have things like, say an (x,y) Point class. We occasionally want to change x and y independently. I.e., from a usage perspective, a Point LVALUE should be mutable (even though copies will not see the mutation).

But Python 2.7 doesn't seem to provide any options to enable automatic copy-on-assignment. So that means we actually MUST make our Point class IMMUTABLE because inadvertent references are going to get created all over the place (typically because somebody forgot to clone the object before passing it to somebody else).

And no, I'm not interested in the countless hacks that allow the object to be mutated only "while it's being created", as that is a weak concept that does not scale.

The logical conclusion of these circumstances is that we need our mutation methods to actually modify the LVALUE. For example %= supports that. The problem is that it would be much better to have a more reasonable syntax, like using __setattr__ and/or defining set_x and set_y methods, as shown below.

class Point(object):
# Python doesn't have copy-on-assignment, so we must use an immutable
# object to avoid unintended changes by distant copyholders.

    def __init__(self, x, y, others=None):
        object.__setattr__(self, 'x', x)
        object.__setattr__(self, 'y', y)

    def __setattr__(self, name, value):
        self %= (name, value)
        return self # SHOULD modify lvalue (didn't work)

    def __repr__(self):
        return "(%d %d)" % (self.x, self.y)

    def copy(self, x=None, y=None):
        if x == None: x = self.x
        if y == None: y = self.y
        return Point(x, y)

    def __eq__ (a,b): return a.x == b.x and a.y == b.y
    def __ne__ (a,b): return a.x != b.x or  a.y != b.y
    def __add__(a,b): return Point(a.x+b.x, a.y+b.y)
    def __sub__(a,b): return Point(a.x-b.x, a.y-b.y)

    def set_x(a,b): return a.copy(x=b) # SHOULD modify lvalue (didn't work)
    def set_y(a,b): return a.copy(y=b) # SHOULD modify lvalue (didn't work)

    # This works in Python 2.7. But the syntax is awful.
    def __imod__(a,b):
        if   b[0] == 'x': return a.copy(x=b[1])
        elif b[0] == 'y': return a.copy(y=b[1])
        else:             raise AttributeError,  \
                "Point has no member '%s'" % b[0]



my_very_long_and_complicated_lvalue_expression = [Point(10,10)] * 4


# modify element 0 via "+="   -- OK
my_very_long_and_complicated_lvalue_expression[0] += Point(1,-1)

# modify element 1 via normal "__set_attr__"   -- NOT OK
my_very_long_and_complicated_lvalue_expression[1].x = 9999

# modify element 2 via normal "set_x"  -- NOT OK
my_very_long_and_complicated_lvalue_expression[2].set_x(99)

# modify element 3 via goofy "set_x"   -- OK
my_very_long_and_complicated_lvalue_expression[3]    %='x',   999


print my_very_long_and_complicated_lvalue_expression

The result is:

[(11 9), (10 10), (10 10), (999 10)]

As you can see, += and %= work just fine, but just about anything else doesn't seem to work. Surely the language inventors have created a basic syntax for LVALUE modification that is not limited to goofy-looking operators. I just can't seem to find it. Please help.


Solution

  • I feel like we've given the search for pre-existing solutions its due diligence. Given that "<=" is assignment in some languages (e.g., Verilog) we can quite intuitively introduce:

    value_struct_instance<<='field', value
    

    as the Pythonic form of

    value_struct_instance.field = value
    

    Here is an updated example for instructive purposes:

    # Python doesn't support copy-on-assignment, so we must use an
    # immutable object to avoid unintended changes by distant copyholders.
    # As a consequence, the lvalue must be changed on a field update.
    #
    # Currently the only known syntax for updating a field on such an
    # object is:
    #
    #      value_struct_instance<<='field', value
    # 
    # https://stackoverflow.com/questions/45788271/
    
    class Point(object):
    
        def __init__(self, x, y, others=None):
            object.__setattr__(self, 'x', x)
            object.__setattr__(self, 'y', y)
    
        def __setattr__(self, name, value):
            raise AttributeError, \
                "Use \"point<<='%s', ...\" instead of \"point.%s = ...\"" \
                % (name, name)
    
        def __repr__(self):
            return "(%d %d)" % (self.x, self.y)
    
        def copy(self, x=None, y=None):
            if x == None: x = self.x
            if y == None: y = self.y
            return Point(x, y)
    
        def __ilshift__(a,b):
            if   b[0] == 'x': return a.copy(x=b[1])
            elif b[0] == 'y': return a.copy(y=b[1])
            else:             raise AttributeError,  \
                    "Point has no member '%s'" % b[0]
    
        def __eq__ (a,b): return a.x == b.x and a.y == b.y
        def __ne__ (a,b): return a.x != b.x or  a.y != b.y
        def __add__(a,b): return Point(a.x+b.x, a.y+b.y)
        def __sub__(a,b): return Point(a.x-b.x, a.y-b.y)
    
    
    
    my_very_long_and_complicated_lvalue_expression = [Point(10,10)] * 3
    
    # modify element 0 via "+="
    my_very_long_and_complicated_lvalue_expression[0] += Point(1,-1)
    
    # modify element 1 via "<<='field'," (NEW IDIOM)
    my_very_long_and_complicated_lvalue_expression[1]<<='x', 15
    print my_very_long_and_complicated_lvalue_expression
    # result:
    # [(11 9), (15 10), (10 10)]
    
    my_very_long_and_complicated_lvalue_expression[1]<<='y', 25
    print my_very_long_and_complicated_lvalue_expression
    # result:
    # [(11 9), (15 25), (10 10)]
    
    # Attempt to modify element 2 via ".field="
    my_very_long_and_complicated_lvalue_expression[2].y = 25
    # result:
    # AttributeError: Use "point<<='y', ..." instead of "point.y = ..."