Classes and OOP

Python provides Object-Orient Programming (OOP) and its simplier to learn than other interpretered languages, lukcy Python uses very similar syntax as over compiled languages so it will be easy to pickup if you used languages like Java.

Classes

At the heart of OOP is the class, which is a blue print of a data type, you use the class statement to define a class, again Python does not use curly brackets to enclose the class but indentation. To create an instance of a class you use brackets () (not the new keyword like over languages), you call it like a function.

Class and Instantiation
# Class definition
class MyClass:
    body

# Create an instance
instance_of_class = MyClass()

Note: By convension, classes use camel case but they don't have to

Now that you know how to create a class lets start adding constructors, data fields (variables) and methods, however one difference with other compiled languages is that a class instance can be used as sturcture or record, data fields don't need to be declared ahead of time, the can be created on on the fly . The pass statement in Python is used when a statement is required syntactically but you do not want any command or code to execute. The pass statement is a null operation; nothing happens when it executes.

Class and Instantiation
# Class definition
class MyClass:
    pass                         # pass does nothing, its a place holder

# Create an instance
instance_of_class = MyClass()

# Notice we did not declare a variable var1 in the class above, we can add these on the fly
instance_of_class.var1 = 5
print(instance_of_class.var1)

Python has a concept of a constructor in other languages, you can initialize fields automatically by using a __init__ initialize method in the body of a class, it is run everyting you create a new instance. You can pass variables to the __init__ method as well.

Instance Variables
# Class definition
class MyClass:
    def __init__(self, name="Will"):    # we can also pass values, self is always first argument
        self.var1 = 5
        self.name = name                # we pass name which is assigned


# Create an instance
instance_of_class = MyClass("Paul")     # pass value to variable name

print(str(instance_of_class.var1) + " " + instance_of_class.name)

Note: self refers to the instance that was created, its very similar to this() in Java
Default values
# Class definition
class MyClass:
    def __init__(self, name="Will"):    # use a dfault value if none is passed
        self.name = name                # we pass name which is assigned


# Create an instance
instance_of_class = MyClass()           # name will be set to default value as we have not passed one

print(instance_of_class.name)

Note: default values are useful to make sure variables are set to something

You can create the classes own method, which generally should work on the data of that class (instance), again we pass self as the first argument

Instance Variables
# Class definition
class MyClass:
    def __init__(self, name):  # we can also pass values
        self.var1 = 5
        self.name = name  # we pass name which is assigned

    def getname(self):          # we use self to reference the instance variables
        return self.name        # we use self to get the name variable


# Create an instance
instance_of_class = MyClass("Paul")

print(str(instance_of_class.var1) + " " + instance_of_class.getname())

You can create class variables which can be used by all methods of that class

Class Variables
# Class definition
class MyClass:
    lname = "Valle"                   # class variable, also acts like a static class variable

    def __init__(self, fname):        # we can also pass values
        self.fname = fname            # we pass name which is assigned

    def getname(self):          
        return self.fname + " " + self.lname


# Create an instance
instance_of_class = MyClass("Paul")

print(instance_of_class.getname())

print(MyClass.lname)                   # class variables are like static class variables

You can also create static and class methods by using the @staticmethod and @classmethod decorators, with having to initialize the class

Static and Class Methods
# Class definition
class MyClass:
    lname = "Valle"                             # class variable

    @staticmethod                               # static method
    def getfullname(fname):
        return fname + " " + MyClass.lname      # accessing the class variable

    @classmethod                                # class method
    def getlname(cls):                          
        return MyClass.lname                    # accessing the class variable


# Notice we don't initialize anything
print(MyClass.getfullname("Paul"))                        # use the static method
print(MyClass.getlname())                                 # use the class method

Inheritance

Python does not restrict too much when it comes to inheritance because of it dynamic nature, you can abstract variables and methods, etc in a class then inherit that class. The superclass looks like a normal class but the subclass needs to call the superclasses __init__ method and passing any parameters, it does this but calling super.__init__()

Python does not restrict you in regards to multiple inhertiance but making code too complex helps no one, so keep things simple and most of all readable.

Inheritance example
# Superclass definition
class Person:
    def __init__(self, fname, lname):
        self.fname = fname                              # variable will get inherited by subclasses
        self.lname = lname                              # variable will get inherited by subclasses

    def getfullname(self):                              # method will get inherited by subclasses
        return self.fname + " " + self.lname


# Subclass definition
class Professor(Person):                                # inherit Person class
    def __init__(self, fname, lname):
        super().__init__(fname, lname)                  # call superclasses __init__ method


# Subclass definition
class Student(Person):                                  # inherit Person class
    def __init__(self, fname, lname):
        super().__init__(fname, lname)                  # call superclasses __init__ method


professor1 = Professor("Will", "Hay")                   # create professor instance
student1 = Student("Paul", "Valle")                     # create student instance

print("Professor: " + professor1.getfullname())         # get professors name, using inherited method
print("Student: " + student1.getfullname())             # get students name, using inherited method

Private Variables and Methods

Python also provides encapsulation were you can make variables and methods private, they are used to enchance security and reliability by protecting important and delicate parts of the object. You use the double underscore (__) to declare something private

Private variable and method example
# Class definition
class MyClass:
    __var1 = "class var1"                       # private class variable

    def __init__(self):  # we can also pass values
        self.__var2 = "class var2"              

    def __getname(self):                        # private class method
        return self.__var1 + ", " + self.__var2

    def getname(self):                          # you have to get name via this method only
        return self.__getname()


# Create an instance
instance1 = MyClass()

print(instance1.getname())

# print(instance1.__getname())                  # compile error __getname() is private
# print(MyClass.__var1)                         # compile error __var1 is private

Property Decorator

You can use the @property decorator to allow you to setup setter/getter methods and this use the dot notation to make your code cleaner

Private variable and method example
# Class definition
class Employee:

    def __init__(self, name):  # we can also pass values
        self.__name = name

    @property
    def name(self):                         # property (you can create getter/setter)
        return self.__name

    @name.setter                            # setter
    def name(self, value):
        self.__name = value

    @name.getter                            # getter
    def name(self):
        return self.__name


# Create an instance
emp1 = Employee("Paul Valle")
print(emp1.name)                            # use dot notation to access name

emp1.name = "Will Hay"
print(emp1.name)                            # use dot notation to access name