I’ve been writing code for over half of my life. Used Javascript, C, Perl, PHP, Java, Ruby, C#, and even Haskell. But never — not ONCE — have I ever written a single line of Python. With Python making a surge in popularity due to some popular AI and ML libraries, I figured I would take a stab at learning a bit of Python. This post is to document some of my first impressions.
Python calls itself an object-oriented programming language. In most object-oriented languages, this means that the language supports the use of classes and provides syntax for creating instances of those classes. Those instances are the “objects” of object-oriented programming. Take the following for example:
class Foo: def __init__(self, name): self.name = name def say_hello(self): print("Hello. My name is " + name)
This class provides the ability to create instances of the Foo class, each with its own name and a method to print a greeting. You can create as many Foo objects as your heart desires and each instance has its own name. Invoking say_hello on any given instance of a Foo object will print the greeting with the specific instance’s name.
Ok, so… Python does provide the fundamental capability of an object oriented language. Congratulations Python, point to you!
But is Python a good object oriented language? I guess to answer that, I need to provide my criteria for what separates the good from the not-so-good. For now, I will set aside things like syntax preference and focus on three keys aspects of what I believe every good object oriented programming language should provide out-of-the-box: polymorphism, inheritance, and encapsulation.
Polymorphism means that the object’s runtime type is not as important its capabilities. In statically typed languages like Java, this means that a method signature might different objects derive from the same type but provide their own implementation of a method. A classic example is having different shapes define their own area methods.
public static void printArea(Shape shape) { System.out.println("Area of this shape is " + shape.getArea()); } public interface Shape { public double getArea(); } public class Rectangle extends Shape { private double h; private double l; public Rectangle(double height, double length) { this.h = height; this.l = length; } public double getArea() { return this.h * this.l; } } public class Circle extends Shape { private double r; public Circle(double radius) { this.r = radius; } public double getArea() { return Math.PI * Math.pow(this.r, 2); } }
In this example, the static function printArea allows for any type of Shape to be passed as an argument. Individual shapes provide their own implementation of how to calculate their area. So, the Shape type can take “many forms”. This is what is meant by being polymorphic.
So, what does polymorphism look like in Python? Well… different. Python is not statically typed so you cannot enforce things like argument types at compile time. In other words, there is no way to enforce that an object being passed to a get_area(shape) function is actually a shape. In Python, like in Ruby and other “dynamically typed” languages, the only thing that is important is that the argument passed to the get_area function has a get_area() method defined. ( I put “dynamically typed” in quotes because there really is no runtime checking of the type). This is sometimes referred to as duck-typing — As in, if it walks like a duck and quacks like a duck, it must be a friggin duck. In our case, it must be a shape. So, does Python support polymorphism? I guess, but only because it doesn’t ever care about type in the first place.
Python does provide some pretty nifty capabilities for using operators such as ‘+’ and ‘in’. By overriding the __add__, __contains__, or similar methods, you can use operators in your Python code instead of calling methods. For example, we could define a Vector class that lets us add two (or more) vectors together like so:
class Vector: __init__(self, x, y): self.x = x self.y = y __add__(self,other): return Vector(self.x + other.x, self.y + other.y) v1 = Vector(5, 5) v2 = Vector(2, -3) v3 = v1 + v2
This is pretty cool and lets you do a lot with Pythons built-in operators. It can make the code much more readable by allowing you to abstract away a lot of code into methods for these operators. However, this can also be a really tricky source for bugs if you aren’t careful. I once ran into a bug in a ruby gem that was performing date manipulation using this same sort of operator overwriting mechanism that took way too long to track down (see this bug fix). I stepped over the operation dozens of times before realizing that the operator was invoking a method. So long story short, this is a nifty trick in Python (and Ruby) but you have to be very familiar with the code.
Overall, I’d say Python’s polymorphism capabilities are one of its strengths. Just like with Ruby, any object can be any type at any time. This makes it easy to extend classes and there is no compiler to get in your way.