Object-Oriented Programming

Object-oriented programming is a huge topic, way too much to cover in a single video, let alone a hundred videos.

To begin with, every programming language has its own idea of what object-oriented programming is, and no two languages are in perfect agreement. No one can even agree what “object-oriented” even means, and any definition that seems to have a consensus is at disagreement with major programming languages and large numbers of very good programmers.

This video will cover OOP in the following parts:

  • What Python Objects are.

  • What attributes are and how to use them

  • What types are and how to use them

  • How to create your own types with 2 methods.

What Are Python Objects?

I made a video a while back that talks about what Python objects are and how they work, but I could only scratch the surface in that video because this series is an introduction to Python and you just didn’t have a lot of introduction to Python at that point.

At this point, I’m going to be very specific about what a Python object is, and how objects work in Python. You’ve had a lot of exposure to a lot of different objects, so you’ll be able to understand objects at a deeper and more fundamental level.

First, everything in Python is an object. That is, every value of every variable, every item in a list or tuple or set, every key and every value of every dictionary. And we’ve seen lots of different types of objects, types that aren’t considered objects in other languages, such as functions and such.

In the implementation of Python in C, every Python object is a struct. Depending on what type of object you are looking at, it could be any number of structs, however, it must always have the first two fields be the reference count and a reference to the type of the object. What I’m saying is that every object in Python, even objects that are implemented in the C code itself, are simple a reference counter and a reference to a type. That’s it.

Now, what you can do with a Python Object depends entirely on what features the object allows. Most importantly. every object supports attribute access. That is, you can query the object for the value for a particular attribute by name. This is not at all unlike a dictionary key lookup, where the key is an attribute name rather than any type of possible hashable object.

Many objects support attribute assignment, which can either be reassigning a current attribute to a new value, or creating an entirely new, never-before-seen attribute with a new value. Many objects also support attribute deletion which allows you to remove an attribute from the object as if it had never been added.

Everything else, all of the syntactic sugar, really boils down to an attribute access and occasionally, an attribute assignment.

Let me show an example to you.

Suppose you want to look up a value in a dictionary:

d['a']

What this does is it looks at the value of d. Then it looks up the __getitem__ attribute of d. If that is present, then it calls it with the value 'a'.

Let’s look at an interactive session:

>>> d
{'a': 5}
>>> d['a']
5
>>> d.__getitem__
<built-in method __getitem__ of dict object at 0x000001A5114BC480>
>>> d.__getitem__('a')
5

Every operator, every little bit of syntax you learned so far always boils down to one of these special attributes and typically a function call on that attribute. We’ll cover all of these special attributes and how they are used, and we’ll even cover how to create your own objects that behave similar to the native objects later.

All About Attributes

I mentioned the three important operations of an object in Python:

  • Attribute Access

  • Attribute Assignment

  • Attribute Deletion

Attribute Access

There is only one little bit of syntax that you need to be aware of: the . (dot) operator. You can call it “period” or “full stop” or whatever. Typically, we just use “‘s” as in the possessive in English.

In python, the dot operator can follow any expression. What follows the dot is an identifier. The result of this is another value, called the attribute or method of the object.

We’ve already seen plenty of examples of this syntax, so let’s just review a few.

z = 3 - 4j
z.real # 3
z.imag # -4
z.conjugate() # 3+4j

"a string".title()

def foo():
    """This is a doc string."""
    ...

foo.__doc__

Attribute Assignment

In Python, you can assign to attributes. I don’t think any of the types we discussed allow this, but when you see non-native objects, especially objects you’ve created, you’ll do this a lot.

foo.bar = 'baz'

Like list and dictionary subscription, you can nest these as needed, and you can do parallel and serial assignment.

Attribute Deletion

Just like you can delete variables, items in a list, or items in a dictionary with the del statement, you can delete attributes as well, if the object type allows it.

del foo.bar

Attributes vs. Methods

Typically, in object-oriented programming, you need to keep attributes and methods separate. In Python, there is no such distinction.

When I do use the word “method” in Python, I generally mean an attribute of a class or type that is a function that is meant to be called with an instance of the class or type. There’s a lot to parse here, such as “What is a class? What is a type? What is a class or type attribute?” Don’t worry, it will all make sense when we cover that.

For now, just think that methods are really attributes which tend to be functions. In the future, as you learn more about classes and such, you’ll think of them completely differently, but this is ok for now.

Attributes as Namespaces

We’ve already seen modules as a namespace. Each module has its own global namespace, and you can access that namespace through the module’s attributes.

Hopefully it shouldn’t surprise you that objects (including modules) have their own namespace, but it may surprise you that this is the same type of namespace as the global namespace or the local or nonlocal namespaces for functions. The difference is you can’t really write code that runs “in” the namespace, as you can for functions and such. However, you can access the namespace through the attribute syntax I described here in this video.

Types

In Python, every object has a type. In OOP, we typically call objects that have a type an “instance of that type”, so every object is an instance. The instance-object-value distinction doesn’t make a lot of sense in Python, so for now, just remember that every object has a type, and every object is an instance of that type.

You can use the type() function to get the type of an object.

The “functions” that I told you about that create new objects are really types themselves. In fact, you might’ve noticed that Python calls these class instad of function. Let’s list them here:

  • int()

  • float()

  • complex()

  • bool()

  • str()

  • bytes()

  • bytearray()

  • tuple()

  • list()

  • dict()

  • set()

  • frozenset()

Note

There are some other types out there that you can discover, but we typically don’t use them.

For example, None has the NoneType type, and that this is a function that only ever returns None. However NoneType is not defined in the global namespace initially.

What is the type of a type? The “supertype” is itself type. Let’s look at this interactive session:

>>> type(5.0)
<class 'float'>
>>> type(float)
<class 'type'>
>>> type(type)
<class 'type'>

Duck Typing and isinstance()

Although you could compare the types of two objects, or test to see that an object is a particular type, this is not recommended.

You can use the isinstance() function to see if an object is an instance of a specific type. The second parameter can take a tuple of types. Some types are derived types.

(Examples)

We haven’t covered type inheritance yet – we’ll cover that later. It’s a complicated topic that I think people overcomplicated needlessly. However, suffice it to say that isinstance() isn’t just checking for type equality, it’s also checking inheritance.

Although you can check the type of objects in Python, only rarely should you ever do so. Most commonly, we use “Duck Typing.” Duck typing follows the idea that if something looks like a duck, quacks like a duck, walks like a duck, then it is, for all intents and purposes, a duck. It might be a goose or something else, but in terms of our program it’s close enough to a duck that we can use it.

So in Python, we don’t ask objects if they are strings or numbers of dictionaries, we just pretend that they are, and see what happens. If an exception is raised, maybe it’s because it’s not quite the duck we are a looking for.

The rare cases where we need to know the type include:

  • We are translating an object to a byte stream and we need to know what exactly it is to faithfully represent it. We don’t want to encode ints as floats or vice versa, and we don’t want to confuse string for lists.

  • We want different behavior based on the type, although we typically have fallback behavior. IE, we want to print a value differently if it’s a dict or a string or a list or an int. The fallback behavior could be just to call repr() on it.

If your code raises an exception if the type of an object is not what you were looking for, it’s probably better to be more generous. There’s the chance that someone will introduce a new type of object you haven’t seen before that is perfectly compatible with your code, but if you’re just religiously checking the type that will cause problems, especially if that person changes their type to appear to be the type you are looking for, when it clearly isn’t.