Object Oriented programming in Python

The first paradigm of programming was:
"Programs are active, data is static, data sits there in a lump and programs DO things to or with the data".

As programs got more complex, larger, it got harder to handle all the details. There were problems caused by the complexity of team programming, problems of controlling access to the data, controlling the formatting of data, preventing the duplication of code.

The paradigm of Object Oriented programming is:
"Data is not static, data knows things and knows how to DO things. Data and code would be bundled together into one thing, an object". What an object "knows" are its properties or attributes (its values). What it can do are its methods. Objects _sometimes_ refer to things in the real world: a customer, a student, a class, a widget, a circle, a square, a shopping cart, etc. Sometimes they do not, e.g., objects can be a transaction. Class Examples (in UML notation)

Some basic principles of OOP:

More on data encapsulation: Imagine you have a library that you sell to your customers. It provides some methods that they use to manipulate objects of their business. A month later you discover this great new data structure which will make your product work much faster! It needs different algorithms to make it work. Ok, you rewrite your class definition and sell it to your customers (in safe binary form!) as an update or upgrade. They do NOT have to change their code at ALL. As long as you kept the names of the methods they use the same with the same list of parameters, they need to know NOTHING of what is inside the object! That's great from their point of view - why should they have to change THEIR code because of something YOU did? Think if this were NOT the case. Wouldn't most customers be happy with staying with the old version if it meant they could keep their code the same? if they had to rewrite their code to make it work with your new stuff, they are likely to say "pass on the upgrade".

A class is a framework or template that you write describing some kind of concept you want to treat as one thing, e.g., a customer, a student, a UPC, a temperature.

Most people do not write classes to represent something very simple, like one integer or a string. You write a class to 'bundle together' some values which belong to the same thing. (This is not the only reason but it's the first one people think of.) A fraction has two parts, a numerator and a denominator. You could certainly keep those in two different variables but there would be nothing to mark them as "together" except your treatment of them. This "name" variable and "address" variable and GPA variable and major variable all belong to a student, but that is not enforced by the language. Putting them into an object allows you to talk about them all as one thing. Printing them, comparing them, inputting, outputting, whatever else you need to do with them, can be accomplished via the object.

NOTE: OOP has not become standardized enough for anyone to make many statements which would be true for ALL languages. C++ and Python are both OOP languages, but they still do things in different ways. Sometimes they even use different terms, not for most of the basic things, but some specialized areas. Tread carefully when switching languages and writing OOP!

you create a Class by describing the data in it, making a method to set up one (constructor) and making more methods (functions) to work with it. Then in the program you ask for the creation of as many as you need, by calling the constructor with the information needed to make one of the class. You use other methods you have provided to do things with the objects.
Syntax:
To start the definition of a class, you use the keyword class and an identifier followed by a colon. Typically this is at the left margin and near the top of the file. Everything indented under this line is part of the class definition.

class identifier:   

To make a constructor in Python, use the name __init__ (that's two underscores on both sides) and a parameter called self. You may also have other parameters to provide initial values. Self is a special name which means that things (variables, other functions) that are inside the object can be accessed. You do this by using the notation "self." in front of the object's pieces.

def __init__ (self, horz, vert):
  self.x = horz 
  self.y = vert
This could be a constructor for a Point class, so that when you make a Point, you have to provide the horizontal and vertical location of the Point. You say, but the Point constructor is called with TWO arguments, not three! That is true, the "self" argument is implied.

Another strange fact: you call the constructor by the name of the _class_, even though its name inside the class definition is __init__! Constructors do not return any value. A constructor is called ONE time per object, at the time it is created.

Another piece of vocabulary: when you call the constructor in a program, you are asking for the instantiation of the class or an "instance" of the class. This thing you are asking to be created is also called an object. (Yes, it looks like a variable! that's how we have treated them all semester long.)

A complete example should help:

class Fraction:     # note, no parentheses! this is not a function!
   
## the data in this class represents a fraction, two integers
##  for numerator (num) and denimnator (den)

##This is the constructor
   def __init__ (self, numerator, denminator):  
       self.num = numerator
       if denminator != 0:  # data validation, no 0 denominators!
          self.den = denminator      
       else:
          self.den = 1  
# a default value of 1, it may not be what the user wants
# but it's a lot better than having a zero!
## that's it - you do not have to return anything!

## this is how the object's data can be turned into a string
## usually used for outputting it
   def __str__ (self):
       return str(self.num) + "/" + str(self.den)

## Get the greatest common divisor and divide both parts of
## the fraction by it to reduce to lowest terms.
## Returns a new object containing the reduced fraction.
## Assumes we have included the gcd function written in the
##  C++ vs. Python notes.

## def reduce (self):
       g = gcd(self.num, self.den)
       return Fraction (self.num//g, self.den//g)

##  why // and not /?  / will always be float and we want ints
##  // will give an int result if both operands are ints


## these are called accessors, they just access and return one
##  of the values of the object  (also called getters)
##  why bother with these? Python lets you have direct access to 
##  an object's values anyway.  It's style. In C++ it's REQUIRED!
   def get_num(self):
       return self.num
   def get_den(self):
       return self.den
    

def main():
    n1 = int(input("Enter a numerator for fraction 1 "))
    d1 = int(input("Enter a denominator for fraction 1 "))
    n2 = int(input("Enter a numerator for fraction 2 "))
    d2 = int(input("Enter a denominator for fraction 2 "))
    frac1 = Fraction(n1, d1)  # calling the constructor (aka __init__)
    frac2 = Fraction(n2, d2)  # calling the constructor (aka __init__)
    print(frac1, "is fraction 1") # using the __str__ method given above
    print("fraction 2 is", frac2)
    print(frac1.reduce()) # note, no argument! self is implied 
    frac3 = frac2.reduce()
    print(frac3)
    myden = frac3.get_den()
    if myden == 1:
        print("frac3 is a whole number!")

Reference Page for Object Oriented Python