Skip to content

Instantly share code, notes, and snippets.

@iankronquist
Last active November 26, 2023 04:07
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save iankronquist/eeafb55b139b3d8f7e244ce64d0f00f3 to your computer and use it in GitHub Desktop.
Save iankronquist/eeafb55b139b3d8f7e244ce64d0f00f3 to your computer and use it in GitHub Desktop.
The Fundamentals of Programming

Programming Paradigms

In programming, a paradigm is an abstract way to understand and solve a problem. A paradigm is like a perspective, a high point from which you can survey the terrain and try to decide the path your journey will take.

Toay, there are three major programming paradigms:

  1. Imperative Programming.
  2. Object Oriented Programming (OOP).
  3. Functional Programming (FP).

In principle any language can be used to program in any paradigm, but in practice certain languages tend to favor certain paradigms. For instance C is generally used in the imperative style, while Java and C++ favor Object Oriented Programming, and Haskell and Lisp encourage functional programming.

In this tour we will focus on Python and how it can be used in all three paradigms. We will discover that Python is excellent at imperative programming, decent at object oriented programming, and poor at functional programming. We will explore the characteristics of each paradigm and understand their advantages and drawbacks.

OOP Classes and Objects

The Object Oriented Programmer sees the world as groups of data (objects), and actions on that data (methods). Where the imperative programmer was a cook or a chemist, following recipes and formulas to acheive a desired result, the object oriented programmer is a greek philosopher or 19th century naturalist concerned with the proper taxonomy and cescription of the creatures and places of the programming world. Object Oriented Programming is characterised by describing a program as groups of data (objects) and actions on that data (methods).

Object Oriented Programs typtically also have, classes, a differentiation between class and object data, virtual functions, and strict access controls. However, not every object oriented programming language has all of these features. You may be surprised to learn that Javascript is an OOP language that doesn't even have classes!

What is an Object?

An object is a collection of data and functions which act on that data. Different data which belong to an object are called attributes. Functions which act on an object called methods. In Python we can inspect the methods and attributes with the dir function:

>>> s = 'hello world'
>>> dir(s)
['__add__', '__class__', '__contains__', ... 'upper', 'zfill']
>>> s.upper
<built-in method upper of str object at 0x7fc92b3676f0>
>>> s.upper()
'HELLO WORLD'

You can see what type an object is with the type function:

>>> type('asdf')
<class 'str'>
>>> type(42)
<class 'int'>

Creating Your Own Classes

You can create your own types of objects with the class keyword:

class GPA(object):
   pass # do nothing for now...

Defining methods on this class is easy:

class GPA(object):
    def show(self):
        print(self.value) # What is self.value? Undefined for now, we'll get to that in a minute.

A method always takes at least one argument. The first argument, conventionally called self, will be the object which the method is called on. When we do self.value we're accessing the value attribute of the self object. One of the most useful and important methods is called the constructor. A constructor is a special method which is called automatically whenever a new object is created. In Python the constructor is called __init__. It doesn't have to return anything, but it can take however many arguments one likes. It can be used to initialize the method so it can be used later. Let's initialize the value attribute so it can be used in the show method later:

class GPA(object):
    def __init__(self, value):
        self.value = value # Set the attribute value on the self object to be equal to the value argument.
        
    def show(self):
        print(self.value)

chemistry_gpa = GPA(3.0)
chemistry_gpa.show() # prints 3.0

The object in parentheses means that the GPA class inherits from the object class. It is imporatant to note that we are not calling GPA here. We're just telling Python what the parent of the GPA class is. Objects are arranged in a hierarchy: each class inherits from one or more parent classes. Child classes have all of their parents methods and attributes, and act just like their parents. Think like a family tree, where a parents have many children which have their eye color, brains, brawn, or other traits. Now this analogy is kind of flawed, because all humans have the same attributes, and can do the same things with their methods. Maybe it would be better to describe humans as class which can be instantiated into individual people objects?

Analogy: Humans as Objects

