5.7. OOP Staticmethod

  • Method which don't use self in its body should not be in a class

  • If method takes self and use it (it requires instances to work) it should be in class

  • If a method don't use self but uses class as a namespace use @staticmethod decorator

  • Using class as namespace

  • No need to create a class instance

  • Will not pass instance (self) as a first method argument

>>> class User:
...     def say_hello():
...         return 'hello'
>>> class User:
...     @staticmethod
...     def say_hello():
...         return 'hello'

5.7.1. Problem: Instance Method

  • Instance methods require self as a first argument

  • Calling instance method on a class will result in TypeError

  • Calling instance method on an instance will work

>>> class User:
...     def login(self):
...         print('ok')

Call method on a class:

>>> User.login()
Traceback (most recent call last):
TypeError: User.login() missing 1 required positional argument: 'self'

Call method on an instance:

>>> User().login()
ok

5.7.2. Problem: Class Function

  • Calling class method on a class will work

  • Calling class method on an instance will result in TypeError

>>> class User:
...     def login():
...         print('ok')

Call method on a class:

>>> User.login()
ok

Call method on an instance:

>>> User().login()
Traceback (most recent call last):
TypeError: User.login() takes 0 positional arguments but 1 was given

5.7.3. Solution: Static Method

  • Calling static method on a class will work

  • Calling static method on an instance will work

  • Use @staticmethod decorator to create static method

>>> class User:
...     @staticmethod
...     def login():
...         print('ok')

Call method on a class:

>>> User.login()
ok

Call method on an instance:

>>> User().login()
ok

5.7.4. Case Study

import json
from dataclasses import dataclass


@dataclass
class User:
    firstname: str
    lastname: str


DATA = '{"firstname": "Mark", "lastname": "Watney"}'


result = User(**json.loads(DATA))
result
# User(firstname='Mark', lastname='Watney')
import json
from dataclasses import dataclass


@dataclass
class User:
    firstname: str
    lastname: str

    def from_json(self, string):
        data = json.loads(string)
        return User(**data)


DATA = '{"firstname": "Mark", "lastname": "Watney"}'


result = User.from_json(string=DATA)
# TypeError: User.from_json() missing 1 required positional argument: 'self'


result = User().from_json(string=DATA)
# TypeError: User.__init__() missing 2 required positional arguments: 'firstname' and 'lastname'


result = User('', '').from_json(string=DATA)
# User(firstname='Mark', lastname='Watney')
import json
from dataclasses import dataclass


@dataclass
class User:
    firstname: str
    lastname: str

    @staticmethod
    def from_json(string):
        data = json.loads(string)
        return User(**data)


DATA = '{"firstname": "Mark", "lastname": "Watney"}'


result = User.from_json(string=DATA)
result
# User(firstname='Mark', lastname='Watney')

5.7.5. Case Study 1

>>> from dataclasses import dataclass
>>> import json

Assume we received DATA from the REST API endpoint:

>>> DATA = '{"firstname": "Mark", "lastname": "Watney"}'

Let's define a User class:

>>> @dataclass
... class User:
...    firstname: str
...    lastname: str
...
...    def from_json(self, string):
...        data = json.loads(string)
...        return User(**data)

Let's use .from_json() to create an instance:

>>> User.from_json(string=DATA)
Traceback (most recent call last):
TypeError: User.from_json() missing 1 required positional argument: 'self'

The string is unfilled. This is due to the fact, that we are running a method on a class, not on an instance. Typically while running on an instance, Python will pass it as self argument and we will fill the other one. Running this on a class, turns off this behavior, and therefore this is why the string parameter is unfilled.

We can create an instance and then run .from_json() method.

>>> User().from_json(string=DATA)
Traceback (most recent call last):
TypeError: User.__init__() missing 2 required positional arguments: 'firstname' and 'lastname'

Nope, we cannot. In order to create an instance we need to pass firstname and lastname. We can pass None objects instead. We can also make a class to always assume a default value for firstname and lastname as None, but this will remove those arguments from required list and allow to create a User object without passing those values. In both cases this will work, but it is not good:

>>> User(None, None).from_json(DATA)
User(firstname='Mark', lastname='Watney')

