This is Python 401: Some Advanced Topics. We're waiting for A/V, which seems more professional this year. Last year's video sucked, hence dreams of importing a chief of operations this time.
Ian just walked in, regrets his hearing aid isn't also a recording device. I might be overdoing it with the "NATO" professor look. I think this sweater vest shrunk in the wash (heaven forbid I'd be bigger).
Wow, data driven precision using * -- hadn't tuned in that feature. He's milking this thing for every feature, reminding us of our heritage (as Pythoneers). I hope these slides are online. Ah, the "dunder dict of an instance may successfully be used as a mapping". Excellent. "Dunder" is for "under under" i.e. "double under".
Python 3 keeps most of this stuff as I recall, let me check:
Ian and I are whispering about the mathematical model for mapping (the formal idea of a function -- more injection than interpolation is what Ian is thinking). I'll mention string.Template in my Python for Teachers.Python 3.0.1 (r301:69556, Mar 14 2009, 14:06:26)
[GCC 4.2.4 (Ubuntu 4.2.4-1ubuntu3)] on linux2
Type "copyright", "credits" or "license()" for more information.
>>> for (i,j) in ((8,2), (10,5), (0,0)):
print("%*.*f" % (i, j, 3.14159))
Python uses the iteration protocol although it honors the older __getitem__ mechanism if the new protocol isn't followed. Good historical perspective here, why it helps to be old. Strings are still not iterables. Iterables vs iterators. WTF?
dir(somelist) dumps dunder iter (__iter__), which is supposed to return an iterator (a list iterator). The returned iterator has a __next__ method, which we might call to yeild successive values in the iterator. Subtle point about how iterators aren't independent, whereas to iterate over an iterable is to return an independent iterator (containing __next__). Got that? You'll hear my voice on the tape around here (presuming you can get one). Iterators are basically generators. iter(iterator) returns itself whereas iter(iterable) returns an iterator.
The rise of iteration and generation in Python has to do with "just in time" supplying of values, meaning you're not committing memory ahead of time -- you do the work when called upon. For example, dict.iteritems and dict.iterkeys don't return lists. Check out itertools.
Remember: enumerate returns an iterator.
>>> n = enumerate(["this", "is", "a", "list"])Django makes use of iterators when returning query objects, doesn't touch the data until it has to. "Just in time" is also called "lazy" or "on demand" in computer science (other industries have their analogies).
['__class__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> b = iter(n)
>>> b is n
I'm wondering if Pythonistas ever feed args to yield to "reset" the a generator or "rewind" it, i.e. it advances forward inexorably, but maybe we want to backtrack? Is that a use pattern people use? I might raise this question. These older Python generators have in internal 'next' versus Python 3.x's __next__ (dunder next -- invoked by next, a top-level builtin-in, i.e. kick = next, kick(can), kick(can)... down the road). In Python 3 again:
>>> def thegen():Side note: when you decorate a function, you lose its __name__ and __doc__. You can fix this with the wraps function in functools. Steve gives us our money's worth.
a = 0
a = a + 1
>>> f = thegen()
>>> dir(f) # note __next__
['__class__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'send', 'throw']
>>> kick = next # next is top-level
functools.partial does what we sometimes call "currying" in computer science (sounds yummy, named for Dr. Haskell (Haskell is also the name of a language)) -- let's see if he uses the term... nope.
Steve is deep in some example of wrapping a partial function object in the context of talking about decorators. Way to go guy.
Time for break.
On to descriptors and properties. "Who could put their hand on their heart and say 'I know how descriptors work'?" Only one or two hands, in a room of a hundred or more. Praise for Guido at this point, for integrating a lot of good thinking into an existing language.
First, the __mro__ discussion (for newstyle classes, the c3 algorithm). However, if you don't find an attribute in dunder dict by climbing the inheritance tree (graph), then find the dunder getattr, otherwise raise an error. New style classes call dunder getattribute. Acknowledgements to Alex Martelli here, for the manga code (runnable psuedo code) giving lookup mechanics.
If the class has a foo in the class, then use its dunder get if it has one, otherwise check for foo in the instance's dunder dict, then start climbing the tree.
You get a bound method when you look up a callable on an instance i.e. a method call carries object creation overhead.
The property function gives us hooks to __get__, __set__ and __delete__, plus a docstring. Read-only attributes are the easiest to create (use the property decorator).
OK, this is interesting: create a Property function, usable as a decorator, that pulls the getter, setter, deleter and docstring from inside a function that defines them. Property(**func) solves the problem of "polluting the namespace". However, in 2.6 we're moving to different technique. Do help(property) for an example. Booting 2.6 now, going help(property):
| Decorators make defining new properties or modifying existing ones easy:Deep Python: instance(object, type), instance(type, object) both return true, even though you can't be circular like this lower down. Good lead-up to metaclasses I suppose...
| class C(object):
| def x(self): return self._x
| def x(self, value): self._x = value
| def x(self): del self._x
Type is a class factory and it's a class. In calling it, we use its __new__ and __init__ ribs. Or does it actually have __call__? When I use type as a factory, am I calling __call__ or __new__? I think __new__. I should get clear on this.
>>> "__call__" in dir(type)We're down to the closing discussion (you'll hear my voice again, asking about __call__ versus __new__). This was a productive session.