I do mathematical biology curriculum development and design and sometimes teach the associated computer labs. The students are very much beginners. Recently, I have had to start working with a Python 3-based system rather than a Python 2.7-based one. Some of the changes in Python 3 have made basic functions much more abstract. I can't have students just type in zip([1,2,3], [4,5,6])
to see what it does. (I had them do list(zip([1,2,3], [4,5,6]))
, but that's really changing the output of zip
into something else.) And range
no longer outputs a list -- instead, it outputs something that counts, but again, is quite abstract. How would you explain these things to total beginners?
4 Answers
I don't think the concepts behind generators, sequences and iterators are too difficult to grasp if you're clear on what exactly they are.
The 'old' range(n)
function of Python 2 just produces a list of numbers:
[0, 1, 2, 3, ... , n - 1]
You ask it to give you a list of numbers from $0$ to $n - 1$, and it gives you a list
.
The new range(n)
doesn't do that. Instead, it makes an object that knows the start, end and step that you asked for... but it doesn't give you a list! This object will, however, give you the next element of the sequence if you ask for it.
Running list(range(n))
specifically asks for the list of all the integers, but in the vast majority of cases, you don't need the list, so there's no point making it and storing the whole thing in memory.
Imagine you wanted to print the numbers 1 to 1000. Python 2's range()
would generate every number up front, before you can begin iterating. Clearly, this is very wasteful and pointless; it's much easier to work out each number at the end of the iteration, and doesn't require nearly as much memory.
However, I suppose that the merits of the feature aren't really relevant or important to absolute beginners; it's better that they gain some understanding of how to use the feature before understanding the merits behind it. Instead, consider explaining like this:
Iterables (such as
range
andzip
) only do work when specifically asked (they're lazy — if your students have had any exposure to functional programming, that might be familiar)Generally, being lazy makes things faster, because you don't do unnecessary work (e.g. in case you quit the loop early).
If you specifically need the whole list, you can ask for it with
list(iterable)
— but this is usually not as common as just iterating through withfor
.
If your students know Python 2's xrange
, they'll find range
simple to understand.
-
$\begingroup$ They definitely don't know
xrange
or anything about functional programming. Depending on the class, they either have never programmed before at all or have a rudimentary background. $\endgroup$– jaiaJan 11, 2018 at 19:37
As suggested, moved from comment to answer.
A range is exactly that - a definition of a beginning and an end point. The 2.7 default behavior made it look as if it were a list, and confused the issue. To new programmers, there is no "change", it is simply how things are. Only those with prior expectations will be confused.
My perspective is someone who has no practical experience in Python, but decades of OO programming experience. I would find the 2.7 implementation of range confusing because a list is not inherently a range, although a range could be implemented as a list with a length of two (beginning and end).
-
$\begingroup$ The thing which is confusing you isn't the implementation but the name.
range
doesn't return a range in the sense of an object which represents a continuous subset of the real line. It returns an iterable/enumerable object which can be used to use foreach loops instead of C-style for loops for fixed start and end points. $\endgroup$ Jan 23, 2018 at 17:26 -
$\begingroup$ Exactly. The 2.7 implementation is not a "range" in the strict sense of the word, so you have to know the library to use it. The 3.0 implementation is exactly a "range" in the strictest sense of the word, so anyone can read the code and see what it's doing. $\endgroup$– pojo-guyJan 24, 2018 at 5:03
A common misconception many people have is that Python 3's range(...)
function returns an iterator of some kind. This is actually false: it returns a custom sort of sequence object (which, by definition, is also an iterable). What this means is that I think it actually is legitimate to teach range(...)
as being basically a "list-like" thing.
More precisely, it returns an object that actually supports almost the same set of operations Python's list
object supports -- the only exceptions are that any methods that allow mutation (such as .append(...)
) are missing, and the .copy()
method is missing for some reason.
More precisely, the range object...
- ...supports O(1) indexing
- ...supports slicing
- ...can be iterated over multiple times, just like a list. They aren't "consumed"after one pass (After all, ranges are not only iterable, they're sequences).
- You can check two ranges against each other by equality
- It supports a wide variety of dunder methods -- for example, doing
4 in range(3, 5)
orreversed(range(3, 10))
both work more or less as expected.
What this means is that if you're teaching how to use range(...)
, you can skip having to talk about generators/the whole iterable vs iterator thing entirely, and just introduce range
as a special sort of list that's specifically optimized to represent a large range of numbers w/o needing to consume a huge amount of memory.
This means you still need to introduce a new concept ("oh btw, here's another list-like thing"), but hopefully this shouldn't be as new of a concept if you're also planning on introducing things like tuples or sets.
You could even use this as a way of introducing the notion of objects/how everything in Python is an object. For example, you could introduce the concept of fields and methods using range
's .start
, .stop
, and .step
attributes and .count(...)
and .index(...)
methods.
For zip, no such luck, I'm afraid. The zip(...)
function genuinely does return an iterator (e.g. an object that's consumed after one iteration).
But given how zip
is really more of a convenience feature more then anything, you could perhaps defer talking about it until later once students are comfortable with the notion of iterating over various collection types.
...but that's really changing the output of zip into something else.
You can print the value returned by zip(...)
. It just isn't as transparent as it used to be.
Python 3.5.3 (default, Jan 19 2017, 14:11:04)
[GCC 6.3.0 20170118] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> z = zip([1, 2, 3], ["x", "y", "z"])
>>> repr(z)
'<zip object at 0x7f197ce6f688>'
You shy away from "turning it into something else," but really? You only have to type eight more keystrokes in the repl:
>>> list(z)
[(1, 'x'), (2, 'y'), (3, 'z')]
>>>
And anyway, that's how it works. You have to turn it into something else. Python3 is not just a bunch of new features added to Python2: It's a new language.
It's a better language---one where you can do this:
>>> z = zip(range(10000000000), range(10000000000))
>>> z.__next__()
(0, 0)
>>> z.__next__()
(1, 1)
-
$\begingroup$ Yes, a new language, and a better one. Think anew about how you teach it. $\endgroup$– BuffyFeb 16, 2018 at 21:27
range(10)
into the interactive shell (to see what it is), but it now repliesrange(0,10)
. When in the past it showed a list[0,1,2,3,4,5,6,7,8,9]
. This made it possible to see that it could be replaced by a list. $\endgroup$