text: 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