-2

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.

personal_cloud
  • 3,943
  • 3
  • 28
  • 38
  • This is terrible. Use `numpy` to implement your points instead. – Neil G Aug 21 '17 at 01:45
  • Even if you could do this, you'd still get the same bugs, except with accidentally sharing the containers containing the Points instead of accidentally sharing the Points themselves. The semantics you're looking for just don't fit well in a language like Python. – user2357112 Aug 21 '17 at 01:56
  • @NeilG Point was just an example. I don't see how numpy is solving the lvalue modification problem. Please be more specific. – personal_cloud Aug 21 '17 at 02:16
  • @user2357112 That is true, these bugs can happen with containers, pixel buffers, you name it. But for simple structs there should be a clean solution like "struct" in C, or "record" in Pascal, Modula-3, etc. or you'll get a LOT of bugs. – personal_cloud Aug 21 '17 at 02:21
  • 1
    You're looking for value types, and Python just doesn't have those. The best you can do is get out of the habits you learned in languages with value types, like clearing objects instead of creating new ones. – user2357112 Aug 21 '17 at 02:29
  • @user2357112 Yes, value type would be ideal, but I don't necessarily need it. I am perfectly OK with the behavior of "+=". I just don't want to be forced to use an arithmetic operator when there would be a more appropriate method name, such as `set_x`. – personal_cloud Aug 21 '17 at 03:00
  • As I have recently learned Python, I am still hopeful that there is a simple, not-so-obscure answer to the question. (It would seem quite ironic to have a language that forces people into the "everything is a reference" dogma, and then doesn't even provide pass-by-reference except for a few arithmetic operators.) – personal_cloud Aug 21 '17 at 03:03
  • Not to be rude, but I would not say that you have "learned Python" just yet. The answer to your question is that this is not good Python, and it will take practice to write good Python. – Neil G Aug 21 '17 at 03:18
  • Excuse me, but I have already written GUIs and microservice engines in Python and they are working well and people say my code is very understandable. "+=" is perfectly good Python, and solves the problem posed above from a technical standpoint. It's just perhaps not ideal for code readability. The question is whether there is any cleaner syntax in the case where "+", "-", "%', etc doesn't convey the meaning of the operator. – personal_cloud Aug 21 '17 at 06:42
  • I suggest you post your code on https://codereview.stackexchange.com/ There are so many things that a more experienced Python developer would not do. – Neil G Aug 21 '17 at 07:28
  • I'm not sure what you're getting at. This is not a deep question here. We seem to agree on the need for immutable types combined with copy operations. If this can be done in a reliable way (i.e., low chance of human error) without arithmetic operators, then please post your solution. Or get one of those experienced developers to post their solution please. – personal_cloud Aug 22 '17 at 03:16

2 Answers2

1

In Python, the typical pattern is to copy before modification rather than copying on assignment. You could implement some kind of data store with the semantics you want, but it seems like a lot of work.

Neil G
  • 32,138
  • 39
  • 156
  • 257
  • So... the question is how to make the copy-before-modification happen automatically, as it does with "+=" -- but when that kind of operator is confusing or not appriopriate. – personal_cloud Aug 21 '17 at 02:28
  • @personal_cloud `+=` doesn't necessarily copy-on-write. In some cases, it can mutate the object on the left-hand side; in others, it can return a new object. Write a data store with the semantics you want or be more careful not to forget to copy objects before mutating them. – Neil G Aug 21 '17 at 03:16
  • @personal_cloud I don't know of anyone who has written such a package because I don't think it's as useful as you do. You would have to write it yourself. I definitely don't have time to do that. – Neil G Aug 21 '17 at 07:26
  • The basic problem with copy-before-modification is that "before modification" is hard to predict. When you're passing an object to somebody else's module, you can't be entirely sure they're not going to modify it during the call -- or worse: after the call when you don't expect it. Technically the author of every module should document what it can modify. But what if they do a bad job? Then I have to do more debugging. Therefore the absence of pass-by-value is a serious problem with the language. I would much rather have most objects copied automatically every time I pass them to someone else. – personal_cloud Sep 13 '17 at 04:59
0

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 = ..."
personal_cloud
  • 3,943
  • 3
  • 28
  • 38
  • @Neil G If x is an integer, can we do "x+=1"? Remember, integers are objects! Definitely don't use an operator to copy on modification??? – personal_cloud Aug 21 '17 at 17:09
  • Try `x += 1` with integers. And yes, definitely don't override an operator for this. – Neil G Aug 21 '17 at 18:43
  • And what about strings? Am I allowed to do `h += ".txt"`? And then, could one perhaps infer that the language designers were perfectly aware of the general need for lvalue modification when dealing with immutable objects? Or we simply can't apply principles like automatic-copy-on-modification to any other type. Just because int and string are different in some way. And in what way, exactly, are int and string different from other types? – personal_cloud Aug 22 '17 at 03:07
  • (hint: people think of them as VALUES not objects. Oh but we wouldn't want to admit that this EVER happens for something that's not an int or a string) – personal_cloud Aug 22 '17 at 03:21