user10200421
user10200421

Reputation: 217

How to make a class object iterable

I have the following point class:

import math

class Point:
    """Two-Dimensional Point(x, y)"""
    def __init__(self, x=0, y=0):
        # Initialize the Point instance
        self.x = x
        self.y = y

   # def __iter__(self):
 #       return iter(int(self.x))
 #       return iter(int(self.y))
        return self

    @property
    def magnitude(self):
        # """Return the magnitude of vector from (0,0) to self."""
        return math.sqrt(self.x ** 2 + self.y ** 2)


    def distance(self, self2):
         return math.sqrt((self2.x - self.x) ** 2 + (self2.y - self.y) ** 2)

    def __str__(self):
        return 'Point at ({}, {})'.format(self.x, self.y)

    def __repr__(self):
        return "Point(x={},y={})".format(self.x, self.y)

I want to make 'points' iterable so that the following is possible:

point = Point(2, 3)
x, y = point
print(x)
    2
print(y)
    3

If you see my commented code, that is what I attempted but it says TypeError: iter() returned non-iterator of type 'Point'. Any ideas on how to correctly do this?

Upvotes: 2

Views: 1278

Answers (4)

abarnert
abarnert

Reputation: 366083

tl;dr:

def __iter__(self):
    yield self.x
    yield self.y

To make something iterable, you need your __iter__ method to return an iterator.

You can't just return self, unless self is already an iterator (which means it has a __next__ method). But you don't want points to be an iterator, you want it to be iterable over and over.

You can't return iter(int(self.x)) then return iter(int(self.y)), for two reasons. First, after you return, your function is done; any code that happens after that never gets run. Second, you can't call iter on an int. (Also, there's no reason to call int on an int.)


You could fix that last set of problems by creating an iterable out of each int, like a single-element list, iterating that iterable, and then delegating to it with yield from instead of return. Using yield or yield from in a function makes it a generator function, and the generators created by generated functions are iterators:

yield from iter([self.x])
yield from iter([self.y])

… but this is kind of silly. Once we know we want a generator, we can just yield the values we want to be iterated. Hence the code at the top.


Alternatively, you could explicitly create a single-element iterables for both elements and chain them together and return that:

def __iter__(self):
    return itertools.chain(iter([self.x]), iter([self.y]))

But that's also silly. Chaining two iterators over single-element lists is the same as just iterating a two-element list:

def __iter__(self):
    return iter([self.x, self.y])

Finally, rather than relying on a generator, list iterator, chain, or other iterator type that comes with Python, you can always write one explicitly:

class PointIterator:
    def __init__(self, point):
        self.values = [point.x, point.y]
    def __next__(self):
        try:
            return self.values.pop(0)
        except IndexError:
            raise StopIteration
    def __iter__(self):
        return self

class Point:
    # ... other code
    def __iter__(self):
        return PointIterator(self)

Upvotes: 7

Ajax1234
Ajax1234

Reputation: 71471

You can use yield:

class Point:
  def __init__(self, x=0, y=0):
    self.x = x
    self.y = y
  def __iter__(self):
    yield self.x
    yield self.y

a, b = Point(4, 5)
print([a, b])

Output:

[4, 5]

Upvotes: 2

Sunitha
Sunitha

Reputation: 12025

Your __iter__ method should yield x and y values

def __iter__(self):
    yield self.x
    yield self.y

With this, you get the following output

>>> point = Point(2, 3)
>>> x, y = point
>>> print (x)
2
>>> print (y)
3
>>> 

Upvotes: 1

blue note
blue note

Reputation: 29099

You should implement a special __iter__(self) method, which should return an iterator. Add something like

def __iter__(self):
    return iter([self.x, self.y])

Upvotes: 1

Related Questions