Lesson 1 - Introduction to object-oriented programming in Python
Welcome to the first lesson of the object-oriented Python programming course. We finished the Python basic constructs course last time with the article about Declaring functions in Python. In this course, you'll learn to program in an object-oriented way and will also develop an object-oriented way of thinking. It's a bit different than anything we've done until now. For starters, we'll no longer treat programs like several lines of code that the interpreter executes one by one.
The invention of object-oriented programming, OOP hereinafter, was no coincidence, but the result of a development which led to its creation. It's a modern software development methodology supported by most programming languages. A common mistake is that people think that OOP should only be used for certain kinds of programs and that for other cases it would be needlessly complicated; however, the opposite has proven to be true. OOP is a philosophy. It's a new way of looking at a program and the communication between its parts. We should always use it whenever we create a simple utility or a complex database system. The OOP is not just a technology or a recommended program structure, it's mainly a new way of thinking. A new perspective from which we could analyze problems and a new era in software development.
As first, we'll look quickly into the history of how people programmed before OOP and what specific problems OOP resolves. It's important for us to fully understand why the OOP had to be created.
The evolution of methodologies
There is a big difference between programming nowadays and programming 40 years ago. The first computers didn't have great performance and their software was quite simple. The evolution of hardware is so fast that the number of transistors in microprocessors doubles every year (Moore's Law). Unfortunately, people aren't able to evolve as quickly as hardware does. Faster computers require more and more sophisticated and complex software (people want more and more from their computers). So much that at one point, it was found out that about 90% of all software doesn't meet deadlines, requires additional costs or isn't finished at all. Developers started looking for new ways to write programs. Several new approaches took turns. More precisely, paradigms (ways of thinking). Listed as follows:
1. Machine code
The program was just a set of instructions where we weren't able to name variables or enter mathematical expressions. The source code was obviously specific for the then current hardware (processor). This paradigm was replaced soon after it was established.
2. The unstructured paradigm
The unstructured approach is similar to the assembly languages, it's a set of
instructions which are executed line by line. The source code wasn't dependent
on the hardware and was human readable. This approach enabled the creation of
more complex programs for a while. There were still many pitfalls: the only way
to repeat something or to branch off a code was the GOTO
statement.
GOTO
allowed to "jump" to different locations within the program.
Locations were previously specified by a line number in the source code, which
is obviously impractical. When we insert a new line of code somewhere, the
numbers no longer match and the code is broken. Later, it was made possible to
define what they then called "labels". This approach served as a way of
simulating loops. This method of writing programs is of course very confusing
and soon failed to be sufficient for developing complex programs.
Consider that the huge adoption of personal computers over the past few decades caused the growth of software demand and, naturally, a demand of more people who make programs (programmers). Certainly, there are people who can write bulletproof programs even in ASM or other low-level languages, but how many are there? How much does this superhuman job cost? It's necessary to write programs in a way that even less experienced programmers could write high-quality programs and not need to go through 5 years of experience to code a simple utility app.
3. The structured programming
Structured programming is the first paradigm that lasted for a longer time and was quite sufficient for the development of new programs. They would mostly program using loops and branching. Conceptually, we are in the structured programming era based on what we have learned from the first course.
The program would be decomposed into functions, as we've already shown in Declaring functions in Python. A problem is decomposed into several subproblems and each subproblem is then solved with some parametrized function. The disadvantage to it is that a function can only do one thing and when we want a different behavior, we have to write a new one. There is no way to reuse old code and modify it. We need to write it again and again - this creates unnecessary, potentially costly, errors. This disadvantage can be partially worked around by using parametrized functions or using global variables. However, such universal functions usually require a lot of parameters to pass and are hard to use and maintain.
With global data, there's another pitfall. Functions can access the data of other functions. This is the beginning of the end, we can't guarantee that global data isn't being overwritten somewhere between functions. It leads to uncontrollable problems. The entire program will consist of unencapsulated code blocks and can hardly be maintained. Any modification increases the complexity of the program, and then the program will necessarily come to a situation where the cost of adding new features will overbalance the value added by these features. Languages using this approach are, for example, the C language and Pascal.
Between the structured programming and the object-oriented programming, there was one more intermediate approach called modular programming. It involved the encapsulation of specific functionality into modules. Regardless, there was no way to modify and reuse already written code.
As I mentioned at the beginning of the article, it's sometimes said that simple programs mustn't be written in an object-oriented way, but structural, which isn't true. If we program in a structural fashion, we will end up making a blob that will be almost unreadable by most people. Then again, it would come to a point where the program wouldn't even be upgradable and we'd either have to throw it away or rewrite it using the OOP.
The non-object-oriented methods of writing code are called "spaghetti code" because of their lack of clarity (everything is tangled together like spaghetti).
The object-oriented approach
OOP is a philosophy and a way of thinking, designing and implementing solutions that focuses on reusability. This approach is inspired by the industrial revolution - the invention of basic components. For example, when we build our house, we don't burn our own bricks and forge nails, we order them.
Making a "component program" is smarter and cheaper. Components don't fail, they're tested and maintained. If there's a problem, it is most likely in the code you have written in one specific location. We're motivated to write clear code since it can be used by others or by ourselves in other projects. Let's face it, humans are lazy by nature and if we thought that our code wouldn't ever be reused, we simply wouldn't write it good enough ).
Of course, we'll use the knowledge we have gained until now. The main difference is that now, our code will be structured differently into multiple communicating objects.
How the OOP works
It tries to simulate reality as we're used to see it. We can say that we abandon the idea how the program is seen by the computer (machine) and write it from the programmer's (human's) point of view. As we had replaced the assembler with human-readable mathematical notations, now we're going even further and replace those, too. OOP is, therefore, a certain level of abstraction above the program. This has significant advantages because it's more natural and readable for us.
The basic component is object which corresponds with some object from the real world, e.g. a human object or a database object).
Objects have attributes and methods.
Attributes
Object attributes are properties or data that it stores,
e.g. the human's name
and age
. For the database object
it could be the password
or whatever the objects requires to work.
Attributes are just like ordinary variables with which we've worked a hundred
times. Sometimes they're called the "object's internal state".
The term "property" is reserved by Python for some specific usage so we'll refer to object variables as attributes.
Methods
Methods are abilities that the object can perform. Human,
for example, could have the methods go_to_work()
,
greet()
, or blink()
. For the database, it could be
add_entry()
or search()
. Methods can have parameters
and can also return values. You've actually used them before without even
knowing! Remember the upper()
method on the string
object? A string is actually an object that represents text.
You can see that we can easily imagine that we're dealing with text. It contains
methods that a text can perform, e.g. replacing an occurrence, converting to
upper case etc.
In older languages, methods didn't belong to objects but were loosely placed
in modules (units). We could call them by typing upper(text)
instead of text.upper()
. The disadvantage to that was, of course,
that the upper()
method didn't belong to anything. There was no way
to list what string could do and the code was messy. Additionally, we couldn't
have two methods with the same name. In OOP we can have both
user.remove()
and article.remove()
. It's very clear
and simple. In a structured program we'd have to write:
remove_user(user)
and remove_article(article)
. We'd
have to create thousands of silly, unnecessary methods. If you're thinking, hey,
isn't that what the PHP language does? You are absolutely
right. PHP is terrible when it comes to things like this, and for that same
reason, its design is considered old. It became fully object-oriented later, but
its foundations will probably never change. Python is a modern language and
provides a full support of the object-oriented paradigm.
In this article, we're going to explain the basics of how to create objects and how to encapsulate their internal logic. Other OOP features, mainly inheritance, will be explained in the following lessons, to not overload your brain today
Class
We have already encountered the term "class". We understood it as a set of commands. A class, however, allows us to do so much more. A class is a pattern that we use to create objects. It defines their properties and abilities.
An object created according to a class is called an
instance. Instances have the same interface as
the class according to which they were created, but they mutually differ in
their internal data (attributes). For example, let's consider a
Human
class and creating the carl
and
jack
instances from it. Both instances will have the same
attributes as the class (e.g. name
and age
) and
methods (go_to_work()
and greet()
), but the values in
them will be different. For example, the first instance will have the value
"Carl"
in the name attribute and 22
in
age
, the second will have the values Jack
and
45
.
The communication between objects is performed by messaging which makes the
syntax clear. The message usually looks like this:
recipient.method_name(parameters)
. For example,
carl.greet(neighbor)
could cause the carl
instance to
greet the neighbor
instance.
OOP is based upon three core concepts:
- encapsulation
- inheritance
- polymorphism
Let's look into the first of them.
Encapsulation
Encapsulation allows us to hide some methods and attributes so they can remain available only from inside of the class. The object can be thought of as a black box that provides an interface through which we can pass instructions/data to be processed by it.
We don't know how the object works internally, but we know how it behaves on the outside and how we should use it. We can't cause errors because we are only allowed to use it in a way its creator meant it to be used.
An example might be the Human
class having a
birth_date
attribute and a few more based on its value:
full_aged
and age
. If someone changed
birth_date
from outside the object, the values in
full_aged
and age
variables could become invalid.
Which means that the internal state of the object would be inconsistent. This
could happen to us in structured programming. In OOP, however, we encapsulate
the object and mark the birth_date
attribute as private so it won't
be visible from the outside. To the outside, we'd provide a
change_birth_date()
method which would store a new birth date into
a birth_date
variable and also perform a necessary age
re-calculation and full-age re-valuation. Using the object would be always safe
and the application stable.
Encapsulation forces programmers to use objects only in the right way. The class interface is divided into publicly accessible and internal members.
In the next lesson, First object-oriented app in Python - Hello object world, we'll create our first object-oriented program.