Re: [Tutor] Nested use of replication operator on lists

2018-05-25 Thread Steven D'Aprano
On Thu, May 24, 2018 at 10:33:56PM -0500, boB Stepp wrote:
[...]
> I am having trouble correlating the behavior of the one-dimensional
> case with the two-dimensional case.  The result of [1, 2]*3 seems to
> be an actual list, not a replication of the references to the items in
> the original list, [1, 2].

The result of list * 3 is always a list. What matters is the items 
inside the list.

What the * operator does is create a new list containing the entries of 
the old list repeated. We can write our own version:

def replicate(alist, count):
newlist = []
for i in range(count):
newlist.extend(alist)
return newlist

Notice that we don't copy the items in alist. We just shove them into 
the new list, repeatedly.

If the items are immutable, like integers, that is perfectly fine. 
Copying an immutable object is a waste of time, and in fact the standard 
copy function will usually refuse to do so:

py> import copy
py> a, b = 1234567, []  # immutable int, mutable list
py> copy.copy(a) is a  # is the copy the same object as the original?
True
py> copy.copy(b) is b  # is the copy the same object as the original?
False

(To be precise, it is not the copy() function that refuses to make a 
copy. It the object itself: each object knows how to copy itself, and 
immutable ones will typically return themselves because they know it 
makes no difference.)


Let us go back to * the replicator operator. We can use "is" to check 
for object identity:

py> obj = 987654321
py> alist = [obj]
py> assert alist[0] is obj
py> blist = alist*5
py> all(x is obj for x in blist)
True

So our blist contains five references to the same int object.

For integers, floats, strings and other immutable objects, this is 
exactly what you want. There is no operation we can do to an immutable 
operation to change its value, so there is no way to distinguish between 
the same object twice or an object and a fresh copy.

(Except for using the "is" operator, or the id() function.)

So when we have a list full of ints (or floats, strings, etc) the only 
way we can change the value of the list is to *replace* the individual 
objects with a brand new object:

py> blist
[987654321, 987654321, 987654321, 987654321, 987654321]
py> blist[0] = -1
py> blist[3] = -1
py> blist
[-1, 987654321, 987654321, -1, 987654321]

Since we're *replacing* the objects with a new object, the remaining 
987654321 items don't change (indeed they can't change).

Now let us do the same with a list instead of an int:

py> obj = []
py> alist = [obj]
py> assert alist[0] is obj
py> blist = alist*5
py> all(x is obj for x in blist)
True


So far, the behaviour is identical. And replacing items works just like 
it does with ints:

py> blist
[[], [], [], [], []]
py> blist[0] = [1, 2, 3]
py> blist
[[1, 2, 3], [], [], [], []]


But because mutable objects like lists can be modified in place, not 
just replaced, we can do this:

py> blist[4].append(999)
py> blist
[[1, 2, 3], [999], [999], [999], [999]]


Touching the last item modifies all the other references to that same 
list object, since the * operator doesn't make copies.

Think of it this way: I got into a fight the other day, on one side 
there was Tom, Dick and Harry, but fortunately it was a fair fight 
because on the other side there was me, myself and I.



> Also the "replication operator" does not seem to be replicating
> anything list-wise if it is instead replicating references to the
> original list's members.

It replicates the *contents* of the list into a new list, not the list 
itself.



-- 
Steve
___
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor


Re: [Tutor] Nested use of replication operator on lists

2018-05-25 Thread boB Stepp
The subtleties of the interactions between the concepts of references
to objects, mutable objects, immutable objects, shallow copy of
objects and deep copy of objects continue to surprise me!

On Fri, May 25, 2018 at 1:27 AM, Steven D'Aprano  wrote:
> On Thu, May 24, 2018 at 10:33:56PM -0500, boB Stepp wrote:

> The result of list * 3 is always a list. What matters is the items
> inside the list.
>
> What the * operator does is create a new list containing the entries of
> the old list repeated.

I was not properly appreciating that that these repeated objects were
the *same identical* objects that were in the pre-replicated list.

>
> If the items are immutable, like integers, that is perfectly fine.
> Copying an immutable object is a waste of time, and in fact the standard
> copy function will usually refuse to do so:
>
> py> import copy
> py> a, b = 1234567, []  # immutable int, mutable list
> py> copy.copy(a) is a  # is the copy the same object as the original?
> True
> py> copy.copy(b) is b  # is the copy the same object as the original?
> False
>
> (To be precise, it is not the copy() function that refuses to make a
> copy. It the object itself: each object knows how to copy itself, and
> immutable ones will typically return themselves because they know it
> makes no difference.)

It appears to me that a logical consequence of a particular object
being immutable is that it also must be unique.

> For integers, floats, strings and other immutable objects, this is
> exactly what you want. There is no operation we can do to an immutable
> operation to change its value, so there is no way to distinguish between
> the same object twice or an object and a fresh copy.
>
> (Except for using the "is" operator, or the id() function.)

I should have thought to use "is".  I had seen "id()", but had not
previously investigated its usage.  Danny's answer inspired me to read
up on id() tonight.  Useful investigative tools!

There is another subtlety that I am not certain I am truly understanding:

py3: empty = []
py3: a = copy.copy(empty)
py3: a is empty
False
py3: a = [empty]
py3: b = copy.copy(a)
py3: a is b
False
py3: a[0] is b[0]
True
py3: c = copy.deepcopy(a)
py3: a is c
False
py3: a[0] is c[0]
False

Up to this point I am fine, but ...

py3: e = a*5
py3: e
[[], [], [], [], []]
py3: all(x is empty for x in e)
True

OK, I was expecting this.

py3: f = copy.deepcopy(e)
py3: any(x is empty for x in f)
False

Still OK ...

py3: all(x is f[0] for x in f)
True

But this I found mildly surprising.  I guess I was thinking that

f = copy.deepcopy(e) would be equivalent to manually entering "[]"
five times inside an enclosing list:

py3: f = [[], [], [], [], []]
py3: f[0] is f[1]
False
py3: all(x is f[0] for x in f)
False

So it surprised me that the deep copy created the same reference for
all members, just like in the original list.


-- 
boB
___
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor