Adv Functions and OOP
Adv Functions and OOP
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
# x is closed in
wrap functions within functions.
# the definition of inc
Outer functions have free return x + y
variables that are bound to return inc
inner functions.
inc5 = make_inc(5)
A closure is a function object inc10 = make_inc(10)
that remembers values in
enclosing scopes regardless of print(inc5(5)) # returns 10
whether those scopes are still print(inc10(5)) # returns 15
present in memory.
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 def say_hello(name):
functions. return "Hello, " + str(name) + "!"
my_say_hello = p_decorate(say_hello)
print my_say_hello("John")
# Output is: <p>Hello, John!</p>
DECORATORS
Wrappers to existing def say_hello(name):
functions. return "Hello, " + str(name) + "!"
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 def say_hello(name):
nice syntactic sugar for return "Hello, " + str(name) + "!"
creating 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 def func_wrapper(name):
nice syntactic sugar for return "<p>" + func(name) + "</p>"
creating decorators. return func_wrapper
@p_decorate
def say_hello(name):
Some nice syntax return "Hello, " + str(name) + "!"
that does the same
thing, print say_hello("John")
except this time I # Output is: <p>Hello, John!</p>
can use
say_hello instead of
assigning a new
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>
There are also some built-in functions we can use to accomplish the
same tasks.
hasattr(x, 'year') # Returns true if year attribute exists
getattr(x, 'year') # Returns value of year attribute
setattr(x, 'year', 2015) # Set attribute year to 2015
delattr(x, 'year') # Delete attribute year
VARIABLES WITHIN CLASSES
>>> class Dog:
Generally speaking, ... kind = 'canine' # class var
variables in a class fall ... def __init__(self, name):
under one of two ... self.name = name # instance var
categories: >>> d = Dog('Fido')
• Class variables, which are
shared by all instances.
>>> e = Dog('Buddy')
>>> d.kind # shared by all dogs
• Instance variables, which are
unique to a specific instance. 'canine'
>>> e.kind # shared by all dogs
'canine'
>>> d.name # unique to d
'Fido'
>>> e.name # unique to e
'Buddy'
VARIABLES WITHIN CLASSES
Be careful when using >>> class Dog:
mutable objects as class >>> tricks = [] # mutable class variable
variables.
>>> def __init__(self, name):
>>> self.name = name
>>> def add_trick(self, trick):
>>> self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks # unexpectedly shared by all
['roll over', 'play dead']
VARIABLES WITHIN CLASSES
>>> class Dog:
To fix this issue, make it an >>> def __init__(self, name):
instance variable instead. >>> self.name = name
>>> self.tricks = []
>>> def add_trick(self, trick):
>>> self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']
BUILT-IN ATTRIBUTES
Besides the class and instance attributes, every class has access to
the following:
• __dict__: dictionary containing the object’s namespace.
• __doc__: class documentation string or None if undefined.
• __name__: class name.
• __module__: module name in which the class is defined. This
attribute is "__main__" in interactive mode.
• __bases__: a possibly empty tuple containing the base classes, in
the order of their occurrence in the base class list.
METHODS
We can call a method of a class object using the familiar function
call notation. >>> x = MyClass()
>>> x.f()
'hello world'
class Pet:
def __init__(self, name, age):
self.name = name
self.age = age The __str__ built-in
def get_name(self): function
return self.name defines what happens
def get_age(self): when I
return self.age print an instance of Pet.
def __str__(self): Here I’m
return "This pet’s name is " + str(self.name)
overriding it to print the
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 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 upda
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
Equivalent to:
def gen(exp):
for x in exp:
yield x**2
g1 = gen(iter(range(10)))