Why does updating a set in a tuple cause an error?
I have just tried the following in Python 2.6:
>>> foo = (set(),)
>>> foo[0] |= set(range(5))
TypeError: 'tuple' object does not support item assignment
>>> foo
(set([0, 1, 2, 3, 4]),)
>>> foo[0].update(set(range(10)))
>>> foo
(set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),)
I have several questions here:
- Why does
foo[0] |= set(range(5))
update the set and throw an exception? - why does
foo[0].update(set(range(10)))
work without a problem? Should it not have the same result as the first statement?
Edit Many people have pointed out, that tuples are immutable. I am aware of that. They have also pointed out, that |=
would create a new set
object and assign it to the tuple. That is wrong. See this:
>>> foo = set()
>>> bar = foo
>>> foo is bar
True
>>> foo |= set(range(5))
>>> foo
set([0, 1, 2, 3, 4])
>>> bar
set([0, 1, 2, 3, 4])
>>> foo is bar
True
This means that no new object开发者_如何学编程 has been created, but the existing one was modified. This should work with the tuple. Please note also that, although my first code throws a TypeError
, the set within the tuple is still updated. That is the effect I am interested in. Why the TypeError
, when the operation obviously was successful?
>>> def f():
... x = (set(),)
... y = set([0])
... x[0] |= y
... return
...
>>> import dis
>>> dis.dis(f)
2 0 LOAD_GLOBAL 0 (set)
3 CALL_FUNCTION 0
6 BUILD_TUPLE 1
9 STORE_FAST 0 (x)
3 12 LOAD_GLOBAL 0 (set)
15 LOAD_CONST 1 (0)
18 BUILD_LIST 1
21 CALL_FUNCTION 1
24 STORE_FAST 1 (y)
4 27 LOAD_FAST 0 (x)
30 LOAD_CONST 1 (0)
33 DUP_TOPX 2
36 BINARY_SUBSCR
37 LOAD_FAST 1 (y)
40 INPLACE_OR
41 ROT_THREE
42 STORE_SUBSCR
5 43 LOAD_CONST 0 (None)
46 RETURN_VALUE
This shows that the statement x[0] |= y
is implemented by calling x[0].__ior__(y)
and then assigning the returned value to x[0]
.
set
implements in-place |=
by having set.__ior__
return self
. However, the assignment to x[0]
still takes place. The fact that it's assigning the same value that was already there is irrelevant; it fails for the same reason that:
x = (set(),)
x[0] = x[0]
fails.
In your example foo
is a tuple. Tuples in python are inmutable, this means that you cannot change the reference of any tuple element - foo[0]
in your case. Things like the following can't be done:
>>> x = ('foo','bar')
>>> x[0]='foo2'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>>
You could use a list
instead
>>> foo = [set(),None]
>>> foo
[set([]), None]
>>> foo[0] |= set(range(5))
>>> foo
[set([0, 1, 2, 3, 4]), None]
>>>
foo[0] |= set(range(5))
doesn't work, because what you wanted to achieve is:
foo[0] = foo[0] | set(range(5))
and you can't assign new elements to an old tuple, because they are immutable. For example you cant do this:
x = (0, 1, 2)
x[0] = 3
When you are running update, you don't change references in the tuple, but only object behind the reference. You could also do this like this:
x = set()
y = (x,)
x.update(set(range(5))
as you can see you don't change the tuple, but x
(and y[0]
) will be changed.
x |= y
and
x.update(y)
aren't the same, because update
works in place and x |= y
will create a new object (x | y)
and store it under name x
.
Tuples are immutable. By trying to assign to foo[0]
, you are attempting to change a value that the tuple stores (a reference to a set). When you use the update()
function, you are not changing the reference, but instead the actual set. Because the reference is the same, this is allowed.
Tuples are immutable so u cannot reassign values to it. But if a tuple contains a mutable type such as list or set u can update them. now in your case when u use '|=' u actually first update the set (which is a value in the tuple) then assign it to tuple which causes the exception. Exception is thrown after the updation of the set.
In the next case u r simply updating the set so there is no exception. Refer to http://docs.python.org/reference/datamodel.html
"Why the TypeError, when the operation obviously was successful?".
Because there are multiple side-effects. Try to avoid that.
That's what the
foo[0] |= set(range(5)) update the set and throw an exception
Correct. First the set mutation is done.
Then the tuple mutation is attempted and fails.
foo[0].update(set(range(10))) work without a problem?
Correct. The set is mutated.
Should it not have the same result as the first statement?
No. The first statement involves explicit assignment -- changing the tuple -- which is forbidden.
The second statement updates a member of an immutable tuple, an operation that is not forbidden, but is suspicious as pushing the envelope.
But the Legalism Scholar argues, aren't they supposed to be the same? Or similar? No.
Updating the tuple object (via assignment) is forbidden.
Updating a member of an existing tuple object (via a mutator function) is not forbidden.
a |= b
is equivalent toa = operator.ior(a, b)
.s[i] |= b
is equivalent tos[i] = operator.ior(s[i], b)
.- Item assignment on a tuple is forbidden by contract.
- The
set.__ior__
method callsset.update
without creating a new instance.
That explains the behaviour you are observing.
The underlying problem, is that changing the value of a tuple is a violation of contract. You should not try doing it. Since you can have any object in a tuple, there are loopholes you can exploit, but then you get the kind of weird behaviour you are observing.
Tuple items should be frozenset
instead of set
. If you do this, you will get a consistent behaviour, and no unwanted side-effect on error.
The best way to explain this is to show it "algebraically":
foo[0] |= set(range(5))
foo[0] = set.__ior__(foo[0], set(range(5)))
tuple.__setitem__(foo, 0, set.__ior__(foo[0], set(range(5))))
foo[0].update(set(range(5)))
set.__ior__(foo[0], set(range(5)))
As you can see, the update
form is not the same, it modifies foo[0]
in place. __or__
generates a new set from the elements of the left and right operands. This is then assigned back to foo
.
Note that for simplicity, the expansions that aren't helpful to the problem are not expanded (such as foo[0] -> tuple.__getitem__(foo, 0)
).
The TypeError
thrown is in tuple.__setitem__
. tuple
does not allow its items references to be replaced. The update
form does not touch foo
in any way (ie. it doesn't not invoke tuple.__setitem__
).
精彩评论