Python Class Overview

I thought it would be fun to review classes in Python and produce a blog post as an artifact, maybe to help others, or to serve as a brush-up for future-me.

Why do we even care about classes? They allow you to define your own reusable data types.

Classes also allow you to store common functionality in a central location, then split off more specialized versions of a thing that are wrapped with extra functionality.

Overview

A class is a template for an object, which is created when you instantiate the class. Resulting objects are “instances” of that class.

Classes in Python contain methods and fields (“attributes”). These are referred to generally as class “members.”

Instances of a given class have access to the methods and attributes defined in the class definition.

Basic class definition

A basic class definition might look like:

class MyClass():

	class_variable_that_should_be_inherited_by_all_instances = 10

	def __init__(self, value_passed_during_instantiation):
		self.attribute_to_set_on_instantiation = value_passed_during_instantiation

	def get_value_of_attribute_for_this_instance(self):
		return self.attribute_to_set_on_instantiation

I’m using obnoxiously long variable names for clarity. I hope you don’t mind.

Notice that the naming convention is capitalized CamelCase, rather than snake_case like everything else in Python.

This class has two methods, a conventional “dunder” (“double-underscore”, internal) __init__ method, and a getter method that we define. __init__ is automatically called when the class is instantiated, and any kind of instance customization or setup you want to do should be written here. In this case, we’ve designed the class to accept a value and set it on the instance at the time of instantiation.

Notice that we pass self into these methods, so they know what to bind to when the class is instantiated; they should be bound to the instance, not the class definition. Honestly, this is a little wonky and would be better if implicit, but it’s okay, we still love you Python.

Let’s use dir to introspect and see what’s in the class we just defined (we could also call MyClass.__dict__ to get more information like member addresses and types):

>>> dir(MyClass)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'class_variable_that_should_be_inherited_by_all_instances', 'get_value_of_attribute_for_this_instance']

You can see that this class has a bunch of out-of-the-box dunder methods for internal use, but that it also has the class variable and getter method we added.

Notice that it has no attribute_to_set_on_instantiation, because we hain’t instantiated it yet.

Class instantiation

To use the class, we need to instantiate it. To instantiate an object of this class, we just call the class and assign the result to a variable so we have a handle on it and can refer to it usefully:

my_instance = MyClass("init_value")

Then we can use the instance by accessing it’s methods and attributes.

my_instance.get_value_of_attribute_for_this_instance() # "init_value"
my_instance.class_variable_that_should_be_inherited_by_all_instances # 10

That’s the gist of classes in Python.

Inheritance

Inheritance is an object-oriented programming (OOP) principle, referring to a hierarchy of classes, where subclasses/children inherit access to any members contained in their superclasses/parents.

For example, you can have a general superclass like Musician, and have more specific subclasses like Violinist and HarmonicaPlayer …this may be a dumb example, but let’s roll with it.

class Musician():

	def make_sound(self, sound_to_make):
		print(sound_to_make)

The idea is that subclasses typically entail the behavior of their superclass, but also add a layer of additional specialization. Methods and attributes common to all musicians should be contained in Musician, and those are inherited by all subclasses. Then, any violin-specific methods or attributes (i.e. apply_rosin_to_bow) can be defined in Violinist.

When you define Violinist, you pass the class to inherit from as an argument, like:

class Violinist(Musician):
	
	def apply_rosin_to_bow(self, how_much_rosin_to_apply):
		self.rosin += how_much_rosin_to_apply

Now, any violin player we make has access to whatever methods/attributes we defined in Musician:

timmy = Violinist()
timmy.make_sound("*sad emotional sounds*") # "*sad emotional sounds*"

Fancy decorators

There are a few other ways we can set up members in a class using decorators.

Static methods

A static method is one that can be accessed in a class without instantiation. These are typically useful for when you want to define a class and use it as a toolkit; it’s more like a namespace or collection/container.

Let’s pretend all musicians are paid the same, and before we go ahead and hire (instantiate) one for our wedding, we’d like to know how much it’s going to cost.

class Musician():

	@staticmethod	
	def calculate_pay_rate(hours):
		return hours * "$"

	def make_sound(self, sound_to_make):
		print(sound_to_make)

Notice we don’t need to pass self in to a static method; these methods have no referent that needs to be bound. They’re “static” because they don’t change their behavior when the class is instantiated.

hours = 3
Musician.calculate_pay_rate(hours) # $$$

Okay, cool. So hiring a musician for three hours will cost us three dollar signs. Notice that we don’t need to instantiate a Musician to use the static method.

Class methods

A class method is one that is bound to the class definition, not the instance of that class. Recall that we passed self in to each of the methods so they refer to the instance object after instantiation.

