Overview of Object-Oriented Programming (OOP)
---------------------------------------------
What is OOP?
- programming paradigm based on the
following:
a. objects - may contain data in the form
of fields (often known as
attributes)
b. code - procedures (also known as
methods)
- makes code reusable
- allows easy debugging, maintenance, and
testing
- what's inside a class?
a. data
b. functions
What are objects?
- a car is compose of parts: engine, wheel,
electronics, etc..
- we use the car by putting the key, stepping
at the pedal
- object is like a car, a package composes of
components that we can use to
without knowing the internal components
- instance of a class
- anything you put `()` on their names (eg:
x(), foo(), ..)
What are classes?
- similar to car's blueprint that explains
how the object was built
- parent category/template of a car: has 4
wheels, engine, brakes, etc..
- certain criteria can be passed on classes
to make unique objects like a car
with v8 engine, a 2-door car, etc..
What is class
inheritance?
- deriving classes from parent class
- e.g: breaking a car into subclasses (car
-> sports car -> V8 sports car)
What is abstract
interaction?
- We can use TV remote control by pressing
certain keys and without
actually knowing what and how the
components inside (microchips) function.
- OOP is similar to that, we use the object
(remote control) w/o knowing
the code inside (microchips)
Popular OOP
Languages:
Java
Python
Ruby
C++
Smalltalk
Visual Basic .NET
Objective-C*
Curl
Delphi
Eiffel
* OOP is a core tenet
of iOS mobile app programming,
and Objective-C is
essentially the C language with
an object-oriented
layer.
Namespaces
----------
- mapping of names to
objects
- a name is anything
you define
a. variable
b. function
c. class
- mostly implemented
as python dictionary for improved performance
- created at
different moments
- have different
lifetimes
- examples:
a. built-in names
- created when interpreter starts up
- never deleted
b. global names in a module
- created when the module is read
- lasts until interpreter quits
c. local names in a function
- created when the function is called
- deleted when function returns /
returns an exception
- allows same names
in different locations
* same file name in multiple directories
* same first name but with different
surnames
Different
namespaces
|
Important thing to
note is that there is no relation between names in different
namespaces. In the example below, the variable `spam` has different values on each 3 namespaces - module, function and subfunction levels.
spam
= 'module spam' # local to module
def
parent():
spam = 'parent spam' # local to parent()
print(spam)
def child():
spam = 'child1 spam' # local to child()
print(spam)
child()
parent()
print(spam)
The output would be:
parent
spam
child1
spam
module
spam
|
Useful namespace
functions
|
This allows you to
access the contents of a namespace by converting it into a dictionary.
>>>
import argparse
>>>
parser = argparse.ArgumentParser(description='testing')
>>>
parser.add_argument('-H', help='test option')
_StoreAction(option_strings=['-H'],
dest='H', nargs=None, const=None, default=None, type=None, choices=None,
help='test option', metavar=None)
>>>
>>>
>>>
args = parser.parse_args()
>>>
type(args)
>>>
args
Namespace(H=None)
>>>
vars(args)
{'H':
None}
>>>
|
Scopes
------
- textual
region in python where the namespace is accessible
- defines
part of the program where you can use that name w/o using a prefix
(function.var_name)
- order of
search:
1. innermost scope (current function)
2. scope of enclosing functions moving
outward (if there is any)
3. middle scope (global names in module
level)
4. outermost scope (built-in names)
- when no
name was found, `NameError` exception is raised
Demo on
scope and namespaces
|
Having this sample
code,
def
scope_test():
def do_local():
spam = "local spam"
def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"
def do_global():
global spam
spam = "global spam"
spam = "test spam"
do_local()
print("After local
assignment:", spam)
do_nonlocal()
print("After nonlocal
assignment:", spam)
do_global()
print("After global
assignment:", spam)
scope_test()
print("In global scope:", spam)
will produce this result.
After
local assignment: test spam
After
nonlocal assignment: nonlocal spam
After
global assignment: nonlocal spam
In
global scope: global spam
|
Using dir() to
explore current
scope
|
dir() can be used
to see the names defined in the current scope
>>>
dir()
['__builtins__',
'__doc__', '__name__', '__package__']
>>>
a = 'apple' # let's define another
name
>>>
a
'apple'
>>>
>>>
# the newly defined name now appears
>>>
dir()
['__builtins__',
'__doc__', '__name__', '__package__', 'a']
>>>
>>>
# let's define a function
>>>
def test():
... pass
...
>>>
>>>
t = test()
>>>
>>>
# the function is now also included in the current namespace
>>>
dir()
['__builtins__',
'__doc__', '__name__', '__package__', 'a', 't', 'test']
>>>
>>>
# same is true for a class
>>>
class testClass:
... pass
...
>>>
>>>
dir()
['__builtins__',
'__doc__', '__name__', '__package__', 'a', 't', 'test', 'testClass']
>>>
|
Python Classes
--------------
- a set of
code bundled together providing functionality
- must be
executed before taking effect (similar to function definitions)
-
statements inside are usually function definitions (others are allowed)
- when a
class is created, a new namespace is created and used as a local scope
- ways of
using class:
a. by assigning attributes -> class.num
= 23
b. by instantiating it -> x = class()
- meant to
be reausable
General
syntax
|
class
ClassName:
.. put something here .. |
Class Objects
-------------
- any
object that is created from a class
- supports
2 operations:
a. attribute references
b. instantiation
- FYI: in
Python, all are objects (functions, class, strings, etc ..)
Attribute
references
|
Consider
the following class:
>>>
class MyClass:
... """A simple example
class"""
... i = 12345
... def f(self):
... return 'hello world'
...
>>>
Both
`i`, `f(self)`, and `__doc__` are valid attribute references.
>>>
type(MyClass.i)
>>>
type(MyClass.f)
>>>
type(MyClass.__doc__)
>>>
Since
class objects supports attribute references, you can define another name
on
its local scope:
>>>
MyClass.fruit = 'apple'
>>>
MyClass.fruit
'apple'
>>>
You may
also change a name's value on its local scope.
>>>
MyClass.__doc__
'A
simple example class'
>>>
MyClass.__doc__ = 'I have overwritten the original __doc__'
>>>
MyClass.__doc__
'I
have overwritten the original __doc__'
>>>
|
Instantiation
|
To
instantiate a class, assign it to a local variable:
x = MyClass() That creates an empty object. To create an initial state everytime you instantiate a class, use `__init__`.
def
__init__(self):
self.data = []
Another example of an initial state with some statements inside:
>>>
class Complex:
... def __init__(self, realpart, imagpart):
... self.r = realpart
... self.i = imagpart
...
>>>
x = Complex(3.0, -4.5)
>>>
x.r, x.i
(3.0,
-4.5)
|
Instance Objects
----------------
- when you
instantiate a class (x = MyClass()), the variable where it is stored
(x) is called the instance of the class
- instance
object and class instance are the same thing
- instance
objects only understood 1 operation which is attribute references
- valid
attribute names:
a. data attribute
- instance variables
- just like a normal variable but this
time you are defining it in a
class instance
b. methods
- aka: instance methods
- functions that belong to an object
- basically just a function inside a
class (but it is more than that
..)
- calling it is similar to a regular
function but this time you call it
via a class instance
- important: a method doesn't always
pertain to class instances, there
are also methods on other
object type like a list
which have methods append(),
insert(), sort(), and so
on ..
Data
attributes
|
Consider
the same `MyClass` on the previous section and its instance `x`. We can
define
a data attribute or a new variable on its local scope. After defining, you do whatever you want with it or even destroy it if you no longer need it.
x.counter
= 1
while
x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del
x.counter
|
Methods
|
Details
on method objects will be discuss on the next section but for now, let's try
to
see what is an instance method.
>>>
x = MyClass()
>>>
type(x.f)
>>>
Note
that in the previous `MyClass` definition, `f()` is a function which makes it
a valid
method of the class. |
Method Objects
--------------
- function
definition inside a class instance
- first
argument passed is always the class instance (self)
- can also
be stored on a variable
- a method
can call other method within same class instance
What is
"self"?
|
"self"
is a convention which is used to refer to the class' own instance.
class SampleClass:
def hello(self):
pass
x =
SampleClass()
Since it
is a convention, you may use whatever you want like "x",
"elephant", etc.
But "self" is a convention used by all and you should use same thing. using another convention
might
decrease the redability of your code.
When a method in a class instance is called, it always passes the instance in to itself as the first argument. So this one:
x.hello()
Is
equivalent to:
SampleClass.hello(x) |
An
instance without "self"
|
Let's define a
function definition without using "self"
>>> class test():
... def hello():
... print("hello world")
...
>>>
test.hello()
hello
world
>>>
test.hello # this is not a method,
just an ordinary function
>>>
If we try to create
an instance of it and call its method, this will look for an extra
argument
(which self) and will raise an exception when it dosn't find it.
>>>
a = test()
>>>
a.hello()
Traceback
(most recent call last):
File "
TypeError:
hello() takes 0 positional arguments but 1 was given
>>>
|
Removing
"self"
|
If we
remove self like this:
class
SampleClass:
def hello(input1):
print(input1)
And call the instance:
x =
SampleClass()
x.hello('hi')
We will receive the following error:
Traceback
(most recent call last):
File "practice.py", line 6, in
x.hello('hi')
TypeError: hello() takes 1 positional
argument but 2 were given
That happened since a method always passes the instance to itself as the first argument. So we need to add "self" to catch that 1st argument. |
Function
define outside
a class |
A
function, which turns into a method when class is instantiated, can also be
defined outside
the class definition.
#
Function defined outside the class
def
f1(self, x, y):
return min(x, x+y)
class
C:
f = f1
def g(self):
return 'hello world'
h = g
|
Methods
calling other
methods |
class
Bag:
def __init__(self):
self.data = []
def add(self, x):
self.data.append(x)
def addtwice(self, x):
self.add(x)
self.add(x)
|
Class and Instance Variables
----------------------------
- class
variables -> shared among all
instances
- instance
variables -> unique for every instance
Simple
demo
|
Looking
at this sample class:
class
Dog:
kind = 'canine' # class variable shared by all
instances
def __init__(self, name):
self.name = name # instance variable unique to each
instance
We can
see that `kind` is same on all instances of the class while `name` is unique.
>>>
d = Dog('Fido')
>>>
e = Dog('Buddy')
>>>
d.kind # shared by all
dogs
'canine'
>>>
e.kind # shared by all
dogs
'canine'
>>>
d.name # unique to d
'Fido'
>>>
e.name # unique to e
'Buddy'
|
Another
example
|
class
Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # creates a new empty list for each dog
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']
|
Class Inheritance
-----------------
-
construction of a "derived" class from "base" class(es)
- derived
class aka child class
- base
class aka parent class
- search
for method/attribute starts from the derived class the on to the base
class(es)
- derived
classes may override methods of their base classes
- useful
built-in functions:
* isintance() -- checks instance type
* issubclass() -- checks class inheritance
* issubclass() -- checks class inheritance
- multiple
inheritance is also supported
* search order:
a. inside derive class
b. inside 1st base class (and all its base classes if any)
b. inside 1st base class (and all its base classes if any)
c. inside next base class (and all its
base classes if any)
General
syntax
|
form
1
class DerivedClassName(BaseClassName):
.
.
.
class
DerivedClassName(modname.BaseClassName):
.
.
.
|
Multiple
inheritance
|
class
DerivedClassName(Base1, Base2, Base3):
.
.
.
|
Class
inheritance in action
|
base
class:
class
Fish:
def __init__(self, first_name,
last_name="Fish",
skeleton="bone",
eyelids=False):
self.first_name = first_name
self.last_name = last_name
self.skeleton = skeleton
self.eyelids = eyelids
def swim(self):
print("The fish is
swimming.")
def swim_backwards(self):
print("The fish can swim
backwards.")
derived
class:
class
Trout(Fish):
pass
# since `pass` is the only statement, all methods of the base
# class will be inherited
terry
= Trout("Terry")
print(terry.first_name
+ " " + terry.last_name)
print(terry.skeleton)
print(terry.eyelids)
terry.swim()
terry.swim_backwards()
output:
Terry
Fish
bone
False
The
fish is swimming.
The
fish can swim backwards.
|
Derived class with
different
method |
Using same base
class above, we will add a new derived class with its own unique method.
class
Clownfish(Fish):
# this method belongs only to Clownfish
unless Clownfish is used also
# as a base class of other derived
class(es)
def live_with_anemone(self):
print("The clownfish is
coexisting with sea anemone.")
casey
= Clownfish("Casey")
print(casey.first_name
+ " " + casey.last_name)
casey.swim()
casey.live_with_anemone()
Class `Clownfish`
will still inherit all methods of its base class together with its
own unique method which is `live_with_anemone`
output:
Casey
Fish
The
fish is swimming.
The
clownfish is coexisting with sea anemone.
|
derived class
overriding its
base class' method |
derived
class:
class Shark(Fish):
# we want to override the parent method
because Shark's skeleton is cartilage
# and the last name should be Shark
def __init__(self, first_name,
last_name="Shark",
skeleton="cartilage", eyelids=True):
self.first_name = first_name
self.last_name = last_name
self.skeleton = skeleton
self.eyelids = eyelids
def swim_backwards(self):
print("The shark cannot swim
backwards, but can sink backwards.")
sammy
= Shark("Sammy")
print(sammy.first_name
+ " " + sammy.last_name)
sammy.swim()
sammy.swim_backwards()
print(sammy.eyelids)
print(sammy.skeleton)
output:
Sammy
Shark
The
fish is swimming.
The
shark cannot swim backwards, but can sink backwards.
True
cartilage
|
accessing the
overwritten
method using
super()
|
Using same parent
class as above
class
Trout(Fish):
def __init__(self, water =
"freshwater"):
self.water = water
super().__init__(self)
terry
= Trout()
Initialize first
name
terry.first_name
= "Terry"
Use parent
__init__() through super()
print(terry.first_name
+ " " + terry.last_name)
print(terry.eyelids)
Use child
__init__() override
print(terry.water)
Use parent swim()
method
terry.swim()
Output:
Terry
Fish
False
freshwater
The
fish is swimming.
|
Multiple
inheritance
|
1st base
class:
class school():
def knowledge(self):
print('I will shape a child\'s future')
def building(self):
print('I am made of concrete')
2nd base class:
class
hospital():
def heal(self):
print('I save lives!')
def building(self):
print('I am made of metal')
derived class:
class
community(school, hospital):
pass
c =
community()
c.knowledge()
c.heal()
c.building() # this will inherit the method from the
first base
# class called w/c is school
Output:
I
save lives!
I am
made of concrete
|
Private Variables and Methods
-----------------------------
- used to
preserver contents of variables/methods with same name
- not used
to ro prevent accidental access
- syntax
(atleast 2 leading underscores): __VarName or __FunctionName
- once the
private variable is defined, python will convert its name to:
_ClassName__VarName or
_ClassName__FunctionName
Simple demo
|
Sample statement:
class vehicle():
def __init__(self):
self.__door = 4
def show_door_4(self):
print("door:", self.__door)
class
sports_car(vehicle):
def __init__(self):
super(sports_car, self).__init__() # access __init__ of parent class
self.__door = 2
def show_door_2(self):
print("door:", self.__door)
v =
sports_car()
v.show_door_2()
v.show_door_4()
Output:
door:
4
door:
2
If we name
self._door as self.door, the value that will return is
the value on the
child class; thus, overwriting the parent's.
If we use `dir(v)`,
we can see that both classes' `__door` was converted to
`_ClassName__door`.
['__class__',
'__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__',
'__lt__', '__module__', '__ne__', '__new__'
,
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', '_sports_car__door',
'_vehicle__door', 'show_door_2', 'show_door_4']
|
Calling a private
method
|
Let's create our
class containing a private method and instantiate it afterwards.
>>> class warriors():
... def soldier(self):
... print("upfront and can be
seen")
... def __ninja(self):
... print("hidden in the dark")
...
>>>
>>>
h = warriors()
>>>
>>>
h.soldier() # calling soldier is easy
as pie
upfront
and can be seen
>>>
Trying to call it
using its name as defined in the class will raise an exception.
>>>
h.__ninja()
Traceback
(most recent call last):
File "
AttributeError:
'warriors' object has no attribute '__ninja'
>>>
dir(h) # here reveals a new way of
calling that ninja
['__class__',
'__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__', '__weakref__', '_warriors__ninja', 'soldier']
>>>
To call it
correctly, use the convention used by the interpreter after name conversion.
>>>
h._warriors__ninja()
hidden
in the dark
>>>
|
Where to use classes?
---------------------
Empty
class definition to create records
|
class
Employee:
pass
john
= Employee() # Create an empty
employee record
#
Fill the fields of the record
john.name
= 'John Doe'
john.dept
= 'computer lab'
john.salary
= 1000
|
Iterators
|
As we
know, iterators can accept operations in any of the following `for`
statements.
for
element in [1, 2, 3]:
print(element)
for
element in (1, 2, 3):
print(element)
for
key in {'one':1, 'two':2}:
print(key)
for
char in "123":
print(char)
for
line in open("myfile.txt"):
print(line, end='')
Behind the scenes, `for` statements calls `iter()` on the container object which returns an iterator object. The iterator object defines the method `next()` which accesses the elements one at a time. If there are no more elements, `next()` raises a `StopIteration` exception. Here is a demo on how `iter()` and `next()` works.
>>>
s = 'abc'
>>>
it = iter(s)
>>>
it
>>>
next(it)
'a'
>>>
next(it)
'b'
>>>
next(it)
'c'
>>>
next(it)
Traceback
(most recent call last):
File "
next(it)
StopIteration
We can construct a class having an iterator behavior like this.
class
Reverse:
"""Iterator for looping
over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index
Output:
>>>
rev = Reverse('spam')
>>>
iter(rev)
<__main__ .reverse="" 0x00a1db50="" at="" object="">
>>>
for char in rev:
... print(char)
...
m
a
p
s
|
Generators
|
Generators
are a simple and powerful tool for creating iterators. Instead of returning a
single object (like in a function), it returns a sequence of results which
can be produced by iterating over it (e.g using a for loop).
In
contrast to functions, generators use `yield` instead of `return`. The values
that can be iterated is created by
the
`yield` statement.
>>>
def gen():
... yield 123
...
>>>
>>>
def func():
... return 123
...
>>>
A
generator automatically creates `__iter__()` and `next()` methods. It
remembers the last state and local variables
are save
between subsequent calls. Whereas in functions, execution always start at the
beginning.
>>>
def gen():
... yield('a')
... yield('b')
... yield('c')
... yield('d')
...
>>>
g = gen()
>>>
>>>
next(g)
'a'
>>>
next(g)
'b'
>>>
next(g)
'c'
>>>
next(g)
'd'
>>>
next(g)
Traceback
(most recent call last):
File "
StopIteration
>>>
The
order operation are as follows:
1. Generator is called like a function,
return value is an iterator. Code is not yet execute at this stage
2. Iterator can be used via `next()`
method. Codes are executed until the 1st yield statement is reached.
3. The value right after `yield` is return.
Python keeps track of this value.
4. Any succeedng calls via `next()` will
execute codes after the `yield` statement (if there is any) and return the
next `yield` statements
5. All iterators are finished
>>>
def gen():
... print('first')
... yield('a')
... print('second')
... yield('b')
...
>>>
g = gen()
>>>
>>>
>>>
next(g)
first
'a'
>>>
>>>
next(g)
second
'b'
>>>
next(g)
Traceback
(most recent call last):
File "
StopIteration
>>>
Here are
some uses of generators:
Printing
in reverse:
def
reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]
# output
>>>
for char in reverse('golf'):
... print(char)
...
f
l
o
g
Generators can be used like list comprehension which uses parenthesis instead of brackets. They are design to be used in situation
where
the enclosing function uses it right away. Generators are more compact bu
less versatile than full generator definitions and
tend to
be more memory friendly than list comprehensions.
>>>
sum(i*i for i in range(10))
# sum of squares
285
>>>
xvec = [10, 20, 30]
>>>
yvec = [7, 5, 3]
>>>
sum(x*y for x,y in zip(xvec, yvec))
# dot product
260
>>>
from math import pi, sin
>>>
sine_table = {x: sin(x*pi/180) for x in range(0, 91)}
>>>
unique_words = set(word for line in
page for word in line.split())
>>>
valedictorian = max((student.gpa, student.name) for student in graduates)
>>>
data = 'golf'
>>>
list(data[i] for i in range(len(data)-1, -1, -1))
['f',
'l', 'o', 'g']
|
Tips and Tricks
---------------
Using
namedtuples()
to create classes |
# Why Python is
Great: Namedtuples
# Using namedtuple
is way shorter than
# defining a class
manually:
>>>
from collections import namedtuple
>>>
Car = namedtuple('Car', 'color mileage')
# Our new
"Car" class works as expected:
>>>
my_car = Car('red', 3812.4)
>>>
my_car.color
'red'
>>>
my_car.mileage
3812.4
# We get a nice
string repr for free:
>>>
my_car
Car(color='red'
, mileage=3812.4)
# Like tuples,
namedtuples are immutable:
>>>
my_car.color = 'blue'
AttributeError:
"can't set attribute"
*** I still don't
fully understand this because I am encountering the
error below.
>>>
from collections import namedtuple
>>>
help(namedtuple)
>>>
car = namedtuple('model', 'speed')
>>>
bmw = car('bmw', 1212.34)
Traceback
(most recent call last):
File "
TypeError:
__new__() takes 2 positional arguments but 3 were given
>>>
>>>
bmw = car('bmw', '1212.34')
Traceback
(most recent call last):
File "
TypeError:
__new__() takes 2 positional arguments but 3 were given
>>>
|
No comments:
Post a Comment