Re: [Edu-sig] Some design patterns with iterators
At 07:44 PM 3/30/2003 -0500, Guido van Rossum wrote: .... Your comments highly welcomed and greatly appreciated!
--
That's a contorted example. Why not change this a bit so that instead of __call__ it has a next method that raises StopIteration when exhausted, and an __iter__ method returning self?
Agreed. Students should realize that objects don't need to be wrapped by iter() in order to perform as iterators. It's sufficient to define a next() method internally, and then assign the object itself as the return of __iter__ -- as you point out. It's also possible to equate __call__ to next, so if the programmer *does* happen to wrap the object inside iter(), even with a sentinel, it won't break -- although the sentinel is irrelevant in this pattern, because now the test raises an exception instead. class Fib2(object): def __init__(self,start=0,next=1,test=lambda x: False): # note revised default for test -- previous default of # 'False' was not callable self.start = start self.next = next self.test = test def next(self): self.start,self.next = self.next, self.start + self.next if self.test(self.start): raise StopIteration return self.start __call__ = next # makes object callable def __iter__(self): return self def testfunc(max): """a factory for test functions""" def t(x): return x > max t.__doc__ = "stop when x > %s" % max return t t1 = testfunc(1000) t2 = testfunc(5000) def looper(iterable): for i in iterable: print i, print # newline when done def tests(): print t1.__doc__ looper(Fib2(test=t1)) # note: no iter() wrapper -- Fib2 is already iterable print t2.__doc__ looper(iter(Fib2(test=t2),-1)) # but we can wrap it without problems print t2.__doc__ looper(iter(Fib2(test=t2))) # with or without a sentinel Having a sentinel where it's not needed is of course a sign of programmer confusion. But one might argue the object itself is "more robust" this way.
def fibfunc(test): data = [1,1] while not test(data[0]): yield data[0] data[0],data[1] = data[1],data[0]+data[1] return
Comment here: instead of using a two-element list (which could just as well be a tuple!), use two variables. Faster and clearer IMO:
def f(test): last, next = 1, 1 while not test(last): yield last last, next = next, last+next
Yes, clearer. The list-based approach was left over from a version which makes the initial two-value seed a parameter as in: def iterseq(seed=[0,1],test=lambda x: False): # but even so, it's easier to read with two variables: last,next = seed while not test(last): yield last last, next = next, last+next With this new freedom we can generate the Lucas Numbers, with seed [2,1] instead of [1,1]: http://www.mcs.surrey.ac.uk/Personal/R.Knott/Fibonacci/lucasNbs.html def tests(): #... print "Lucas Numbers" looper(iterseq([2,1],t1))
iters.tests() ... Lucas Numbers 2 1 3 4 7 11 18 29 47 76 123 199 322 521 843
A modification lets us iterate until a last/next ratio is reached through convergence -- need to be careful here, as infinite loops often result from equating floating point numbers. def iterseq2(seed=[1,1],test=lambda x,y: False): # variation, where test looks at both last and next last,next = seed while not test(last,next): yield "%s/%s" % (last,next) last, next = next, last+next # probaby overkill to wrap a function here, but I'm wanting to *not* rely on a # global value for phi, nor do I want to keep re-evaluating a square root with # every test -- so I pass it in explicitly and build a function around it. def testratio(terminus): def t(inta,intb): return (intb/inta) == terminus # uses new division # it'd be safer to test for abs(difference) < tolerance return t t3 = testratio(0.5*(1+pow(5,.5))) # phi = (1 + sqrt(5))/2 def tests(): # ... print "Converging..." looper(iterseq2([1,1],t3)) # Fibonacci seed, convergence test Converging... 1/1 1/2 2/3 3/5 5/8 8/13 13/21 21/34 34/55 55/89 89/144 144/233 233/377 377/610 610/987 987/1597 1597/2584 2584/4181 4181/6765 6765/10946 10946/17711 17711/28657 28657/46368 46368/75025 75025/121393 121393/196418 196418/317811 317811/514229 514229/832040 832040/1346269 1346269/2178309 2178309/3524578 3524578/5702887 5702887/9227465 9227465/14930352 14930352/24157817 24157817/39088169 39088169/63245986 63245986/102334155 The Lucas Number next/last ratios likewise converge to phi.
Are your students familiar with functions as first-order objects at this point? It would be simpler if you could simply pass the limit as the argument to Fib() rather than having to construct a test function first. Sure, a test function allows more freedom, but the API design here doesn't actually make it much easier to do something different in the test function -- e.g. another useful termination test would be to produce the first N numbers, but that would require the test function to be stateful, and the API doesn't promise that test() is only called once per iteration.
Yes, I'm sort of demonstrating two concepts -- iterators, and the fact that functions may be passed as parameters. There's more convolution as a result. Plus the above example points up a weakness in this design: the passed-in function is relying on a hard-wired set of arguments, is in test(last), whereas we might want access to both last and next for our test. Our only access to the local scope, within the generator function, is based on the arguments to test() -- a limit on our freedom. I do find it interesting that objects don't suffer as much from this limitation, as one has access to all the object's instance variables through 'self' -- so it's possible to write a top-level test function knowing that 'self' is the only argument needed -- yet internal to the test function, you may access self's scope at will: class Fib3(object): def __init__(self,start=0,next=1,test=lambda x: False): self.start = start self.next = next self.test = test def next(self): self.start,self.next = self.next, self.start + self.next if self.test(self): # single parameter sufficient to access __dict__ raise StopIteration return self.start __call__ = next def __iter__(self): return self def testratio2(terminus): def t(obj): # obj will be the self of the enclosing object return obj.next/obj.start == terminus # uses new division return t But we're just getting more and more convoluted here (useful learning on my end though) ... Kirby
So, based on the foregoing, I'm playing around with the Seq class below as a kind of template iterator. It's designed to accept a passed-in test, or to let the user override the default test method -- or to not supply a test at all. The idea is: you want to generate this sequence, based on some rule. By subclassing Seq, you inherit the iterator qualities you need, so you can just focus on the __init__ and update methods, which define the variables you'll be using, the the rule for going from one term to the next in sequence (hence the name Seq). The other wrinkle is the reset() method, which restores the iterator to its initial state. I'm not sure if this is a good idea, but it seems like it might be useful. Example use:
def t(self): return self.t1>1000
fibs = iters.Fibseq(0,1,t)
for i in fibs: print i,
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
fibs.reset() fibs.next() 1
#---------- iters.py from __future__ import division class Seq(object): """ Subclass and override update() to do the next iteration and to return a next value. Your subclass's __init__ should define whatever instance variables you need, including one named 'test' if you want to pass in a test for the purpose of limiting iteration. Or you may choose to override test in the source code. Iteration stops when the passed in test returns True. The test function, if written externally, should have self as an argument, and refer to your instance variables as if in a class, e.g. as self.variable. """ def next(self): if not self.__dict__.has_key("_savedict"): self._savedict = self.__dict__.copy() if self.test(self): raise StopIteration else: return self.update() def update(self): pass def test(self): return False def __iter__(self): return self def reset(self): if self.__dict__.has_key("_savedict"): self.__dict__ = self._savedict.copy() class Fibseq(Seq): """ Demonstrates how to subclass Seq. In this case, a test will be supplied upon initialization. The user need define only __init__ and update. """ def __init__(self,t1,t2,test): self.t1 = t1 self.t2 = t2 self.test = test def update(self): self.t1, self.t2 = self.t2, self.t1 + self.t2 return self.t1
Here's an email from the front lines. I asked Shelley's permission to post it to edu-sig and she gave it -- and of course I invited her to join this discussion list, which she says she'll plan to do soon, once she gets some other things out of her in box. My reply to Shelley's letter follows (next post). I hope this might generate some further discussion on our list. Shelley will be able to catch up on the thread via the web archives if she likes. Kirby ============================================= Date: Sun, 06 Apr 2003 00:29:48 +0100 From: Shelley Walsh To: urnerk@qwest.net Subject: Python Anxiety Hi Kirby, I don't know whether you remember me, but I have seen your posts many times in the various math education groups, and it was through you that I discovered Python. The first time I saw it, I was absolutely delighted with its potential for helping with math understanding. But lately I have become disillusioned, because I am constantly finding that students don't like Python, and I can't at all figure out why. I thought at first it was because I was teaching students who were very computer illiterate, but then recently I have had the opportunity to teach a Discrete Mathematics for Computing distance education class, and again I saw great potential for using Python to make the abstract ideas more concrete, but again it has fallen flat. Many of these students have programmed in C++ and Java, so you would think they could learn enough Python for what I was suggesting in 5 minutes. I don't know C++ and Java and it only took me a slight bit longer. I have given them so many opportunities for extra credit projects having to do with it that they could all have perfect scores for the class if they wanted, but nobody has taken the bait. But partly this doesn't surprise me, because these distance education students are very lazy. There are a lot of people that do DE for a free ride, and the familiar is always more comforting to such people. But what really shocked me was the experience I had today with my colleagues when I tried to show it to them as something with great potential for help with understanding algebra. I was just showing them how you could use it as something better than a hand calculator for doing such things as solving equations by searching, which I think is a really good idea for keeping students in touch with the base meaning of solving equations. And one of my colleagues practically expressed horror and said that this would totally put him off of mathematics. And others expresses similar opinions. I remember the first time I saw you write about how you could define a function in the console mode def f(x): return x**2, and then proceed to evaluate it on form a composition function, I immediately thought that was just such a great way for students to see such things right in front of their eyes, for them to no longer be abstract. But he seemed to think it would take him hours to master the syntax of it and for the students it would be just one more thing to learn when they were already afraid of the subject. And partly from some of the reactions I have gotten from students, it seems that he is likely to be right. For him the fact that it there is a : and a return instead of just an equal sign was totally daunting and the ** makes it even worse. So my question for you is have you found this kind of Python anxiety, and if so how have you dealt with it? -- Shelley Walsh shelley.walsh9@ntlworld.com http://homepage.mac.com/shelleywalsh
Here was my reply to Shelley, for which she has since thanked me (and given permission for me to share this correspondence). =============== Date: Sat, 05 Apr 2003 17:20:54 -0800 To: Shelley Walsh From: Kirby Urner <urnerk@qwest.net> Subject: Re: Python Anxiety Thanks for your interesting letter Shelley. If you're willing, I'd like to post it to edu-sig, the Python mailing list re Python in education. Or perhaps you'd like to subscribe to it, as here's a community of people interested in this goal. See: http://www.python.org/sigs/edu-sig for more info. Re your specific questions, I have to confess up front that I have very limited personal experience trying to phase in Python in the ways I suggest. I would *like* to have more of these opportunities, but the fact is that I am not now a classroom teacher (I used to be, but that was many years ago). When I do get together in a room to present Python or Python-related topics, chances are they're already sold on the program -- I'm mostly just preaching to the choir as it were. With that confession out of the way, I will attempt to give you some feedback. I think you're encountering two different reactions here, when you talk about (a) trying to teach Python to students who may already have some C++ or Java experience versus (b) showing off Python's potential utility as a math-teaching aid to faculty members in a mathematics department. In the former case, there's some chauvinism in the various language communities. C++ and Java both take longer to become productive in than Python, and are better established in the commercial sector. Python does enjoy a growing following on many fronts, but it's not atypical to encounter dismissive attitudes amidst those who've already made considerable investment in another language. My riposte is that anyone serious about programming needs to keep an open mind and appreciation for multiple languages. The idea of a "monolingual professional programmer" is something of an oxymoron. More specifically, to C/C++ people I'll point out that Python is open source, written in C, and extensible in C/C++, so if you have C/C++ skills, you have lots of opportunities in the Python community, which is inhabited by a great many accomplished and sophisticated C programmers (Python being a wonderful example of C's capabilities). To Java people, I'd point out Jython, a version of Python implemented entirely in Java, and through which one has interactive access to the complete Java class hierarchy. Serious Java programmers needn't leave Java behind in order to avail them- selves of Jython's power, and should realize that schools using Python and Jython are not necessarily competing with the Java community -- on the contrary, they're helping to train a next generation of Java programmer. Also in this context, I might direct these skeptics to Bruce Eckel's web site: http://www.mindview.net/. Here's an excerpt from an interview at this site, which you might find interesting, given your own experience with distance education: One of the things I'm working on now is a distance-learning program for people who want to learn to program using Python. I think it will be a much faster and more efficient way for people to come up the learning curve. This is still in the formative stages; as you might have guessed by now I generally think about something for awhile before the right approach comes to me. Once you've had success with programming and are comfortable with objects, then you're ready to tackle a language like C++ or Java, which is heavier weight and has more arbitrary details for the programmer to master (or become confused by). [ http://www.mindview.net/Etc/About/InformITRaw_html ] So here's a guy with some very deep and meticulous books on both C++ and Java -- who now advocates Python as his current language of choice. Again, Python is not just some toy academic language, nor just a "scripting language" beneath the dignity of serious programmers. It's a very high level general purpose language, and learning it first can make Java and C++ a lot more accessible -- as second or third languages. But with the math teachers, I think the reaction is coming from a different place. If they're horrified by the colon and the return keyword in Python, they'll be even more horrified by all the syntactical clutter of *any* computer language -- even Mathematica, which has gone a long way to accommodate traditional math notation. But as Wolfram points out, traditional notation is ambiguous. Does s(x-1) mean the function s, applied to x-1, or does it mean s times x-1? In Mathematica, when a function is being applied, we use square brackets exclusively, while curved parentheses serve to indicate the order of operations. Whereas humans can tolerate a lot of ambiguity, owing to sensitivity to context, computers cannot. And so in a lot of ways, computers force *more* precision on a notation. With math educators, it does no good to talk about Python's power and sophistication vis-a-vis C++ and Java. Their beef is with the whole idea of diluting the purity of their discipline with material from an alien discipline, i.e. computer science and/or engineering. To start using a computer language in an early math curriculum looks like the harbinger of nothing good: it means math will become mixed up with all kinds incompatible grammars which come and go, vs. the staying power of a more stable, core notation. Plus if computer languages invade the math classroom, then teachers will be forced to learn programming, which many are loathe to take up. The hand held graphing calculator is as far into computing technology as these teachers want to go, and even there, their programmability is often ignored. But not all math educators are on the same page here. Many recognize the advantage of having an "executable math notation" vs. one that just sits there on the printed page, doing nothing (except crying out to be deciphered). Kenneth Iverson makes this point very clearly when he writes: It might be argued that mathematical notation (to be referred to as MN) is adequate as it is, and could not benefit from the infusion of ideas from programming languages. However, MN suffers an important defect: it is not executable on a computer, and cannot be used for rapid and accurate exploration of mathematical notions. Kenneth E. Iverson, Computers and Mathematical Notation, available from jsoftware.com It's this ability of computer languages to promote the "rapid and accurate exploration of mathematical notions" which some of us find exciting and empowering (and if you want to impress math teachers with a wholly alien notation, which nevertheless expresses a lot of the same ideas (sometimes as generally and formally as any traditional notation), have them look at APL or J). Furthermore, one might argue that imparting numeracy is *not* limited to teaching just those topics and notations most traditionally favored within mathematics. It's about imparting some familiarity and comprehension around *whatever* happen to be the culture's primary symbolic notations (music notation included) beyond those which we group under the heading of literacy. We need to provide some exposure to computer languages because our industrial society is completely dependent upon them, because they've become ubiquitous. We have the choice of segregating these topics from mathematics, just as we've divorced mathematics from the physical sciences. But some educators in every generation advocate curriculum integration through cross-pollination, and to such educators, it makes perfect sense to meld computer topics with math topics, with science and even humanities topics (cryptography is a good example of where all of these converge). In my view, the benefits to be obtained through synergy outweigh the arguments of turf-protectors who would keep their respective disciplines "pure". That's the kind of overview debate I think is going on here. Then come the more specific points about the advantages of a computer language such as Python, versus the calculators, which are already pretty well established. Python's abilities with big integers are another selling point. Even though some calculators can work with 100-digit integers, they have a lot more trouble displaying them (more an argument for a big screen over a tiny one). The ability to scroll back through a work session is another advantage. And I think algorithms which involve alphanumeric processing, not just numeric processing, should get more central treatment, including in mathematics. For example, it's fine to express permutations using integers only, but for applications purposes, it's best if we can map 1-27 to the 26 letters and a space. Then you can see how a permutation results in a scrambling of the alphabet -- back to cryptography again (I recommend Sarah Flannery's 'In Code' by the way -- she's very clear about how important Mathematica was to her conceptual development -- which isn't the same as Python of course, but some of the same synergies apply). Thanks again for your interesting letter. If you give your permission, I'll post it, along with my response, to edu-sig, in hopes of generating more discussion along these lines. Or again, perhaps you'd like to subscribe and make a fresh start. I think the attitudes you're running up against are not unique to your experience and it might benefit a lot of us to take part in more of such discussion. Sincerely, Kirby
participants (1)
-
Kirby Urner