## Today

• classes
• decorators
• generators
• iterators
• comprehensions
• files and contexts

## Oh, a couple more expression things

• Python mostly borrows C-style operators, but there are a few it doesn’t
• here is a table of C-style operators and their Python equivalents

C-style Python
++ += 1
-- -= 1
&& and
|| or
! not
• Python has a nice extension to the relational comparison operators too:
• rather than writing x >= 10 && x <= 20
• you can write 10 <= x <= 20
• this would give a syntax error, a type error, or weird behavior in C-style languages
• but this Just Works How You Expect It To in Python
• last, I mentioned == vs. is last time
• in Python, == is how you test if two things look the same
• this applies not only to things like numbers and strings…
• but also lists and dicts
• if you use == on two lists, it will see if they have the same length and contents
• same with dicts, tuples, sets…
• but if you want to see if two variables hold the same object
• that is done with is, and its negated companion is not
• a is b is only true if a and b are literally the same object, i.e. same memory address

## Classes

• Python has a more traditional OOP model
• but it’s still quite a bit different from, say, Java in many regards
• let’s look at the syntax (click that for the example code)
• important things to remember:
• constructors are called __init__
• ALL methods must have self as the first argument
• you must access all fields/methods inside them using self.blah (like this.blah in JS)
• there is no explicit privacy mechanism in Python, but…
• there is a convention of prefixing names with an _underscore to say “this is internal”
• there are also __doubleUnderscoreNames which are treated a little special
• the object model is somewhere between what JS uses and what e.g. Java uses
• it’s kinda like JS in that each object is a dict (map from names to values/functions)
• but there is no prototype relation
• basically, each instance gets its own copies of all the fields/methods
• the methods of an instance are called “bound methods”
• they are “hardwired” to always use that instance as their self parameter
• you can even put that method in a variable and call it:
  c = SomeClass()
m = c.method
m() # self will be c!

• but that’s the basics, and most of the time you won’t really notice the way things work.
• there are lots of extra features on classes in Python…
• lots of special methods (__new__, the rich comparison methods, __hash__, __str__)
• things like properties and metaclasses
• and a common partner for classes are…

## Decorators

• Python is a dynamic language
• you can change classes on the fly
• you can create new classes or functions or whatever
• you can make anything behave like anything else
• a decorator is…
• a function that follows a specific API.
• it takes at least one argument, which will be the thing to be “decorated”
• and it returns one value.
• really, that’s it.
• see this example
• the “magic” happens with this special syntax:

  @deco
def f():
print("hello!")

• in this example, deco is a decorator function.
• what is really happening here is something like…
  def f():
print("hello!")

f = deco(f) # this is the magic bit

• in other words, rather than f being the function you declared…
• the function you declare is passed to the decorator…
• and the decorator’s return value is what ends up in f.
• so decorators can kind of “transparently” add functionality to things.
• decorators can be used on classes and methods as well
• and decorators don’t even have to be functions!
• and don’t have to return the same “kind” of thing they were given!
• this is a technique called metaprogramming
• “using code to create new code”
• but you will see these things in a lot of Python code, so now you know what’s going on

## Generators and Iterators

• what if you had a function that could pause and resume?
• that’d be neat
• that’d be a generator
• a generator is a function in which the yield keyword is used at least once
• yield works like return, except…
• it just pauses the function instead of ending it
• then you can resume at that yield point!
• as the name implies, these are really useful for “generating” sequences of things.
• though you can use them for all kinds of stuff…
• they are a limited form of coroutine
• they can be used to perform collaboratively scheduled user threading (!)
• okay, what’s the “iterator protocol”?
• an iterable object has an __iter__() method which returns an iterator
• which could be itself.
• an iterator has a __next__() method
• calling that method either…
• returns the next item in the sequence
• raises a StopIteration exception
• and that’s it.
• an object can implement these methods and be an iterator
• I hate these
• generators are just so, so much nicer
• lots of built in objects (lists, dicts, tuples, sets etc) satisfy this as well

## Comprehensions

• generators and iterators go hand in hand with comprehensions
• how many times have you used this kind of code pattern?
int[] arr = new int[length];
for(int i = 0; i < length; i++) {
arr[i] = someComputation(i)
}

• a lot, right?
• a comprehension lets you create sets, lists, dicts, and generators in a very compact form.
• the above code in Python might look like:
• arr = [someComputation(i) for i in range(length)]
• nice.
• they look like [expr for _ in _]
• you can also add conditions like [i * i for i in range(10) if (i & 1) == 0]
• but that can get a bit verbose…
• you can also do dicts: {i: i * i for i in range(10)}
• and sets: {i * i for i in range(10)}
• and… NOT tuples (i * i for i in range(10))
• that makes a generator object
• so you could e.g. return that from an __iter__() method.
• anyway comprehensions are neat and compact and you’ll see em a lot.

## Files and Contexts

• File I/O is super common when writing little scripts of your own
• and when you do server-side programming…
• in Python you use the open() function
• looks kinda like C’s fopen
• open(filename, 'r') to read text
• open(filename, 'rb') to read binary
• w and wb to write text/binary
• unlike in C, the files will behave quite differently based on the mode you open them in
• but typically we use the with statement with files
with open("text.txt", "r") as f:
for line in f:
print(line.strip())

• this is a context
• for files, it will ensure f is closed when the with is exited in any way
• this is important when you have to deal with exceptions!!