Reputation: 9418
I tried create the following code:
class Test(object):
def __init__(self, arg):
self.arg1 = arg + 1
self.arg2 = arg + 2
self.arg3 = arg + 3
def __iter__(self):
return self
def __next__(self):
yield self.arg1, self.arg2, self.arg3
test_list = [Test(0), Test(1), Test(2)]
for arg1, arg2, arg3 in test_list:
print('arg1', arg1, 'arg2', arg2, 'arg3', arg3)
But when I try to run, python says:
Traceback (most recent call last):
File "test.py", line 20, in <module>
for arg1, arg2, arg3 in test_list:
ValueError: too many values to unpack (expected 3)
I can work around it by unpacking by hand:
class Test(object):
def __init__(self, arg):
self.arg1 = arg + 1
self.arg2 = arg + 2
self.arg3 = arg + 3
test_list = [Test(0), Test(1), Test(2)]
for test in test_list:
arg1, arg2, arg3 = test.arg1, test.arg2, test.arg3
print('arg1', arg1, 'arg2', arg2, 'arg3', arg3)
How can we unpack objects in a python list without doing it explicitly as the workaround demonstrated? For the last example, the result would be like:
arg1 1 arg2 2 arg3 3
arg1 2 arg2 3 arg3 4
arg1 3 arg2 4 arg3 5
Upvotes: 6
Views: 4968
Reputation: 71451
You are close, however, you need to yield
the values in the __iter__
method, not the __next__
method:
class Test:
def __init__(self, arg):
self.arg1 = arg + 1
self.arg2 = arg + 2
self.arg3 = arg + 3
def __iter__(self):
yield from [self.arg1, self.arg2, self.arg3]
for a, b, c in [Test(0), Test(1), Test(2)]:
pass
yield self.arg1, self.arg2, self.arg3
will give a tuple
result (1, 2, 3)
which, when iterating over the list, requires additional unpacking i.e:
for [(a, b, c)] in [Test(0), Test(1), Test(2)]:
pass
Thus, in order to avoid the additional unpacking in the loop, you have to create a stream of generated values by looping over the attributes and yielding each, one at a time.
Upvotes: 8
Reputation: 15513
Question: How to unpack an object as it was a tuple in a for loop?
You are wrong, you get a Test(object)
in the loop.
To get a tuple(arg1, arg2, arg3)
from the object, you have to trigger it.
using iter
:
class Test(object):
...
def __iter__(self):
yield self.arg1
yield self.arg2
yield self.arg3
for arg1, arg2, arg3 in map(iter, test_list):
print('arg1', arg1, 'arg2', arg2, 'arg3', arg3)
or force .format(...
to see t
as type tuple
:
for t in test_list:
print("arg1={}, arg2={}, arg3={}".format(*tuple(t)))
using class property values(self, ...
:
class Test(object):
...
#@property
def values(self):
return self.arg1, self.arg2, self.arg3
for arg1, arg2, arg3 in map(Test.values, test_list):
print('arg1', arg1, 'arg2', arg2, 'arg3', arg3)
or with .format(...
and *
for t in test_list:
print("arg1={}, arg2={}, arg3={}".format(*t.values()))
Recommend usage. without any magic:
for t in test_list:
print('arg1', t.arg1, 'arg2', t.arg2, 'arg3', t.arg3)
or with .format(...
for t in test_list:
print("arg1={t.arg1}, arg2={t.arg2}, arg3={t.arg3}".format(t=t))
Tested with Python: 3.5
Upvotes: 1
Reputation: 106543
__next__
is useful if you have a variable number of items to return. Since you have a fixed number of attributes to yield from your iterator, you can simply use the iter
function to create an iterator from the three attributes and make the __iter__
method return the iterator instead:
class Test(object):
def __init__(self, arg):
self.arg1 = arg + 1
self.arg2 = arg + 2
self.arg3 = arg + 3
def __iter__(self):
return iter((self.arg1, self.arg2, self.arg3))
which is equivalent to, without using the iter
function:
class Test(object):
class Iter:
def __init__(self, lst):
self.lst = lst
self.index = 0
def __next__(self):
if self.index == len(self.lst):
raise StopIteration()
value = self.lst[self.index]
self.index += 1
return value
def __init__(self, arg):
self.arg1 = arg + 1
self.arg2 = arg + 2
self.arg3 = arg + 3
def __iter__(self):
return self.Iter((self.arg1, self.arg2, self.arg3))
Upvotes: 2
Reputation: 43156
The problem is that your __next__
method isn't implemented correctly. You made the Test
class iterable, but it doesn't iterate how you think it does:
>>> itr = iter(Test(0))
>>> next(itr)
<generator object Test.__next__ at 0x7ff609ade9a8>
>>> next(itr)
<generator object Test.__next__ at 0x7ff609ade930>
>>> next(itr)
<generator object Test.__next__ at 0x7ff609ade9a8>
Instead of yielding a (self.arg1, self.arg2, self.arg3)
tuple, it yields generators. This is caused by your use of yield
inside the __next__
method. A __next__
method should return values, not yield them. See this question for a detailed explanation and fix.
Upvotes: 7