@static vs. @class methods

\today

Quick review / Introduction

  • Decorator syntax with @ sign above function definition
  • Decorators are functions that take functions and return function
  • Returned function (maybe) has different behaviour

Investigation

Class definition

class A(object):
    def foo(self,x):
        print("executing foo({}, {})".format(self,x))

    @classmethod
    def class_foo(cls,x):
        print("executing class_foo({}, {})".format(cls,x))

    @staticmethod
    def static_foo(x):
        print("executing static_foo({})".format(x))

a = A()

Usual method call

  • Method: function stored as class attribute
  • Object is passed as first argument self
  • Method foo is bound to the object a, class A
a.foo(1)
executing foo(<__main__.A object at 0x10f0a8550>, 1)
a.foo
<bound method A.foo of <__main__.A object at 0x10f0a8550>>

@classmethod

  • More realistic: A.class_foo(1)
  • Method class_foo is bound to the class A
  • Class of object instance implicitly passed as first argument (instead of self)
A.class_foo(1)
executing class_foo(<class '__main__.A'>, 1)
A.class_foo
<bound method type.class_foo of <class '__main__.A'>>
a.class_foo(1)
executing class_foo(<class '__main__.A'>, 1)

@staticmethod

  • Neither self (object) nor cls (class) passed as first argument
  • Like plain functions but called from instance or class
  • Group functions with logical connection to the class.
a.static_foo(1)
executing static_foo(1)
A.static_foo('hi')
executing static_foo(hi)

"Realistic" Example

Simple date class

  • Simple class to hold day, month and year information
  • No time zone information (see datetime or pytz module)
class Date(object):
    def __init__(self, year=0, month=0, day=0):
        self.day = day
        self.month = month
        self.year = year

    def __repr__(self):
        return "It's the {}.{}.{}".format(self.day, self.month, self.year)
Date(2014, 4, 1)
It's the 1.4.2014

Requirement: Parse date strings

  • Assume external source as string of e.g. yyyy-mm-dd
  • String needs to be parsed into a Date object
  • Needs to be done in different places in code
string_date = '01-04-2014'
day, month, year = map(int, string_date.split('-'))
date1 = Date(year, month, day)
date1
>>> >>> It's the 1.4.2014

Add @classmethod to the mix

class Date(object):
    def __init__(self, year=0, month=0, day=0):
        self.day = day
        self.month = month
        self.year = year

    def __repr__(self):
        return "It's the {}.{}.{}".format(self.day, self.month, self.year)

    @classmethod
    def from_string(cls, date_as_string):
        day, month, year = map(int, date_as_string.split('-'))
        date1 = cls(year, month, day)
        return date1

Benefits

  • Implemented in a single place and logically connected
  • Encapsulates the logic vs. stray parsing function "somewhere"
  • cls object (of class 'type') holds the class itself

All children of Date will have from_string

Date.from_string('01-04-2014')
It's the 1.4.2014

Requirement: Validate date strings

  • Validate e.g. the string '01-04-2014'
  • Does not need an instance of Date class
  • Logically bound to Date class too
date_as_string = '01-04-2014'
day, month, year = map(int, date_as_string.split('-'))
day <= 31 and month <= 12 and year <= 3999
>>> True

Add @staticmethod to the mix

class Date(object):
    def __init__(self, year=0, month=0, day=0):
        self.day = day
        self.month = month
        self.year = year

    def __repr__(self):
        return "It's the {}.{}.{}".format(self.day, self.month, self.year)

    @staticmethod
    def is_date_valid(date_as_string):
        day, month, year = map(int, date_as_string.split('-'))
        return day <= 31 and month <= 12 and year <= 3999

Benefits

  • Implemented in a single place and logically connected to class Date
  • Encapsulate the logic vs. stray validation function "somewhere"
  • Does not need cls or self objects, makes that explicit

All children of Date will have is_valid_string

Date.is_date_valid('01-04-2014')
True

Summary I

  • A.foo is just a function, expects 2 arguments
  • a.foo has object a bound as first argument
  • a.class_foo, class A is bound to class_foo via object a
  • a.static_foo just returns function with no arguments bound
A.foo
<function A.foo at 0x10f082a70>
a.foo
<bound method A.foo of <__main__.A object at 0x10f0a8550>>
a.class_foo
<bound method type.class_foo of <class '__main__.A'>>
a.static_foo
<function A.static_foo at 0x10f082b90>

Summary II

  • Ordinary methods should have self as first argument
  • @classmethods have a class object as first argument
  • @staticmethods are "ordinary" functions, logically connected to class

Python 2 vs. 3

Python class differences

  • Python 2: self object must be of same class
  • Python 3: could be any object
  • Python 2: A.foo unbound method
  • Python 3: A.foo simple function, can be bound

References

Stackoverflow