Sometimes you may want to instantiate a class, but then have the instance retain a binding to the class definition itself. In that case, you can use the @classmethod decorator and pass in cls instead (this could be any name, but cls is conventional).

One use of this could be to define factory methods/alternate constructors to churn out different types of Musicians without having to define subclasses for them:

class Musician():

	def __init__(self, instrument):
		self.instrument = instrument

	@staticmethod	
	def calculate_pay_rate(hours):
		return hours * "$"

	def make_sound(self, sound_to_make):
		print(sound_to_make)

	@classmethod
	def make_new_violinist(cls):
		return cls("violin")

	@classmethod
	def make_new_harmonica_player(cls):
		return cls("harmonica")
guy_from_blues_traveler = Musician.make_new_harmonica_player()

Abstract classes

An abstract class contains at least one abstract method, which is just an empty placeholder that serves as contract that has to be fulfilled by anything that implements the abstract class.

Defining an abstract class is like saying, “we don’t care how you do it, but if you want to build something that counts as a $THING, your $THING needs to have a way to do $METHOD.”

Python doesn’t have native abstract classing, but from Python 3.4+ you can use the ABC module.

from abc import ABC, abstractmethod

class Animal(ABC):
	
	@abstractmethod
	def eat(self):
		pass

	@abstractmethod
	def sleep(self):
		pass

	@abstractmethod
	def poop(self):
		pass

	@abstractmethod
	def move(self):
		pass

This example defines a contract which says “If you want to create your own Animal, we don’t care how it does these things, but it must have ways to eat, sleep, poop, and move.”

Abstract classes can’t be instantiated, only subclassed. Python will throw an exception unless each of these abstract methods are overridden (defined) on your subclass.

Sliding Window Technique

The sliding window technique is used to eliminate travel waste (nested loop) within an array whenever you need to operate on subarrays of length K.

This technique allows you to convert a time complexity of O(N * K) to a single linear pass of O(N).

With a brute force solution, you’d use two loops, where the outer loop increments the subarray starting point, and the inner loop sequentially adds K elements to build the subarray. All of the inner loop’s work is lost/reset when the outer loop increments to the next starting element.

This results in unnecessary repetition: re-iterating over all of the prior subarray’s elements between the first and last; these are common between adjacent subarrays.

Instead of building a new subarray from each starting element, you can create the next subarray by performing two modifications to the current one:

  1. remove prior subarray’s first element from current subarray.
  2. add the next array element to current subarray.

This operation is a bit like an inchworm, inching along the parent array.

Visual example

I’ve been looking for a way to physicalize algorithms for a while, and came across a video of a CS professor who simply used Legos.

Here’s an animation I made of a trivial and contrived array search (find any subarray of length 4), comparing a brute force approach to a sliding window approach.

You can easily see the improved efficiency:

The basic sliding window

Goal: Given an array, get a list of averages for each subarray of length K.

  1. Take an array arr and a subarray length k.
  2. Init a container for your results, to store the averages of each subarray.
  3. Init an intermediary accumulator value sum, to store the sum of elements in the current subarray.
  4. Init a counter windowStart, representing the index of the start of the sliding window.
  5. For each element at index windowEnd in the array,
  6. } Add the element to the accumulator.
  7. } If we have a complete subarray (windowEnd is greater than k-1),
  8. } } Calculate and append result for this subarray.
  9. } } Remove the element at windowStart from sum.
  10. } } Increment windowStart to next index.
  11. Return results.

8 Wastes of Six Sigma, Applied to Software Engineering

Six Sigma is basically the study of efficiency in manufacturing.

They’ve codified a list of efficiency antipatterns called the 8 wastes.

These antipatterns are transferrable to the domain of software engineering and are useful to know in your own work.

The 8 wastes are:

  • Transportation/travel/motion waste: iterating over an entire collection rather than using a sliding window/two pointers, for example.
  • Defects: introducing bugs. Unexpected and unintended behavior.
  • Overproduction: building something you don’t actually need yet, and may not actually ever need. Also, using a data structure that takes more memory when a smaller one would suffice.
  • Waiting: using blocking code when it’s not necessary and asynchronous code is more appropriate.
  • Non-utilized talent: not reusing existing features/libraries.
  • Inventory: a backup of work/information that is sitting idle rather than being processed. Bottlenecks. Backpressure. Tech debt accumulation.
  • Extra processing: building to more restrictive constraints than actually called for. Not starting with MVP and extending on subsequent iterations.

I’ve combined travel/transportation and motion here; within software engineering these are the same, unless you want to consider the waste of commuting to an office instead of working remotely.