We can define a static method .from_json() which will not require creating instance in order to use it:

>>> @dataclass
... class User:
...    firstname: str
...    lastname: str
...
...    @staticmethod
...    def from_json(data):
...        data = json.loads(data)
...        return User(**data)

Now, we can use this without creating an instance first:

>>> User.from_json(DATA)
User(firstname='Mark', lastname='Watney')

5.7.6. Namespace

Functions on a high level of a module lack namespace:

>>> def login():
...     print('User login')
>>>
>>> def logout():
...     print('User logout')
>>>
>>>
>>> login()
User login
>>>
>>> logout()
User logout

When login and logout are in User class (namespace) they get instance (self) as a first argument. Instantiating Calculator is not needed, as of functions do not read or write to instance variables:

>>> class User:
...     def login(self):
...         print('User login')
...
...     def logout(self):
...         print('User logout')
>>>
>>>
>>> User().login()
User login
>>>
>>> User().logout()
User logout

Class User is a namespace for functions. @staticmethod remove instance (self) argument to method:

>>> class User:
...     @staticmethod
...     def login():
...         print('User login')
...
...     @staticmethod
...     def logout():
...         print('User logout')
>>>
>>>
>>> User.login()
User login
>>>
>>> User.logout()
User logout

5.7.7. When to Use Staticmethod

  • Some functions in a class do not require instance (self) to work

  • Hence, they can be @staticmethod... but should they?

  • @staticmethod is a hint for a developer that method does not use instance

SetUp:

>>> from random import randint, seed
>>> seed(0)

Let's create a Hero class with a method make_damage():

>>> class Hero:
...     def make_damage(self):
...         return randint(5,20)

Calling this method on an instance will work as expected:

>>> hero = Hero()
>>> hero.make_damage()
17

And calling this method on a class will fail, as expected:

>>> Hero.make_damage()
Traceback (most recent call last):
TypeError: Hero.make_damage() missing 1 required positional argument: 'self'

This is reasonable, as we need an instance to call this method. Hero needs to be alive (instance created), before it can make damage.

But your IDE may suggest to use @staticmethod, because this method does not use instance (self) to work. It does not modify instance variables, it does not read from them. It is a pure function.

>>> class Hero:
...     @staticmethod
...     def make_damage():
...         return randint(5,20)

Now, calling this method on an instance will work as expected:

>>> hero = Hero()
>>> hero.make_damage()
18

But calling this method on a class will work as well, which is unexpected:

>>> Hero.make_damage()
6

The meaning of this, is that Hero does not need to be alive (instance created) to make damage. And this is not true.

In this example, using @staticmethod is not a good idea. Despite the fact that this method does not use instance (self),

5.7.8. Use Case - 1

  • Singleton

>>> class DatabaseConnection:
...     _instance = None
...
...     @staticmethod
...     def get_instance():
...         if not DatabaseConnection._instance:
...             DatabaseConnection._instance = object.__new__(DatabaseConnection)
...         return DatabaseConnection._instance
>>> a = DatabaseConnection.get_instance()
>>> print(a)  
<__main__.DatabaseConnection object at 0x102453ee0>
>>> b = DatabaseConnection.get_instance()
>>> print(b)  
<__main__.DatabaseConnection object at 0x102453ee0>

5.7.9. Use Case - 2

  • Http Client

>>> class http:
...     @staticmethod
...     def get(url):
...         ...
...
...     @staticmethod
...     def post(url, data):
...         ...
>>>
>>> http.get('https://python3.info')
>>> http.post('https://python3.info', data={'firstname': 'Mark', 'lastname': 'Watney'})

5.7.10. Use Case - 3

The user_create() function is a helper function to create a user. This clearly does belong to the User class, even the prefix user_ in the name suggest it. But it does not use instance (self) to work. It does not read or write to instance variables. It is a pure function.

>>> class User:
...     pass
...
...
>>> def user_create():
...     print('Create User')

We can move this function to the User class, but it will require an instance to work, or we can use @staticmethod decorator:

>>> class User:
...     @staticmethod
...     def create():
...         print('Create User')

5.7.11. Assignments