Object Oriented Python - A Summary

I love Python, but you can notice how object orientation was an afterthought (with some wanky compiler tricks) when you learn how its object-oriented system works. It is powerful, but some concepts hard to grasp, and not always properly explained out there. Hence, the main reasoning behind writing this post.

It is not an exhaustive guide, but I think I've touched the main points and should be a decent enough quick reference guide.

Basic definitions

  • Class: What we define. Kind of the structure of which we'll create instances of. Contains methods, attributes and properties.
  • Instance: Also commonly to what we refer when we say Object. A specific entity that has everything that a certain Class defines, with it's own values/state. One class can have many instances.
  • Simple Inheritance: When a class has only a single parent class. A class inherits methods, attributes, properties, etcetera from the parent class.
  • Multiple Inheritance: When a class has more than one parent class. Python supports multiple inheritance.
  • Attribute: A variable a class defines, and thus, that every instance will contain, each with a different value. An attribute might be public, protected or private.
  • Class Attribute: A variable that exists on the class itself, and thus is shared (and accessible) by all instances (if any).
  • Property: A function or functions (getter and/or setter), that wrap an attribute and allow to also add logic around operating with it, while keeping the usage syntax same as if using an attribute (e.g. my instance.an_attribute -> my_instance.a_property )
  • Method: A function defined inside a class. Same as attributes, can be marked as protected or private, but really all are public. More on methods later.
  • Constant: Python has no constants, but by convention a variable or class attribute that is all uppercased is considered a constant.
  • Abstract classes and abstract methods: Python got support for abstract classes and methods late in its life, so you need to use the abc base package. This tutorial won't cover them as they are not very widely used, but if you want info, check the official documentation for ABCs.

Class creation basics

class Person:
  A_CONSTANT = 33

  a_class_attribute = 'Something'

  # Class constructor with two parameters (`self` is always required)
  def __init__(self, name, address):
    # private instance attribute
    self.__name = name
    # public instance attribute
    self.address = address

  # getter, to do for example: print(myInstance.name)
  @property
  def name(self):
    return self.__name

  # setter, to do for example: myInstance.name = 'New name'
  @name.setter
  def name(self, value):
    self.__name = value

  # `self` is always required for normal class methods
  def a_method(self, a_parameter):
    # note that despite being a class attribute, we can also access `a_class_attribute` through `self`
    print(`{name} {attr} {param}`.format(name=self.name, attr=self.a_class_attribute, param=a_parameter))

Creating new instances of a class is also pretty simple:

a_person = Person('John Doe', 'somewhere')
another_person = Person('Jane Doe', 'somewhere else')

In python we signal with _ protected methods and attributes, and if we "really" want to make them private, we can use __ (two underscores) and, when compiling the class, the Python interpreter will change the attribute name so outside callers can't (so easily) access them.

Remarks

There is also a property deleter, but it is not commonly used.

Do not rely on scope modifiers for security, just for explaining to users of your class the intended interface (what you expose for external usage). You can always access protected attributes and methods, and with a little extra work also the private ones.

You will often find classes simply exposing attributes instead of using properties. Properties can also be added using special functions, but the decorators are cleaner, easier and preferred.

You can dynamically add new attributes anywhere from a class, and in fact you will see this behaviour at many professional and open-source projects. However, a good convention for future code readers is defining all of them inside the constructor.

Class vs Static vs Instance variables and methods

In Python, there are really two state-related scopes for variables and methods: class/static and instance. A variable either lives at each and every instance of a class, or then it lives as a class variable and is shared among all instances; and same goes with methods.

class Person:
  a_class_attribute = 'something'

  def __init__(self):
    self.an_instance_attribute = 'something else'

  @staticmethod
  def a_static_method(a_param):
    print(Person.a_class_attribute, a_param)

  def an_instance_method(self, a_param):
    print(self.an_instance_attribute, a_param)

But when writing classes, we can actually define methods in a third way, as a "class method". This is mostly syntactic sugar, created in my opinion to help you write clean code, and I'll explain why after explaining what it is.

A class method is defined with the @classmethod decorator, and does have an additional first "magic" parameter like self, in this case cls. That parameter allows us to call to the class itself, without repeating its name; other than that, works exactly the same as a @staticmethod:

class Person:
  a_class_attribute = 'something'

  @classmethod
  def a_static_method(cls, a_param):
    print(cls.a_class_attribute, a_param)

Now, what's the point of having @classmethod if does the same as @staticmethod but with more magic? Well, the intention is to write code Python style, by adhering to the following rules when writing methods for your classes:

  • If you need to use or alter instance variables, you need self so it is an instance method
  • If you need to use or alter class variables, you should use cls and @classmethod
  • If neither of the above, then you should use @staticmethod

Let's rewrite and expand Person applying the rules:

class Person:
  a_class_attribute = 'something'

  def __init__(self):
    self.an_instance_attribute = 'something else'

  @staticmethod
  def a_static_method(a_param):
    print(a_param)

  @classmethod
  def another_static_method(cls, a_param):
    print(cls.a_class_attribute, a_param)

  def an_instance_method(self, a_param):
    print(self.an_instance_attribute, a_param)

Using cls when calling static stuff of the own class means you can easily refactor its name without needing to grep the code. There should be a single occurrence in the class definition file, and most IDEs will color and hint and the like the same as using Person.xxxxx.

As I like to sometimes go into the internals of things, to help hold better the concepts, the following snippet shows how you would call each method, and how Python really calls them, which I think it's interesting and revealing:

a_person = Person()

# how you'll call it:
a_person.an_instance_method('A')
# how python calls it:
Person.an_instance_method(a_person, 'A')

# you:
a_person.a_static_method('B')
Person.a_static_method('B')
# python:
Person.a_static_method('B')

# you:
a_person.another_static_method('C')
Person.another_static_method('C')
# python:
Person.another_static_method(Person, 'C')

As we just saw, objects really behave like static classes to which Python passes the instance to operate with. A clever trick but confusing at first nonetheless.

Remarks

You can always use self. from instance methods to refer to class attributes, just take into account that it will first check instance attributes, then class attributes.

You can also overload operators and certain special methods (comparators, string conversion, etcetera).

Inheritance

class A:
  def __init__(self, something):
    pass

  def a_method(self):
    pass

class B(A):
  def __init__(self, something):
    super().__init__(something)

  def a_method(self):
    super().a_method()

class C:
  def __init__(self, something):
    pass

  def a_method(self):
    pass

  def another_method(self):
    pass


class D(B, C):
  def __init__(self, something):
    # calls B's constructor
    super().__init__(something)

  def a_method(self):
    # calls B's a_method
    super().a_method()

To call a parent class method, you use super(). In the example above, we use it to call A's constructor from B's, passing the something parameter (pretty useless example, I know, there are way better ones out there).

What happens when you have multiple-inheritance and two or more parent classes have the same method overloaded? In short, python's method resolution order works from left to right regarding how you defined the parent classes: in the previous example B's a_method takes precedence over C's a_method if D did something like super().a_method() somewhere.

Method Overloading

Python does not support method overload, meaning that you can only have one a_method() inside a given class. In other languages, like for example C#, you could have one overload with 2 parameters, another with 3, etcetera. Not in Python.

What you can do is override a parent class method inside a child class. We already saw this in the previous section.

Closing thoughts

This was a brief introduction to Python's OOP. If you wish to really get into the details, I strongly recommend visiting the website realpython.com, which contains many very detailed and nicely explained articles about most of the language features.

Object Oriented Python - A Summary published @ written by

Comment Share @ Twitter Share @ Linkedin Share @ Mastodon