6
\$\begingroup\$

In a programme that I am writing, I need to loop through all the instances of a class, the way I got around this was by appending every class to a list like below:

allPeople = []
class Person:

    def __init__(self, name, age, height):
        self.name = name
        self.age = age
        self.height = height
        allPeople.append(self)

Jeff = Person("Jeff", 20, "1.6")
Bob = Person("Bob", 39, "1.4")
Helen = Person("Helen", 19, "1.3")

for person in allPeople:
    print(person.name + " is " + str(person.age))

Is this the best way of doing things or is there a built in python function for this?

\$\endgroup\$
0
10
\$\begingroup\$

Your approach is ok, but I have just one mention (this is based on personal preferences): I'd iterate over a class not a list which is defined outside it. You'll just have to define a metaclass which supports iteration.

class IterPerson(type):
    def __iter__(cls):
        return iter(cls._allPeople)


class Person(metaclass=IterPerson):
    _allPeople = []

    def __init__(self, name, age, height):
        self._allPeople.append(self)

        self.name = name
        self.age = age
        self.height = height

if __name__ == '__main__':
    Jeff = Person("Jeff", 20, "1.6")
    Bob = Person("Bob", 39, "1.4")
    Helen = Person("Helen", 19, "1.3")

    for person in Person:
        print(person.name + " is " + str(person.age))

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 that is the correct place to define the __init__ method.

Please note that I also put a _ in front of your list. This is how we tell python the list we'll create is private. I have also used if __name__ == "main": which is good practice (it is used to execute some code only if the file was run directly, and not imported)

Second, keep this kind of thing as simple as possible. Don't waste a lot of time and energy on something complex. This is a simple problem, keep the code as simple as possible to get the job done.

\$\endgroup\$
4
  • 2
    \$\begingroup\$ Metaclasses are a good way to solve the problem but I'd go further by defining the __new__ (to create an _instances list on the new class automatically) and the __call__ (to populate the _instances list with the created instance automatically) methods on the metaclass so it is both reusable for any class we want to track instanciation of and DRY so the user don't have to remember to put the _allPeople list and populate it by hand. \$\endgroup\$ Apr 19 '16 at 12:47
  • \$\begingroup\$ @Mathias could you please elaborate? I tried to implement your suggestions but I can't seem to get it right \$\endgroup\$
    – YeO
    Sep 12 '18 at 11:53
  • 1
    \$\begingroup\$ @YeO ideone.com/mK2g2h \$\endgroup\$ Sep 12 '18 at 12:01
  • 1
    \$\begingroup\$ While this is clever, I've not seen this "iterate over a class" pattern and it strikes me as unintuitive from the perspective of the client code. Are there examples of this pattern being used out in the wild I can look at to understand the rationale? I'm failing to see the benefit of all the abstraction for something that could basically be an explicit list of dicts/named tuples (or even the simple class) that can be directly iterated over. What is the motivation for hiding allPeople and creating a metaclass? Thanks for the clarification. \$\endgroup\$
    – ggorlen
    Aug 8 '19 at 15:44
5
\$\begingroup\$

I recommend keeping the collection of instances of a class public until there is compelling reason to hide such information from the client, and I see no such motivation here. Furthermore, I'm not convinced that Person is a strong candidate for being a class; it has three properties and no methods.

In keeping the code as explicit and simple as possible, I'd prefer a namedtuple, with a client-code scoped, external list containing all instances that need to be collected:

from collections import namedtuple

Person = namedtuple('Person', 'name age height')
people = [
    Person(name='Jeff', age=20, height=1.6),
    Person('Bob', 39, 1.4),
    Person('Helen', 19, 1.3),
]

for person in people:
    print(f"{person.name} is {person.age}")

Here are the benefits of this approach:

  • Everything is completely obvious for the client, who retains power over how to use the data, clarity and access to the list itself with all that it entails.
  • The documentation for named tuple says "Named tuple instances do not have per-instance dictionaries, so they are lightweight and require no more memory than regular tuples." The data we're dealing here is basically a rudimentary C struct, so all we need is a lightweight named container.
  • Less code, fewer classes, reduced complexity.

From a style standpoint, I recommend adhering to PEP-8--variables are snake_case.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.