class Human(object):
   def __init__(self, mother, father):
       self.mother = mother
       self.father = father
       self.number_of_arms = 2
       self.number_of_eyes = 2
       self.brain = []
       # ...
       
   def eat(self, food):
       ick = self.chew(food)
       super_ick = self.digest(ick)
       self.poop(super_ick)
       
   def drink(self, liquid):
       # ...
       
   def sleep(self):
       # ...
       
   def read(self, book):
       self.brain += book.contents
   
   def write(self, contents):
       return Book(contents)

We could get rather carried away describing all of human physiology as a class. What about Dogs and Cats? We could describe them that way too. They can eat, sleep, but they can't read and write. We could just copy pase that code, remove read and write, change some of the physiology and be done with that. However, copy pasting code sucks and is an easy way to make mistakes. If you have to change a bug in one place, now you have to track down every other place it was pasted.

Since sleep, drink, and eat are pretty much the same for all animals, it would be handy if we could share code between them. Animals are arranged in a kind of scientific hierarchy (think taxonomy and Latin names) already based on their attributes and what they do. I wonder if we could apply that to our code? In fact this is an excellent model of inheritence.

class Animal(object):
   def eat(self):
       # ...
   
   def sleep(self):
       # ...
       
   def drink(self):
       # ...
       
class Human(Animal):
    def read(self, book):
        # ...
        
    def write(self, contents):
        return Book(contents)

Quoi = Human()
Quoi.read()
Quoi.sleep()
Quoi.write('Python!')
isinstance(Quoi, Human)  # True
isinstance(Quoi, Animal) # True

