37

I have a class that keeps track of its instances in a class variable, something like this:

class Foo:
    by_id = {}

    def __init__(self, id):
        self.id = id
        self.by_id[id] = self

What I'd like to be able to do is iterate over the existing instances of the class. I can do this with:

for foo in Foo.by_id.values():
    foo.do_something()

but it would look neater like this:

for foo in Foo:
    foo.do_something()

is this possible? I tried defining a classmethod __iter__, but that didn't work.

glglgl
  • 89,107
  • 13
  • 149
  • 217
xorsyst
  • 7,897
  • 5
  • 39
  • 58

5 Answers5

34

If you want to iterate over the class, you have to define a metaclass which supports iteration.

x.py:

class it(type):
    def __iter__(self):
        # Wanna iterate over a class? Then ask that class for iterator.
        return self.classiter()

class Foo:
    __metaclass__ = it # We need that meta class...
    by_id = {} # Store the stuff here...

    def __init__(self, id): # new isntance of class
        self.id = id # do we need that?
        self.by_id[id] = self # register istance

    @classmethod
    def classiter(cls): # iterate over class by giving all instances which have been instantiated
        return iter(cls.by_id.values())

if __name__ == '__main__':
    a = Foo(123)
    print list(Foo)
    del a
    print list(Foo)

As you can see in the end, deleting an instance will not have any effect on the object itself, because it stays in the by_id dict. You can cope with that using weakrefs when you

import weakref

and then do

by_id = weakref.WeakValueDictionary()

. This way the values will only kept as long as there is a "strong" reference keeping it, such as a in this case. After del a, there are only weak references pointing to the object, so they can be gc'ed.

Due to the warning concerning WeakValueDictionary()s, I suggest to use the following:

[...]
    self.by_id[id] = weakref.ref(self)
[...]
@classmethod
def classiter(cls):
    # return all class instances which are still alive according to their weakref pointing to them
    return (i for i in (i() for i in cls.by_id.values()) if i is not None)

Looks a bit complicated, but makes sure that you get the objects and not a weakref object.

glglgl
  • 89,107
  • 13
  • 149
  • 217
  • What about the weak references you were talking about? I wasn't quite sure what you meant, and this answer also doesn't contain anything about it – Niklas B. May 30 '12 at 10:49
  • See this on weak references: http://docs.python.org/library/weakref.html#weakref.WeakValueDictionary – Marcin May 30 '12 at 10:51
  • 4
    Nice touch, delegating iteration back to a method on the class. – Martijn Pieters May 30 '12 at 10:51
  • @NiklasB. While normal references are stored in the `by_id` structure, they will never be garbage collected. – Marcin May 30 '12 at 10:54
  • @Marcin: Yeah, I must have overseen the fact that they are stored inside the class, not an instance... – Niklas B. May 30 '12 at 10:55
  • 1
    I know I suggested weakref.WeakValueDictionary, but note the warning. It might actually be better to store weakrefs, and dereference them in the iterator. – Marcin May 30 '12 at 10:55
  • 2
    PEP8 recommends never inventing names with double leading and trailing underscores like `__classiter__`. – martineau May 30 '12 at 11:49
13

Magic methods are always looked up on the class, so adding __iter__ to the class won't make it iterable. However the class is an instance of its metaclass, so the metaclass is the correct place to define the __iter__ method.

class FooMeta(type):
    def __iter__(self):
        return self.by_id.iteritems()

class Foo:
    __metaclass__ = FooMeta
    ...
Euphe
  • 3,531
  • 6
  • 39
  • 69
John La Rooy
  • 295,403
  • 53
  • 369
  • 502
4

Try this:

You can create a list with a global scope, define a list in the main module as follows:

fooList = []

Then add:

class Foo:
  def __init__(self):
    fooList.append(self)

to the init of the foo class

Then everytime you create an instance of the Foo class it will be added to the fooList list.

Now all you have to do is iterate through the array of objects like this

for f in fooList:
    f.doSomething()
tperera11
  • 143
  • 1
  • 8
3

You can create a comprehension list and then call member methods as follows:

  class PeopleManager:         
        def __init__(self):       
           self.People = []        
        def Add(self, person):     
           self.People.append(person) 

  class Person:                
        def __init__(self,name,age): 
           self.Name = name          
           self.Age = age   

  m = PeopleManager()
  [[t.Name,t.Age] for t in m.People]

call to fill the object list:

  m = PeopleManager()
  m.Add( Person("Andy",38))
  m.Add( Person("Brian",76))
Max Kleiner
  • 1,442
  • 1
  • 13
  • 14
1

You can create a class list and then call append in the init method as follows:

class Planet:
  planets_list = []

  def __init__(self, name):
     self.name = name
     self.planets_list.append(self)

Usage:

p1 = Planet("earth")
p2 = Planet("uranus")

for i in Planet.planets_list:
    print(i.name)
Praveen Perera
  • 158
  • 3
  • 13