OOP: concise version

I'm always quick to recommend Fluent Python as one of the few books when asked "What book should I read to become a good(or better) Python programmer", and that's largely due to its easy absorption; the contents in the book, not the book itself. (Although, I am yet to try the latter)

In the case of validating my taste in finer Python books, I suppose Python Cookbook, Learn Python the Hard Way, and Eric's Python Crash Course book amongst others are in my arsenal.

Going forward, I'll be taking one excerpt code from the Fluent Python book in order to explain some Python object mechanisms then we code our very own class. You do not need the book to follow through but I would suggest buying one to go through other code listings. (Not advertising!)

The one borrowed code listing and our very own example are largely OOP based so the entirety of this article will build around that. A good time to learn (more*) about objects, property, classmethod, staticmethod, and a couple more(Inheritance intentionally left out).

OOP:

This is a method of structuring a program by bundling related properties and behaviors into individual objects. The way of setting up an object is really in the definitive example of say, OOP is an approach for modeling concrete, real-world things; for instance, an object could represent a car with properties such as model, color and manufacture year, and behavior like accelerating, de-accelerating and opening doors.

classmethod

A classmethod is a method that is bound to a class rather than its object. classmethod receives the class itself as the first argument in place on an instance(self). They are commonly identified by the decorator @classmethod on the method. You could also declare a class method anywhere outside the class using:

staticmethod

A staticmethod unlike a classmethod is not bounded to its class. It's pretty much a function* housed in a class, and as might as well be defined at the module level. So what really is a class? A class is used to create user-defined data structures. Classes define functions called methods(typical functions but inside a class), which are responsible for identifying behaviors and actions that an object created from that class can adhere to. Classes hold no real data but when instantiated(making an instance of that class), real data are provided when need be. A simple class structure could be seen below:

In [1]:
class SimpleClass:
    pass


#make an instance of the above class
simple_class = SimpleClass()
In [2]:
#classmethod | staticmethod

class Sample:
    @classmethod
    def klassmethod(*args):
        return args
    
    @staticmethod
    def statmethod(*args):
        return args


#attached to class Sample
print(Sample.klassmethod("hello"))

#not attached to class, only shows arguments
print(Sample.statmethod("hello"))
(<class '__main__.Sample'>, 'hello')
('hello',)
In [ ]:
 

I can think of plenty of ways using the classmethod but not so much on the staticmethod. Let's move on to building another class, the one from the book. A two-dimensional Vector class. Do note that I added to the original code to fit our case even more.

We want this vector class to be able to get its magnitude and angle, compare with other vector class instances, build an instance using external data, and unpack(x,y = vector(2.3, 4.3)) - hence making it iterable.

In [3]:
import math
In [4]:
class Vector2d:
    """A two dimensional vector class that does vector stuff"""
    def __init__(self, x=0, y=0):
        self.x = float(x)
        self.y = float(y)
        
    #instA == instB
    def __eq__(self, other):
        return tuple(self) == tuple(self)
    
    #iterable for unpacking purposes
    def __iter__(self):
        return (i for i in (self.x, self.y))
    
    def __repr__(self):
        className = type(self).__name__
        return '{}({!r}, {!r})'.format(className, *self)
    
    def __str__(self):
        return str(tuple(self))
    
    #magnitude
    def __abs__(self):
        return math.hypot(self.x, self.y)
    
    #angle
    def angle(self):
        return math.atan2(self.y, self.x)
    
    #True for positive nonzeros
    def __bool__(self):
        return bool(abs(self))
    
    #make instance using external data
    @classmethod
    def external(cls, data):
        return cls(data[0], data[1]) 

Pretty straightforward! We shall now move to test this class out.

In [5]:
vect_one = Vector2d(3,9)
vect_one #calls __repr__; print(vect_one) calls __str__
Out[5]:
Vector2d(3.0, 9.0)
In [6]:
#calculate magnitude
print("vect_one has a magntiude of {:.3f}".format(abs(vect_one)))

#calculate angle
print("..and an angle of {:.2f} degrees".format(vect_one.angle()))

'''since we included __eq__ in the class we can use that to\
test out the class method external; compare values'''

vect_two =  Vector2d.external([3,8])
vect_one == vect_two

print("Vect_one: ",vect_one, "& Vect_two: ", vect_two)
vect_one has a magntiude of 9.487
..and an angle of 1.25 degrees
Vect_one:  (3.0, 9.0) & Vect_two:  (3.0, 8.0)
In [ ]:
 

No hassle! Let's try to explain some bits that MIGHT be confusing.

abs, just like the rest with double underscores on both ends is called magic methods (or dunder methods). abs is an absolute operator for numbers; it returns a positive value if provided a negative value; abs(-5) returns 5. This magic method won't serve any immediate goal in our vector class as it's quite okay to have negative values instead we retrofitted it to serve as our magnitude method.

The external method, which as we saw is a class method and already explained. It simply allows for making instances. An example of this would be:

In [7]:
#external csv file imported

import pandas as pd

vec_file = pd.read_csv("XYs.csv")
In [8]:
Vector2d.external(vec_file.iloc[2, :])
Out[8]:
Vector2d(1.0, 6.0)

This is just the basic use of class methods, and if you'd like to see something interesting check the datetime package.

In [ ]:
 
In [ ]:
 

Now, let's make an object person that can use the Vector class; our very own code example.

In [9]:
class JohnPerson:
    NAME = 'John'
    
    def __init__(self, vectorX, vectorY):
        self.X = vectorX
        self.Y = vectorY
    
    def use_vector(self):
        vector = Vector2d(self.X, self.Y)  #1*
        return vector
    
    @classmethod
    def name_of_person(cls):
        return cls.NAME
    
    def __str__(self):
        #vector's str returns a tuple only
        return "My name is {0} person and I just instanced Vector{1}".format(JohnPerson.name_of_person(), self.use_vector())
    
    __repr__ = __str__

1* : Read on composition

In [10]:
person1 = JohnPerson(4,5)
person2 = JohnPerson(7,3)
In [11]:
person1.name_of_person(), person2.name_of_person()
Out[11]:
('John', 'John')
In [12]:
person1
Out[12]:
My name is John person and I just instanced Vector(4.0, 5.0)
In [13]:
person1.NAME = "Fred" #change person1's NAME to Fred; won't work as intended
print(person1)
My name is John person and I just instanced Vector(4.0, 5.0)
In [14]:
#shows Fred still as it references the JohnPerson class itself, not the instance
person1.NAME
Out[14]:
'Fred'
In [15]:
person2.NAME
Out[15]:
'John'
Conclusion:

Do not let OOP frustrate you.

References:
In [ ]: