<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Python Object Orientation Workshop
Paul Chambers
P.R.Chambers@soton.ac.uk
<img style="float
Step1:  Intro to Python OOP
(For The Classy Programmer)
<div align="center" style="width
Step2:  Example
Step3:  Example
Step4:  There's a few things here which I haven't introduced, but all will become clear in the remainder of this workshop.
Step5:  Classes
Step7:  Instance attributes and Class attributes
Instance attributes definition
Step8:  Example
Step9:  Class vs Instance attributes
Step10:  Exercise
Step11:  Example
Step12:  Note
Step13:  Classes
Step14:  Example
Step15:  Notes
Step17:  Magic Methods
Step18:  Note
<ASSISTANT_TASK:>
Python Code:
# Run this cell before trying examples
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
Explanation: Python Object Orientation Workshop
Paul Chambers
P.R.Chambers@soton.ac.uk
<img style="float: left;" src="images/ngcm.png">
<img style="float: right;" src="images/epsrc_logo.jpg">
Prerequisites
Python >= 3.4, Jupyter (Notebook)
Numpy, Matplotlib
Download material and exercises from github:
https://github.com/p-chambers/Python_OOP_Workshop
If you are following this course and do not know how to obtain the above requirements, see Setup Instructions.
End of explanation
# Numpy arrays are classes
import numpy as np
a = np.array([0, 1, 6, 8, 12])
print(a.__class__)
print(type(a))
# We want to operate on the array: try numpy cumulative sum function
print(np.cumsum(a))
# np.cumsum('helloworld')   # Should we expect this to work?
Explanation: Intro to Python OOP
(For The Classy Programmer)
<div align="center" style="width:600px; text-align:centered; font-style:italic; font-size:18pt">
"Object-oriented programming (OOP) refers to a type of computer programming in which programmers define not only the data type of a data structure, but also the types of operations (functions) that can be applied to the data structure."
</div>
Source: Webopedia
Why OOP?
Naturally structured data
Functions used in context
Reduce duplicate code
Maintainability in large codes/software
Many other reasons
OOP in Scientific Computing
Java, C++ and Python designed for OOP
Everything in Python is an object
Scientific libraries, visualisation tools etc.
Pseudo Object Orientation in C and Fortran
How can this course help me
Language in OOP is very different
Learn language used in eg. C++, Java
Ability to read code is essential
Write/migrate code for community library
Better world! Work recognition etc...
Language: Four Fundamental Concepts
Inheritance
Reuse code by deriving from existing classes
Encapsulation
Data hiding
Language: Four Fundamental Concepts (2)
Abstraction 
Simplified representation of complexity
Polymorphism
API performs differently based on data type
Note: Encapsulation is sometimes also used in OOP to describe the grouping of data with methods. It is however more common for texts to use it to describe the hiding of data as will be done here.
Useful explanations of these concepts for Python can also be found here
Material we will cover
Classes
Initialization and __init__
self
Encapsulation with underscore '_'
Inheritance
Magic operators
Python 2.X vs 3.X
Classes: Basics
Attributes (data)
Methods    (Functions operating on the attributes)
'First class citizens': Same rights as core types
pass to functions, store as variables etc.
Example: Numpy arrays showing how it's done
End of explanation
# cumsum is a method belonging to a
a.cumsum()
Explanation: Example: Numpy arrays showing how it's done (ctd.)
We only know what a cumulative sum means for a narrow scope of data types
Group them together with an object!
End of explanation
class Greeter(object):
    def hello(self):         # Method (more on 'self' later)
        print("Hello World")
agreeter = Greeter()         # 'Instantiate' the class
print(agreeter)
# agreeter.                  # Tab complete?
Explanation: Example: Simple class
For now, assume all classes defined by:  class ClassName(object)
End of explanation
# Note that we don't pass an argument to hello!
agreeter.hello()
Explanation: There's a few things here which I haven't introduced, but all will become clear in the remainder of this workshop.
End of explanation
class A(object):
    def __init__(self):
        print("Hello")
a_instance = A()
print(type(a_instance))
Explanation: Classes: Initialisation and self
__init__ class method
Called on creation of an instance
Convention: self = instance
Implicit passing of self, explicit receive
Note: Passing of self is done implicitly in other languages e.g. C++ and Java, and proponents of those languages may argue that this is better. "Explicit is better than implicit" is simply the python way.
What is an "instance"?
Class is like a type
Instance is a specific realisation of that type
eg. "Hello World" is an instance of string
Instances attributes are not shared
Classes: Initialisation vs Construction
Initialisation changes the instance when it is made
... __init__ is not technically Construction (see: C++)
__new__ 'constructs' the instance before __init__
__init__ then initialises the content
More info: The Constructor creates the instance, and the Initialiser Initialises its contents. Most languages e.g. C++ refer to these interchangably and perform these steps together, however the new style classes in Python splits the process.
The difference is quite fine, and for most purposes we do not need to redefine the behaviour of __new__. This is discussed in several Stack Overflow threads, e.g.
Python (and Python C API): __new__ versus __init__
Python's use of __new__ and __init__?
Example: Class Initialisation
End of explanation
class Container(object):
    Simple container which stores an array as an instance attribute
    and an instance method
    def __init__(self, N):
        self.data = np.linspace(0, 1, N)
    
    def plot(self):
        fig = plt.figure()
        ax = fig.add_subplot(111)
        ax.plot(self.data, 'bx')
mydata = Container(11)      # 11 ia passed as 'N' to __init__
print(mydata.__dict__)      # __dict__ is where the attr: value
                            #  pairs are stored!
mydata.plot()
Explanation: Instance attributes and Class attributes
Instance attributes definition: self.attribute = value
Class attributes defined outside functions (class scope)
Class attributes are shared by all instances
Be careful with class attributes
Example: Defining Instance attributes/methods
End of explanation
class Container(object):
    data = np.linspace(0, 1, 5)    # class attribute
    def __init__(self):
        pass
a, b = Container(), Container()
print(a.data)
print(b.data)
a.data = 0                     # Creates INSTANCE attribute
Container.data = 100           # Overwrites CLASS attribute
print(a.data)
print(b.data)
Explanation: Example: Class attributes vs instance attributes
End of explanation
class Container(object):
    def __init__(self, N):
        self.data = np.linspace(0, 1, N)
    def print_data(self):
        print(self.data)