Since all animals eat, and sleep, and drink we can define those methods for all animals as part of an Animal class. Then every animal can inherit from the Animal class or one of its children just like that taxonomic tree. This is what I meant when I said that object oriented programmers are like naturalists. They look at the world and describe it was what attributes a thing has, and what it can do. They look at how objects interact, how objects can create other objects, the objects which fit in each attribute. They look at people and see that they have eyes, which are made up of cells, which can be Eukaryotic or Prokaryotic (two classes!), which are made up of organelles, and molecules (organic and inorganic classes!) and are made up of Atoms and so on. Everything in a problem, everything in the world can be viewed as an object which has attributes and can do things. Code can be shared between objects effortlessly. A change in one place can change the whole system. Methods can work differently for different classes -- who cares whether an animal has one stomach like a human or eight like a cow? As long as it has an animal has a digest method you can pass food to it and get what you want out the other end (errr or don't want. Analogies man, too easy to get lost.).

Analogy: Classes as Form Letters

A class is like a form letter.

Dear ___,
____,
Sincerely,
Dept. of Programming & Charity

A class can inherit from another class, in which case it has all of the attributes of its parents:

Dear ___,
We were hoping you could donate some money.
Sincerely,
Dept. of Programming & Charity

Finally, a completed letter with no more blank spaces can be filled out. This is the final object:

Dear Billy,
We were hoping you could donate some money.
Sincerely,
Dept. of Programming & Charity

Now this analogy is also flawed because it doesn't mention methods, or constructors, at all. But it does show how inheritance works and how attributes are filled in.

Python Class Hierarchy

Okay, classes are arranged in a hierarchy. What is Python's class heirachy? We can use the type and isinstance functions to find out.

>>> type(42)
<class 'int'>
>>> type(int)
<class 'type'>
>>> type(type)
<class 'type'>
>>> type(dir)
<class 'builtin_function_or_method'>
>>> isinstance([], list)
True
>>> isinstance(list, object)
True
>>> isinstance(52, list)
False
>>> type(False)
<class 'bool'>
>>> type(type)
<class 'type'>

Wait. type isn't a function. It's a class. So type is its constructor, which returns the type/class of whatever is given to it.

More Python Methods

In Python, and pretty much only in Python, Attributes or methods which begin and end with two underscores, like __init__ and __del__, have special meaning to the Python interpreter.

For instance, when you write 'a' + 'b', the Python interpreter will look up the __add__ method on the str objects respectively and transform them into something like this: 'a'.__add__('b'). If you want to be able to "add" your objects together you just need to implement the __add__ method for your class.

class GPA(object):
    def __init__(self, value):
        self.value = value
        
    def __add__(self, other):
        return GPA((self.value + other.value) / 2)

python_gpa = GPA(4.0)
film_making_gpa = GPA(3.9)
total_gpa = python_gpa + film_making_gpa
another_total_gpa = python_gpa.__add__(film_making_gpa) # Exactly the same as above.

There are many of the double under score methods for Python objects which have a wide variety of meanings. You can implement or override any operator you can think of (-, /, in, ||, >>, the brackets in my_list[3], etc.) or even dig into the guts of the object. You can learn more about each of these and what they do here: https://docs.python.org/3/reference/datamodel.html.

Class Variables

We already know about two different kinds of variables: global variables and local variables. Here's a quick review of how they act in Python:

def foo(argument):
    global some_global
    local = argument + some_global
    some_global += 1
    return local
    
some_global = 10
print(foo(3))      # 13
print(some_global) # 11
print(foo(4))      # 15
print(some_global) # 12

Classes introduce two more kinds of variables: attributes, which we've already discussed, and class variables. A class variable, also known as a static class variables in other languages like C++ and Java, is shared by all instances of a ccallss (AKA all objects of that class). It's kind of like a global variable which belongs to a class. In Python you can create them like this:

class Example(object):
    my_class_variable = 'something'
    
    def __init__(self, word):
        self.my_attribute = self.my_class_variable + ' ' + word

e = Example('else')
f = Example('other')
print(e.my_attribute) # something else
print(f.my_attribute) # something other

print(e.my_class_variable) # something
print(f.my_class_variable) # something

# You can also access class variables like this:
print(Example.my_class_variable) # something

In a rather annoying move, Python allows you to shadow class variables or create attributes with the same name as the class variable. This is generally considered a bad idea because it's confusing.

class Example2(object):
    class_variable_confusion = 'something'
    
    def __init__(self, word):
        self.class_variable_confusion += ' ' + word

e = Example2('else')
f = Example2('other')
print(e.class_variable_confusion)        # something else
print(f.class_variable_confusion)        # something other
print(Example2.class_variable_confusion) # something

Protection Levels

Most object oriented programming languages have some way of protecting certain attributes and methods so they can't be accessed or called outside of the class and its children. This keeps other programmers from shooting themselves in the foot messing with things they shouldn't and promotes separation of concerns. Python doesn't offer any such protections, but they're still important to know about when learning other languages, and may help you understand certain Python conventions. The assumption is that programmers know what they're doing. Ha.

OOP languages typically have three levels of protection:

  1. Public. Attributes and methods (AKA members when discussed as a group) are accessible to every object, every function, everywhere in the program.
  2. Private. Memberss are only accessible to the class and its children.
  3. Friend. Members are only accessible to the class and other classes which are explicitly marked as friends to the class. If this is somewhat confusing you can remember it with this naughty phrase: "C++ lets friends touch others' private members".

By convention if you don't want other programmers to use a method or an attribute you can prefix it with an underscore:

class Example(object):
   _private_class_variable = 'foo'
   public_class_variable = 'bar'
   def __init__(self):
       self._private_attribute = 'baz'
       self.public_attribute = 'qux'
       
   def _private_method(self):
       # ...
   
   def public_method(self):
       # ...

e = Example()
print(dir(e)) # ['_private_class_variable', 'public_class_variable', '_private_attribute', 'public_attribute', '_private_method', 'public_method']

Note that these protection levels are not a security feature -- a compromised program can potentially access private values. They are only there to help make the code more maintainable.

A Bit of OOP History

FIXME: Move to section 0 The first object oriented language was called Smalltalk and was invented at Xerox Palo Alto Research Center in 1972. The researchers Xerox PARC not only invented one of the most important programming paradigms, they also created the first graphical user interface, the desktop paradigm and similar analogies for modern computing, and made advances in semiconductors. Xerox pretty much squandered everything they did at PARC, but Steve Jobs and Apple ripped off their inventions after a public demo and created the original Macintosh personal computer.

Overriding Parent Methods

What do you think will happen when a child class has a method with the same name as its parent class?

class Parent(object):
    def foo(self):
        print('parent')

class Daughter(Parent):
    def foo(self):
        print('daughter')

d = Daughter()
d.foo() # ???

What happens is that the child class overrides the parent class's method and only the child method is called. However, sometimes you only want to extend the parent class's method, and do a little work at the end. Fortunately there is a function for that called super. super takes an object and its class and returns the object with its type as a parent class (AKA super class).

class Son(Parent):
    def foo(self):
        self_as_parent_type = super(self, Parent)
        self_as_parent_type.foo() # calls the parent's foo method
        print('son')
        
d = Daughter()
d.foo() # daughter

s = Son()
s.foo() # parent
        # son

One common pattern you'll see in constructors is super(self, SOME_PARENT_TYPE).__init__() which calls the parent's constructor.

Multiple Inheritance

In reality people have multiple parents, but in Python so far all of the classes have only had one parent. Python allows you to inherit from multiple classes:

class A(object):
    def do_some_thing(self):
        print('some thing')
        
    def do_a_thing(self):
        print('a thing')

class B(object):
    def do_some_thing(self):
        print('some thing else')
    
    def do_b_thing(self):
        print('b thing')
         
 
class C(A, B):
     def do_c_thing(self):
        print('c thing')

c = C()
c.do_a_thing() # a thing
c.do_b_thing() # b thing
c.do_c_thing() # c thing

c.do_something() # some thing else

If two parent classes have a method or attribute with the same name, the last class, or the leftmost class always wins. Maybe it would make sense to think of it the last class as being genetically dominant. Its genes will always win out. Or maybe "King of the Hill" is a better analogy.

Not all programming languages allow multiple inheritance -- Java in particular forbids it because multiple inheritance tends to make things complicated. If you see a method called on an object, it can be hard to figure out where it was defined. Was it defined on the parent? The grandparent? The other great grandparent? This gets to be a royal pain.

One common pattern multiple inheritance allows is called "mix-ins". A mix-in is a simple class which provides just a little bit of functionality, maybe a couple methods which can be combined with multiple different classes of disparate genealogies. To be honest, I don't have any simple examples of this. I've never actually needed this pattern myself, but you will find it in widely used libraries like werkzeug which is used for HTTP requests in the Flask web application framework.

Abstract Classes

We've seen how you can use classes in order to promote code reuse, and admittedly our animal example was pretty contrived. However, when you stop to think about it, there's one detail which doesn't make sense -- it makes sense to have a base Animal class, from which all other animal species descend, but there's no such thing as a generic animal. All of the animals are a specific species, so doing this doesn't make a lot of sense:

class Animal(object):
   # ...
   
generic_animal = Animal()

We want to make Animal an abstract class, which can't actually be instantiated. This is useful for classes where you just want to share some code, but don't want to provide the full functionality.

In order to prevent Animal from being instantiated we need a virtual method. It's kind of like a slot which says "fill this in here in the child classes". Any class with a virtual method is called an abstract class. It can't be instantiated, or made concrete.

Making abstract classes in Python requires some pretty advanced features of the language. They aren't baked in to the basic syntax the same way as C++. In order to define an abstract class in Python you need something called a metaclass, which is a class of a class, and another thing called a decorator which is a way of wrapping a function in a function. Both of these are fascinating topics for another day.

from abc import ABCMeta, abstractmethod

# Make this a Abstract Base Class
class Abstract(metaclass=ABCMeta):
    
    # The funny thing with an @ sign is called a decorator.
    @abstractmethod
    def virtual_method(self):
        # ...

class Concrete(Abstact):
   def virtual_method(self):
       print('concrete')
       
class Asphalt(Abstract):
    # Don't define virtual_method
    

a = Abstract()
# Throws TypeError: Can't instantiate abstract class Abstract with abstract methods virtual_method

c = Concrete()
c.virtual_method() # concrete

b = Asphalt()
# Throws TypeError: Can't instantiate abstract class Abstract with abstract methods virtual_method

Open and Closed Classes

Some programming languages like Ruby allow you to add methods to a class after it's been defined. This is called having "open" classes, like somone can just open up the hood of the car and jam something in there which they think is useful. Sometimes it's also called "monkey patching" a class. In Python you can change the attributes for an object, or even a class, but you can't change the methods.

Personally, I think open classes are a terrible idea. A coworker of mine has a story about removing a third party Ruby library which wasn't used any more, and then all of a sudden everything break. What happened was the library opened up one of the core Ruby classes and added a method to the string class called to_lower. Code all throughout the website used to_lower everywhere. The worst part was that the string class already had a method called lowercase which did the exact same thing! The most frightening part about this is that this story is in no way unique, and this behavior is common in the Ruby community.

Prototypes

Classes aren't the only way to do object oriented programming. Some languages like Smalltalk and JavaScript don't have classes at all, but still have inheritance using a technique called prototypes. Essentially, it's very easy to define your own objects. You can then take an object and create a new object which inherits directly from it. Instead of inheriting from classes, objects inherit from other objects. We won't provide a lot of examples of prototypal inheritance but it's important to know that classes aren't the only way of having inheritance. Inheritance with classes is sometimes called "classical", which may be the only good programming pun I know.

Object Oriented Programming Without Inheritance at All

Not to put too fine a point on it, but deeply nested inheritance hierachies are a pain in the ass. It can be hard to find where something is defined, and sometimes it's not clear whether a child method calls its parent method or not. Many modern languages like Go and Rust eschew inheritance altogether, but that doesn't mean that they aren't object oriented. In order to leverage some of the same types of code reuse they use concepts called interfaces and traits, but those are a story for another day. In my definition of object oriented programming was that there must be groups of data and methods on that data. Going by that definition it's not hard to object oriented programming in C, the king of all imperative languages.

OOP Conclusion

In between the soaring rhetoric, broken analogies, and useless code samples we covered a lot of ground. We learned about attributes and methods, constructors and destructors, classes and inheritance, and abstract classes and virtual methods. If you understand what all of those things are and how they're useful, then I've done my job.

The foundations of OOP are simple: the world of problems can be broken down into groups of data (objects), and actions on that data (methods). Based on this foundation are layers which make OOP one of the most powerful and widely used paradigms of programming. With all of this power comes a lot of flexibility, and also a lot of complication. It's always a trade off -- do I need this layer abstraction, this level of complication? Does it allow my code to be simpler (Keep It Simple Stupid), and promote code reuse? Or is it a lot of boilerplate which does nothing and obscures what's actually happening? Not every programming language does OOP the same way, but it's important to learn the basic principles so you can transfer them to other languages even if they have different features and slightly different terminology. Always consider whether there's a better way to solve the problem.

OOP Glossary

You should hopefully be familiar with all of these terms. If you aren't, let me know and I'll go expand that section.

TODO: fill out actual definitions in this glossary, for now it's just a list.

  • Class
  • Object
  • Method
  • Attribute
  • Member
  • Public
  • Private
  • Friend
  • Class (Static) Variable
  • Instance Variable (another name for Attribute)
  • Field (another name for Attribute. Jesus, can't we standardize the terminology here?)
  • Message Passing
  • self (AKA this in C++ parlance)
  • Base Class
  • Abstract Class
  • Override
  • Inheritance
  • Single Inheritance
  • Multiple Inheritance
  • Virtual method
  • Constructor
  • Destructor
  • Mix-Ins
  • Prototype
  • superclass (another name for parent class)
@feoneliya
Copy link

Yes, you are absolutely right. In programming, the term "paradigm" is used to refer to a general methodology or approach to software development. I know that developers often combine different paradigms in their projects depending on the requirements of the task. When I need help with programming assignment I try not to hesitate, but to find a solution to the problem by talking to experts. I found a reliable, professional resource where I have the opportunity to connect with several professional programmers who have the skills and experience to complete various programming homework assignments. I try to discuss the details of my order, so I always get the best result.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment