Method Decorators

There are various decorators that are useful when working with OOP. The three I want to highlight are

  • @property
  • @classmethod
  • @staticmethod

@property

While many other languages have getter and setter methods, Python does away with this by adopting a “responsibility lies with the user” approach and has all of its class/object nuts and bolts more or less accessible.

This provides a tough challenge when designing the class attributes, especially when you have multiple data fields that depend on a single object. DateTime variables are a good example of this.

class DateTime(object):
    ''' Takes in a yyyy-mm-dd string and gives datetime functionality'''
    def __init__(self, datetimeStr):
        self.datetimeStr = datetimeStr

Which saves the data to memory as a str

dt = DateTime('2018-01-01')
dt
<__main__.DateTime at 0x5904128>

We can naively use this str to get at all kinds of things

print('month', dt.datetimeStr[5:7])
print('day', dt.datetimeStr[8:11])
print('year', dt.datetimeStr[:4])
month 01
day 01
year 2018

arbitrarily stringing them together to get something like

print('slash-format', dt.datetimeStr[5:7] + '/' +
      dt.datetimeStr[8:11] + '/' + dt.datetimeStr[:4])
slash-format 01/01/2018

Which is cumbersome to type every time, so you might re-write __init__() to take care of this.

class DateTime2(object):
    ''' Takes in a yyyy-mm-dd string and gives datetime functionality'''
    def __init__(self, datetimeStr):
        self.datetimeStr = datetimeStr
        
        # derived attributes
        self.month = self.datetimeStr[5:7]
        self.day = self.datetimeStr[8:11]
        self.year = self.datetimeStr[:4]
        
        self.slashFormat = self.month + '/' + self.day + '/' + self.year
dt = DateTime2('2018-01-01')
dt.slashFormat
'01/01/2018'

But this clutters up your __init__ implementation. Furthermore, users won’t always need these attributes, so building them all at the time of initialization might not be your best option. So you might be inclined to put them into class methods

class DateTime3(object):
    ''' Takes in a yyyy-mm-dd string and gives datetime functionality'''
    def __init__(self, datetimeStr):
        self.datetimeStr = datetimeStr
        
    def month(self):
        return self.datetimeStr[5:7]
        
    def day(self):
        return self.datetimeStr[8:11]
        
    def year(self):
        return self.datetimeStr[:4]
        
    def slashFormat(self):
        return self.month() + '/' + self.day() + '/' + self.year()
dt = DateTime3('2018-01-01')
dt.slashFormat()
'01/01/2018'

But this is really more of an attribute than a function call. And @property lets you call it as such.

class DateTime4(object):
    ''' Takes in a yyyy-mm-dd string and gives datetime functionality'''
    def __init__(self, datetimeStr):
        self.datetimeStr = datetimeStr
    
    @property
    def month(self):
        return self.datetimeStr[5:7]
    
    @property
    def day(self):
        return self.datetimeStr[8:11]
    
    @property
    def year(self):
        return self.datetimeStr[:4]
    
    @property
    def slashFormat(self):
        return self.month + '/' + self.day + '/' + self.year
dt = DateTime4('2018-01-01')
dt.slashFormat
'01/01/2018'

@classmethod

Class methods act directly on the underlying class, not their instances. But this is a little tricky to follow along.

For starters, the last instance of the dt variable was a DateTime4 object

dt.__class__
__main__.DateTime4

Which means that given an existing DateTime4 object, we can determine which class it was, and call it like we did when instantiated dt

dt.__class__('2222-22-22')
<__main__.DateTime4 at 0x58e3630>

therefore producing a different object altogether

dt
<__main__.DateTime4 at 0x58e35c0>

So in a hacky manner, we can write some toy function that will use this functionality to build a new DateTime4 object when passed a number instead of the yyyy-mm-dd string we’ve been using.

def build_DateTime4_with_same_number(dtObj, num):
    num = str(num)
    dumbString = num*4 + '-' + num*2 + '-' + num*2
    return dtObj.__class__(dumbString)
test = build_DateTime4_with_same_number(dt, 3)
test.slashFormat
'33/33/3333'

However, when we package our code, we don’t want our user to have to know every function like this that exists. Instead, we can nest it as a class method as follows.

class DateTime5(DateTime4):
    ''' Takes in a yyyy-mm-dd string and gives datetime functionality'''
    def __init__(self, datetimeStr):
        self.datetimeStr = datetimeStr
    
    @classmethod
    def build_with_same_number(cls, num):
        num = str(num)
        dumbString = num*4 + '-' + num*2 + '-' + num*2
        return cls(dumbString)
    
    # omiting @property implementations
    # we get those with inheritence

Particular emphasis on the use of cls. This acts very similar to self when doing regular class-work, but instead returns the actual memory reference to the class definition.

Which is why the last line looks so much like a regular class instantiation– that’s because it is. And it works just as you’d expect.

test = DateTime5.build_with_same_number(9)
test.slashFormat
'99/99/9999'

This is a dumb example, but is used all over the place when you’ve got multiple ways to instantiate an object. Off the cuff, the pandas.DataFrame comes to mind with the ability to build it from .csv, .json, dict, tuple, and many others. All of those implementations utilize @classmethod in one capacity or another.

@staticmethod

Much easier, a static method is used when you want to be able use a class method without having to first instantiate the class. Or to borrow from my good friend jtzupan:

It is not bound to the method or class and therefore cannot modify the class

i.e. it’s a loosely-related function not dependent on the class or instance variables.

Consider the following class that breaks on instantiation

class StaticMethodTest(object):
    def __init__(self):
        f = open('fileThatDoesntExist.lol')
    
    @staticmethod
    def print_():
        print('Hey, it worked!')
try:
    StaticMethodTest()
except FileNotFoundError:
    print('Well, that broke.')
Well, that broke.

However, this works fine if we go right to the method call.

StaticMethodTest.print_()
Hey, it worked!

A good practical use of this would be a format-checker for our existing DateTime class

import re

class DateTime6(DateTime5):
    def __init__(self, datetimeStr):
        assert(self.is_dt_format(datetimeStr))
        self.datetimeStr = datetimeStr
    
    @staticmethod
    def is_dt_format(datetimeStr):
        dtFormat = re.compile('\d{4}\-\d{2}\-\d{2}')
        return bool(dtFormat.match(datetimeStr))

That either does some up-front assertion at the time of instantiation.

try:
    DateTime6('2018-01-0')
except AssertionError as e:
    print('This failed')
This failed

Or can be used without first building an instance of the class

DateTime6.is_dt_format('20180101')
False