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