Iterables New Teacher in Town Exercise

Link to exercise: https://www.codecademy.com/courses/learn-intermediate-python-3/projects/new-teacher-in-town-project

I am currently on step 3.
The instructions ask me to: Next, we want to create a simple way to run through morning roll call, by ordering all students by first name alphabetically. When you iterate on your ClassroomOrganizer object, it should return each student’s name one at a time on each next() call or for loop iteration.

Define the iterator protocol for our ClassroomOrganizer class that can achieve this. Once defined, use either next() calls or a for loop on the ClassroomOrganizer object to print out the next student on the roll call.

→ I am trying to understand what it means by define the interator protocol for our Classroom Organizer class.

I have attempted the code below but it does not activate the functions of this class.

classroom_organizer_object = iter(student_roster)
  for i in classroom_organizer_object:
    print(i)

what do they want me to do?

1 Like

Just looking at all the provided code we discover that an instance of the ClassroomOrganizer class has a sorted_names attribute. So here is what I did for step 3:

s = ClassroomOrganizer()
for x in s.sorted_names:
  print (x)

Bear in mind that the alternate looping method is also available: an iterator.

x = iter(s.sorted_names)
while True:
    try:
        print (next(x))
    except StopIteration:
        break

I’m still going down through the steps, but I am convinced this is what they are asking for in that step. Whether we will actually need the code is yet to be determined. I don’t think we will, but it is an exercise, at least.

3 Likes

It certainly works! Thank you for this! :slight_smile:

In the exercise instructions it says to define the iterator protocol.

Per the prior lesson content material…

If we desire to create our own custom iterator class, we must implement the iterator protocol, meaning we need to have a class that defines at minimum the iter() and next() methods.

The iter() method must always return the iterator object itself. Typically, this is accomplished by returning self. It can also include some class member initializing.

The next() method must either return the next value available or raise the StopIteration exception. It can also include any number of operations.

I would think that it would have me do those steps. But I have been lost on how to do them. Would you have any ideas?

1 Like

It’s been years since I did this project and without resetting cannot tell what code is mine, and what code was supplied. Can you please post the code from classroom_organizer.py? It will help me to help you.

1 Like
from roster import student_roster
import itertools
# Import modules above this line
class ClassroomOrganizer:
  def __init__(self):
    self.sorted_names = self._sort_alphabetically(student_roster)

  def _sort_alphabetically(self,students):
    names = []
    for student_info in students:
      name = student_info['name']
      names.append(name)
    return sorted(names)

  def get_students_with_subject(self, subject):
    selected_students = []
    for student in student_roster:
      if student['favorite_subject'] == subject:
        selected_students.append((student['name'], subject))
    return selected_students

s = ClassroomOrganizer()
for i in s.sorted_names:
  print(i)


1 Like

Okay, we see where the iterator protocol will be inserted, right after the __init__() method. Go ahead and write the signature lines for two new methods. __iter__() and __next__(). Just give them a pass for the body. Show us what you have and we can continue.

1 Like
from roster import student_roster
import itertools
# Import modules above this line
class ClassroomOrganizer:
  def __init__(self):
    self.sorted_names = self._sort_alphabetically(student_roster)

  def __iter__():
    pass
  
  def __next__():
    pass
    
  def _sort_alphabetically(self,students):
    names = []
    for student_info in students:
      name = student_info['name']
      names.append(name)
    return sorted(names)

  def get_students_with_subject(self, subject):
    selected_students = []
    for student in student_roster:
      if student['favorite_subject'] == subject:
        selected_students.append((student['name'], subject))
    return selected_students

s = ClassroomOrganizer()
for i in s.sorted_names:
  print(i)
1 Like

I have done so. Text requirement for 20 word minimum. Text requirement for 20 word minimum.

If you can still edit that post, please format the code sample. Select the code and click the </> button in the tool tray.

1 Like

Okay, so what would be the default parameter for those two methods? It’s an easy question.

I’d like to invite a member who is much more proficient at Python under the hood than I am. @tgrtim. His guidance will be more in depth than any I can offer.

self would be a default parameter.

however, the lesson examples used fish lists and passed in fishlists as an argument. I wonder if this problem will also require another parameter such as studentlist or something.

Do you have a link to the Iterators lesson? I need to do some review before I get into deep water as this is a rather technical project. Hopefully, the other member will join in soon to carry some of the burden.

1 Like

https://www.codecademy.com/courses/learn-intermediate-python-3/lessons/iterables-and-iterators/exercises/introduction-to-iterables

2 Likes

In the meantime, have a look at this topic:

FAQ: Iterables and Iterators - Custom Iterators II - #6 by tgrtim

1 Like

tgrtim seems to have resonded in the discord. The opening statement being, I really, really dislike this lesson.

  1. I really really dislike this lesson. The instructions push you to create a broken iterator which violates Python’s iterator protocol (a.k.a. rules) so you can’t use it properly. Here’s my opinion on it (unless they’ve changed the lesson it’s still a mistake)- New teacher in town project under LEARN INTERMEDIATE PYTHON 3 - #25 by tgrtim There’s a couple of things you want to know beforehand. An iterable is an object that lets you call iter() on it, use it in for loops etc. An iterator would be an object that (typically) contains both an __iter__ method and a __next__ method. An instance of this iterator should return each element in turn as next is called, usually via next() not the method itself. It’s very difficult to create an abstract like an iterator in the same class as the actual data is stored. It’s almost always a second object that references the other class. For reference this is how most Python objects work, e.g. calling iter() on a list object returns a special ListIterator object. Note that it’s a different object with a different type

tgrtim Today at 1:52 PM

Here’s a bit more info about the kind of issue that can occur when you break this protocol (a very very simple example but it risks hidden bugs which are truly horrible - FAQ: Iterables and Iterators - Custom Iterators II - #6 by tgrtim (edited)

Codecademy Forums

FAQ: Iterables and Iterators - Custom Iterators II

You might already be aware but a for loop effectively calls the iter() function on the target of the loop. So a for loop is basically equivalent to a while loop of the following style- it = iter(customer_counter) while True: try: customer_count = next(it) # execute statements in body of loop… except StopIteration: …

FAQ: Iterables and Iterators - Custom Iterators II

@mtf he appears to be offline. I will try asking my tutor tomorrow. thank you for your help.

2 Likes

We need to accept that Python has its own built in iterator, iter() which object instance has its own __iter__() and __next__() methods. It is the secret sauce that makes for loops possible. This is mentioned but needs to be digested and assimilated in order to be able to move on. This is a fundamental concept that deserves to be well understood, and especially given the appropriate implementation in our custom classes.

It is so clear to me that something is missing in the lesson/project narrative, or if it is present, escaping us all. If the author of this project is not willing to divulge it, then it is paramount that we take the time to get this right. Until I have the wherewithal needed here, it is only proper for me to wait for/seek out other leads while I review the unit lessons.

Bottom line, don’t yield to this project, even while it is possible to give a pass. The stuff of the lessons needs to be ground down to its finest powder. If you and your tutor break any ground, do please share so it becomes part of the conversation. This is groundbreaking if we all walk away with the right understanding so we don’t create bugs in our future work.

1 Like

Trying very hard to treat this with the considerations presented in @trtim’s post:

class ClassroomIterator:
  def __iter__(self, iterable):
    pass
  def __next__(self):
    pass

That is as far as I’ve got. We need to work this out so that ‘self’ is returned as an iterator.


What is our custom iterator doing that goes above and beyond what the built in one promises? This is the area I think we need to focus on since it must be the reason we’re having the discussion, in the first place: Custom iterators.


When we combine iterators with function factories and factory functions we gain a trifecta of processing power that uses very little memory and leaves no footprint when it’s done its job. That is why it is so important that we nail down this aspect, the custom iterator. If I didn’t stress it before, I’m stressing it now. We need to carry out this study to some satisfactory conclusion before brushing it aside.

1 Like

Sorry only just catching these messages.

I’ve tried to cover the background issue in this post, see the next one for possible solutions.


If we desire to create our own custom iterator class, we must implement the iterator protocol, meaning we need to have a class that defines at minimum the __iter__() and __next__() methods.

The __iter__() method must always return the iterator object itself. Typically, this is accomplished by returning self. It can also include some class member initializing.

The __next()__ method must either return the next value available or raise the StopIteration exception. It can also include any number of operations.

This does describe an iterator but the problem I had with this lesson is it tries to (or at least used to) squeeze the iterator behaviour into the ClassroomOrganizer class which is really a container type, see e.g. https://docs.python.org/3/reference/datamodel.html?emulating-container-types#emulating-container-types. They should really be kept separate or you’ll probably never meet Python’s protocol for iterators.

So the closest match to the lessons in my eyes would be to create two classes, one ClassroomOrganizer that stores and orders the data and a second class, an iterator template than can access the data within a ClassroomOrganizer instance.

Bear in mind this is exactly how Python currently treats normal collections/containers (ignoring any precise definition here). Container types like list or perhaps a very similar tool like collections.OrderedDict (a standard module class that acts as a container which can order it’s contents) are iterable. They are not themselves iterators, that task is handled by other types.

To see these actual different iterator types in practice…

from collections import OrderedDict
foo = iter(list())
bar = iter(OrderedDict())

# Note the types here are not list or OrderedDict but distinct iterators
print(f"{type(foo)!r}")  # <class 'list_iterator'>
print(f"{type(bar)!r}")  # <class 'odict_iterator'>

This is really an implementation detail but it matters because the behaviour of iterables and iterators is quite different. For a start iterables have no __next__ method (that’s reserved for actual iterators) and calling next() on an iterable will simply throw exceptions e.g.

x = [1, 2, 3,]
next(x)  # Exception thrown

In addition to the behaviour of next() iterable container types behave differently to iterators when you call iter() on them. You can get typically get several different independent iterators from a single iterable (maybe a couple of CPython things violate this but it’s rare)-

# Standard containers return new iterators on each call-
x = [1, 2, 3,]
y = iter(x)
z = iter(x)
print(y is z)  # False
print(next(y), next(y))  # 2
# The z iterator instance is independent of y
print(next(z))  # 1

On the other hand iterators should return themselves…

x = [1, 2, 3,]
y = iter(x)
iter_y = iter(y)  # called on an iterator!
print(next(y))  # 1
# It's the SAME ITERATOR!
print(next(iter_y))  # 2

In general if you’re defining a new container/collection then making it iterable is wise, making it an iterator probably isn’t (I won’t swear to every possible situation but following this pattern for custom container types is normally a good idea).

Some potentially useful reading-

https://docs.python.org/3/reference/datamodel.html?emulating-container-types#emulating-container-types

https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes
https://docs.python.org/3/library/stdtypes.html#typeiter
https://docs.python.org/3/glossary.html#term-iterator
https://docs.python.org/3/glossary.html#term-iterable

Iterator - Wikipedia
Iterator pattern - Wikipedia
Collection (abstract data type) - Wikipedia
Container (abstract data type) - Wikipedia

Long story short, for the requirements of this lesson (define a custom iterator) I suggest using at least two classes (an iterable and an iterator).

3 Likes

Here’s a quick and dirty example of defining a custom iterator. I’ve not really tested it so I may have fudged the indexing-

1. Custom iterator type example
class ClassroomOrganiser:
    
    ... # rest remains the same
    
    def __iter__(self):  # this class is now ITERABLE
        return ClassroomIterator(self)


class ClassroomIterator:

    def __init__(self, target):
        self._target = target
        self._index = -1
        self._max_index = len(self._target.sorted_names) - 1
        
    def __iter__(self):  # iterator behaivour
        return self
    
    def __next__(self):  # now an ITERATOR
        if self._index < self._max_index:
            self._index += 1
            return self._target.sorted_names[self._index]
        else:
            raise StopIteration

Outside this lesson though for a simple container like this I’d consider calling iter() on the underlying list and you don’t need to run around defining your own types and implementing __next__() methods.

2. Example of re-using existing iterable types-
class ClassroomOrganiser:

    ...  # rest of class stays the same

    def __iter__(self):
        # self.sorted_names is a list
        # we can simply return iter() called on this list
        # it provides an iterator over this list
        return iter(self.sorted_names)
    
    # ClassroomOrganiser is now an iterable (not an iterator)

In this example data is stored in an iterable type anyway but when it isn’t Python’s generators are a great way to create new iterators and a strong alternative to explicitly defining them (they save you the trobule of creating and maintaining additional iterator classes, there is some trade off of course).

3. Silly generator example

A neat trick is to define a generator within __iter__. This example is silly but the idea remains solid, just define a generator that accesses and yields elements from the main data type in whatever order you wish-

class ClassroomOrganiser:

    ...  # rest of class stays the same

    def __iter__(self):
        yield from self.sorted_names

You can of course create a separate generator definition outside the class (instead of with __iter__). It all depends on the use case.

A custom iterator may be better if you’re planning on using inheritance or something but re-using existing iterables where you can is wise. Generators can be convenient but there is that slight trade off no inheritance, docstring differences etc.

3 Likes