Oops
Oops
and OOP
FUNCTIONS
Before we start, let’s talk about how name resolution is done in Python: When a function
executes, a new namespace is created (locals). New namespaces can also be created by
modules, classes, and methods as well.
LEGB Rule: How Python resolves names.
• Local namespace.
• Enclosing namespaces: check nonlocal names in the local scope of any enclosing functions
from inner to outer.
• Global namespace: check names assigned at the top-level of a module file, or declared global
in a def within the file.
• __builtins__: Names python assigned in the built-in module.
• If all fails: NameError.
FUNCTIONS AS FIRST-CLASS
OBJECTS
We noted a few lectures ago that functions are first-class objects in Python. What
exactly does this mean?
In short, it basically means that whatever you can do with a variable, you can do
with a function. These include:
• Assigning a name to it.
• Passing it as an argument to a function.
• Returning it as the result of a function.
• Storing it in data structures.
• etc.
FUNCTION FACTORY
a.k.a. Closures. def make_inc(x):
def inc(y):
As first-class objects, you can wrap
# x is closed in
functions within functions.
# the definition of inc
Outer functions have free variables that return x + y
are bound to inner functions. return inc
A closure is a function object that inc5 = make_inc(5)
remembers values in enclosing scopes inc10 = make_inc(10)
regardless of whether those scopes are
still present in memory. print(inc5(5)) # returns 10
print(inc10(5)) # returns 15
CLOSURE
Closures are hard to define so follow these three rules for generating a closure:
1. We must have a nested function (function inside a function).
2. The nested function must refer to a value defined in the enclosing function.
3. The enclosing function must return the nested function.
DECORATORS
Wrappers to existing functions. def say_hello(name):
return "Hello, " + str(name) + "!"
You can extend the functionality
of existing functions without def p_decorate(func):
having to modify them. def func_wrapper(name):
return "<p>" + func(name) + "</p>"
return func_wrapper
my_say_hello = p_decorate(say_hello)
print my_say_hello("John")
# Output is: <p>Hello, John!</p>
DECORATORS
Wrappers to existing functions. def say_hello(name):
return "Hello, " + str(name) + "!"
You can extend the functionality
of existing functions without def p_decorate(func):
having to modify them. def func_wrapper(name):
return "<p>" + func(name) + "</p>"
return func_wrapper
my_say_hello = p_decorate(say_hello)
Closure print my_say_hello("John")
# Output is: <p>Hello, John!</p>
DECORATORS
So what kinds of things can we use decorators for?
• Timing the execution of an arbitrary function.
• Memoization – cacheing results for specific arguments.
• Logging purposes.
• Debugging.
• Any pre- or post- function processing.
DECORATORS
Python allows us some nice def say_hello(name):
syntactic sugar for creating return "Hello, " + str(name) + "!"
decorators.
def p_decorate(func):
def func_wrapper(name):
return "<p>" + func(name) + "</p>"
return func_wrapper
my_say_hello = p_decorate(say_hello)
Notice here how we have to explicitly
print my_say_hello("John")
decorate say_hello by passing it to
our decorator function. # Output is: <p>Hello, John!</p>
DECORATORS
def p_decorate(func):
Python allows us some nice def func_wrapper(name):
syntactic sugar for creating return "<p>" + func(name) + "</p>"
decorators. return func_wrapper
@p_decorate
def say_hello(name):
Some nice syntax that return "Hello, " + str(name) + "!"
does the same thing,
except this time I can print say_hello("John")
use # Output is: <p>Hello, John!</p>
say_hello instead of
assigning a new name.
DECORATORS
You can also stack decorators with the closest decorator to the function definition
being applied first.
@div_decorate
@p_decorate
@strong_decorate
def say_hello(name):
return “Hello, ” + str(name) + “!”
print say_hello("John")
# Outputs <div><p><strong>Hello, John!</strong></p></div>
DECORATORS
We can also pass arguments to decorators if we’d like.
def tags(tag_name):
def tags_decorator(func):
def func_wrapper(name):
return "<"+tag_name+">"+func(name)+"</"+tag_name+">"
return func_wrapper
return tags_decorator
@tags("p")
def say_hello(name):
return "Hello, " + str(name) + "!"
print say_hello("John")
DECORATORS
We can also pass arguments to decorators if we’d like.
def tags(tag_name):
def tags_decorator(func):
def func_wrapper(name):
return "<"+tag_name+">"+func(name)+"</"+tag_name+">"
return func_wrapper
return tags_decorator
print say_hello("John")
ACCEPTS EXAMPLE
Let’s say we wanted to create a general purpose decorator for the common operation
of checking validity of function argument types.
import math
def complex_magnitude(z):
return math.sqrt(z.real**2 + z.imag**2)
>>> complex_magnitude("hello")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "accepts_test.py", line 4, in complex_magnitude
return math.sqrt(z.real**2 + z.imag**2)
AttributeError: 'str' object has no attribute 'real'
>>> complex_magnitude(1+2j)
2.23606797749979
ACCEPTS EXAMPLE
def accepts(*arg_types):
def arg_check(func):
def new_func(*args):
for arg, arg_type in zip(args,arg_types):
if type(arg) != arg_type:
print "Argument", arg, "is not of type", arg_type
break
else:
func(*args)
return new_func
return arg_check
If you are familiar with OOP in C++, for example, it should be very easy for you to
pick up the ideas behind Python’s class structures.
CLASS DEFINITION
Classes are defined using the class keyword with a very familiar structure:
class ClassName:
<statement-1>
. . .
<statement-N>
class MyClass:
""""A simple example class docstring"""
i = 12345
def f(self):
return 'hello world'
I can create a new instance of MyClass using the familiar function notation.
x = MyClass()
CLASS OBJECTS
I can access the attributes and methods of my object in the following way:
>>> num = x.i
>>> x.f()
We can define the special method __init__() which is automatically invoked
for new instances (constructor).
class MyClass:
"""A simple example class"""
i = 12345
def __init__(self):
print "I just created a MyClass object!"
def f(self):
return 'hello world'
CLASS OBJECTS
Now, when I instantiate a MyClass object, the following happens:
>>> y = MyClass()
I just created a MyClass object!
There are also some built-in functions we can use to accomplish the same tasks.
class Pet:
def __init__(self, name, age):
self.name = name
self.age = age The __str__ built-in function
def get_name(self): defines what happens when I
return self.name print an instance of Pet. Here I’m
def get_age(self): overriding it to print the name.
return self.age
def __str__(self):
return "This pet’s name is " + str(self.name)
PET EXAMPLE
>>> from pet import Pet
>>> mypet = Pet('Ben', '1')
>>> print mypet
Here is a simple class that defines a Pet object.
This pet's name is Ben
>>> mypet.get_name()
class Pet: 'Ben'
def __init__(self, name, age): >>> mypet.get_age()
self.name = name 1
self.age = age
def get_name(self):
return self.name
def get_age(self):
return self.age
def __str__(self):
return "This pet’s name is " + str(self.name)
INHERITANCE
Now, let’s say I want to create a Dog class which inherits from Pet. The basic format
of a derived class is as follows:
class DerivedClassName(BaseClassName):
<statement-1>
...
<statement-N>
class Dog(Pet):
pass
The pass statement is only included here for syntax reasons. This class definition for
Dog essentially makes Dog an alias for Pet.
INHERITANCE
We’ve inherited all the functionality of our Pet class, now let’s make the Dog class
more interesting.
class Dog(Pet):
def __init__(self, name, age, breed):
Pet.__init__(self, name, age)
self.breed = breed
def get_breed(self):
return self.breed
INHERITANCE
For my Dog class, I want all of the functionality of the Pet class with one extra
attribute: breed. I also want some extra methods for accessing this attribute.
class Dog(Pet):
Overriding initialization function
def __init__(self, name, age, breed):
Pet.__init__(self, name, age)
self.breed = breed
def get_breed(self):
return self.breed
Python resolves attribute and method references by first
searching the derived class and then searching the base
class.
INHERITANCE
For my Dog class, I want all of the functionality of the Pet class with one extra
attribute: breed. I also want some extra methods for accessing this attribute.
class Dog(Pet):
def __init__(self, name, age, breed):
Pet.__init__(self, name, age)
self.breed = breed self.name = name
def get_breed(self): self.age = age
return self.breed
class MappingSubclass(Mapping):
def update(self, keys, values):
for item in zip(keys, values):
self.items_list.append(item)
NAME MANGLING
class Mapping: What’s the problem here?
def __init__(self, iterable):
self.items_list = [] The update method of Mapping accepts
self.update(iterable) one iterable object as an argument.
def update(self, iterable):
for item in iterable: The update method of MappingSubclass,
self.items_list.append(item) however, accepts keys and values as
arguments.
class MappingSubclass(Mapping):
def update(self, keys, values): Because MappingSubclass is derived
for item in zip(keys, values): from Mapping and we haven’t overrided
self.items_list.append(item) the __init__ method, we will have an
error when the __init__ method calls update
with a single argument.
NAME MANGLING
To be clearer, because MappingSubclass inherits
from Mapping but does not provide a definition
class Mapping:
for __init__, we implicitly have the following
def __init__(self, iterable):
__init__ method.
self.items_list = []
self.update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
def __init__(self, iterable):
class MappingSubclass(Mapping):
def update(self, keys, values): self.items_list = []
for item in zip(keys, values): self.update(iterable)
self.items_list.append(item)
NAME MANGLING
This __init__ method references an update
method. Python will simply look for the most
class Mapping:
local definition of update here.
def __init__(self, iterable):
self.items_list = []
self.update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
def __init__(self, iterable):
class MappingSubclass(Mapping):
def update(self, keys, values): self.items_list = []
for item in zip(keys, values): self.update(iterable)
self.items_list.append(item)
NAME MANGLING
The signatures of the update call and the update
definition do not match. The __init__ method
class Mapping:
depends on a certain implementation of update
def __init__(self, iterable):
being available. Namely, the update defined in
self.items_list = []
Mapping.
self.update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
def __init__(self, iterable):
class MappingSubclass(Mapping):
def update(self, keys, values): self.items_list = []
for item in zip(keys, values): self.update(iterable)
self.items_list.append(item)
NAME MANGLING
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
NAME MANGLING
Generators use the yield statement to return results when they are ready, but
Python will remember the context of the generator when this happens.
Even though generators are not technically iterator objects, they can be used
wherever iterators are used.
Generators are desirable because they are lazy: they do no work until the first value
is requested, and they only do enough work to produce that value. As a result, they
use fewer resources, and are usable on more kinds of iterables.
GENERATORS
An easy way to create “iterators”. Use the yield statement whenever data is
returned. The generator will pick up where it left off when next() is called.
def even(data):
for i in range(0, len(data), 2):
yield data[i]
Equivalent to:
def gen(exp):
for x in exp:
yield x**2
g1 = gen(iter(range(10)))