a = Container(11)
a.print_data()          # <<< This is better
Container.print_data(a)
Explanation: Class vs Instance attributes: priority
Source: toptotal.com
Note: There's a couple of things going on in this example which are worth elaborating on. By specifying ClassName.attribute, in this case Container.data = 100 we've overwritten the value of data that EVERY instance of the Container class will access. Hence printing b.data gives the expected result.
By setting a.data at the same time, we have set an instance attribute, which is given priority and called first even though we overwrote the class attribute after assigning this.
This could create a hard to track bug. To avoid it:
Stick to instance variables unless you specifically need to share data e.g. constants, total number or list of things that are shared
Don't overwrite things you know are class attributes with instance.attr unless you really know what you're doing (even then, it's probably better and more readable to make it an instance attribute)
For a really in depth explanation of class vs instance attributes, see either of the following links:
python3_class_and_instance_attributes
python-class-attributes-an-overly-thorough-guide
Example: Implicit vs Explicit passing Instance
End of explanation
class Fruit(object):
    def __init__(self):
        self._hasjuice = True
    def juice(self):
        if not self.isfull(): raise ValueError('No juice!')
        self._hasjuice = False
    
    def isfull(self):
        return self._hasjuice
orange = Fruit()
print(orange.isfull())
orange.juice()
print(orange.isfull())
# orange._                  # tab completion behaviour?
# orange._                 # tab completion behaviour now?
orange._hasjuice = True    # bad!
orange.isfull()
Explanation: Exercise: Basics and Initialisation
Python OOP 1 Basics and Initialisation with images
Classes: Encapsulation
Hiding data from users (and developers)
Use underscores '_' or '__'
Useful if data changing should be controlled
Convention only in Python - not enforced!
Single vs Double Underscore
Single underscore
Nobody outside this class or derived classes should access or change
Double underscore
Stronger attempt to enforce the above
Also 'mangles' the attribute name with 
instance._ClassName__Attribute
Example: Data hiding "protected", single underscore
End of explanation
class Fruit(object):
    def __init__(self):
        self.__hasjuice = True
    def juice(self):
        if not self.isfull(): raise ValueError('No juice!')
        self.__hasjuice = False
    
    def isfull(self):
        return self.__hasjuice
apple = Fruit()
# apple._                       # tab completion behaviour?
apple.juice()
apple._Fruit__hasjuice = False   # Definitely bad!
apple.isfull()
Explanation: Example: Data hiding "private", double underscore
End of explanation
# Live coding Bay....
Explanation: Note: This behaviour can be over used in Python. Programmers from C++ or Java backgrounds may want to make all data hidden or private and access the data with 'getter' or 'setter' functions, however it's generally accepted by Python programmers that getters and setters are unnecessary. The Pythonista phrase is "we are all consenting adults here", meaning you should trust the programmer to interact with your classes and they should trust you to document/indicate which parts of the data not to touch unless they know what they're doing (hence the underscore convention). See the top answer on this Stack Overflow thread.
For an entertaining view of encapsulation, see this blog
End of explanation
class Parent(object):
    # Note the base __init__ is overridden in
    # Child class
    def __init__(self):     
        pass
    def double(self):
        return self.data*2
class Child(Parent):
    def __init__(self, data):
        self.data = data
achild = Child(np.array([0, 1, 5, 10]))
achild.double()
Explanation: Classes: Inheritance
Group multiple objects and methods
Child/Derived class inherits from Parent/Base 
Reduce duplicate code
Maintanable: changes to base falls through to all
Beware multiple inheritance rules - we won't cover this
Example: Simple inheritance
End of explanation
class Plottable(object):
    def __init__(self, data):    
        self.data = data
    def plot(self, ax):
        ax.plot(self.data)
    
class SinWave(Plottable):
    def __init__(self):
        super().__init__(
            np.sin(np.linspace(0, np.pi*2, 101)))
class CosWave(Plottable):
    def __init__(self):
        super().__init__(
            np.cos(np.linspace(0, np.pi*2, 101)))
fig = plt.figure()
ax = fig.add_subplot(111)
mysin = SinWave();   mycos = CosWave()
mysin.plot(ax);      mycos.plot(ax)
Explanation: Example: Calling parent methods with super
End of explanation
dir(object) 
Explanation: Notes: 
* We didn't need any arguments to super here as Python 3 allows this
* super requires additional arguments in Python 2, e.g.
  super(Class, self).method(args...)
If you were wondering why we should use super().method instead of BaseClass.method, other than the convenience of renaming classes, it relates to multiple inheritance which is beyond the scope of this course. If you need to write programs with multiple inheritance (and there are strong arguments against this), you may want to look at this blog for advanced use of super.
Classes: Magic Methods
The workhorse of how things 'just work' in Python
object & builtin types come with many of these
Surrounded by double underscores
We've seen one already: __init__!
Example: The magic methods of object
End of explanation
class Wave(object):
    def __init__(self, freq):
        self.freq = freq
        self._data = np.sin(np.linspace(0, np.pi, 101)
                            * np.pi*2 * freq)
    
    def __str__(self):
        RETURNS the string for printing
        return "Wave frequency: {}".format(self.freq)
    
    def __lt__(self, wave2):
        return self.freq < wave2.freq
wav_low = Wave(10)
wav_high = Wave(50)  # A high frequency wave
print(wav_high)
wav_low < wav_high
# Live coding Bay....
Explanation: Magic Methods: Closer look
__lt__:
Called when evaluating
        a < b
__str__
Called by the print function
All magics can be overridden
Example: overriding __str__ and __lt__ magic methods
End of explanation
class OldSyntax:
    pass
class NewSyntax(object):  # This means 'inherit from object'
    pass
    
print(type(OldSyntax))    # Would give <type 'classobj'>
                          # in Python 2
print(type(NewSyntax))
Explanation: Note: Magic methods are very briefly introduced here. For an extensive overview of magic methods for Python classes, view Rafe Kettlers blog
Exercise: Inheritance and Magic Methods
Python OOP2 Inheritance and Magic Methods using shapes
Python 2 vs 3
Dont need to inherit object in Python 3
New classes inherit from object
Inheritance behaviour is different
Old classes removed in Python 3
Example: New class default Python 3
End of explanation
<END_TASK>