Difference between Python classmethod and staticmethod with a better use case.

Personally, in python I believe, classmethod's and staticmethod's are about the use in terms of the case. When it comes to classmethod and instance method, the point is about you are maintaining a state (variables) to be processed.

Couple of days back, I was working on the payment gateway implementation and have come across this best way to understand how to know what is good for the situation.

Let's say we have three class,
  1. BasePaymentGateway
  2. FirstPaymentGateway
  3. SecondaryPaymentGateway
Now, the BasePaymentGateway provides all the signature which the other payment gateways can follow. Considering the fact that each payment gateways will have their own settings and hence we need to fetch the settings from their respective item.

# paymentgateway.py
import settings
import some_util

class BasePaymentGateway(object):
    
    @staticmethod
    def get_payment_settings():
        """Provides the settings for the given item"""
        pass

    @classmethod
    def get_payment_data(cls, order_item, customer_item):
        """Provides the data that needs to be sent for the payment
        to be initiated"""
        pass

    @classmethod
    def verify_payment_data(cls, recevied_data)r
        pass


class FirstPaymentGateway(BasePaymentGateway):
    
    @staticmethod
    def get_payment_settings():
        return settings.FIRST_DATA

    @classmethod
    def get_payment_data(cls, order_item, customer_item):
        p_data = dict()
        
        # Notice that the 'cls' ensures the correct class settings are obtained.
        # If we had directly used the static method without cls, as:
        # FirstPaymentGateway.get_payment_settings() -- This would always provide
        # settings from FirstPaymentGateway.
        _settings = cls.get_payment_settings()
        
        p_data['order'] = order_item.name
        p_data['amount'] = order_item.amount
        p_data['customer_phone'] = customer_item.phone
        p_data['customer_email'] = customer_item.email
        p_data['url'] = _settings['PAY_URL']
        p_data['drop_category'] = _settings['DROP_CATEGORY']
        # Just an example of checksum that is needed for payment
        p_data['checksum'] = some_util.checksum_calculator(p_data)
        return p_data

    @classmethod
    def verify_payment_data(cls, received_data):
        return some_util.is_received_data_valid(received_data)


class SecondaryPaymentGateway(FirstPaymentGateway):
    
    @staticmethod
    def get_payment_settings():
        # We wanna ensure cls.get_payment_settings will get derived data
        return settings.SECOND_DATA

Now, the settings are almost similar except the say, URL, as following:

#settings.py
FIRST_DATA = {
    'PAY_URL': 'https://secure.firstpayment.com',
    'DROP_CATEGORY': 'AMEX,COD'  # The categories we do not want (some settings)
}

import copy
SECOND_DATA = copy.deepcopy(FIRST_DATA)
SECOND_DATA['PAY_URL'] = 'https://secure.secondpayment.com'

So, when we invoke it as follwing, using an object factory:

#object_factory.py
import paymentgateway

class PaymentObjectFactory(object):

    payment_gateways = {
        'first': paymentgateway.FirstPaymentGateway,
        'second': paymentgateway.SecondaryPaymentGateway
    }

    @staticmethod
    def get_payment_handler(pg_name):
        """
        Returns the specific payment gateway for the given name of pg.
        :param pg_name str: The payment gateway name, example "first"
        :return: Object of PaymentGateway if found else None
        """
        return PaymentObjectFactory.payment_gateways.get(pg_name.lower())

The whole idea of having the factory class getting the specific gateways helps us loose couple the scenario and hence we get to expand the scope and would sustain nicely for future expansion.

The usage is as follows:
# main.py
import object_factory.py
import requests

order_item = # Some Order details
customer_item = # Some Customer data

pg_handler = object_factory.PaymentObjectFactory.get_payment_handler('first')
_data = pg_handler.get_payment_data(order_item, customer_item)
response = requests.post(_data['url'], data=_data)
print response.status

pg_handler = object_factory.PaymentObjectFactory.get_payment_handler('second')
_data = pg_handler.get_payment_data(order_item, customer_item)

# Now, since we use @classmethod we are guaranteed to get the derived class url
# getting called and hence would surly get SECOND_DATA['PAY_URL'] = 'https://secure.secondpayment.com'
# else on using @staticmethod we would get always get 'PAY_URL': 'https://secure.firstpayment.com'
response = requests.post(_data['url'], data=_data)
print response.status

Just last words on the value of classmethod and instance method, unless we have any variable (changing values across different object creation), there is no need for instance method. In the case of the above, we know the settings won't change among various objects of same payment gateway requests except for the inner details which had to be generated on the fly, like order name, amount and customer phone, email, etc.

Conclusion:

If we had @staticmethod on get_payment_data we would ended up choosing FirstPaymentGateway always even when we know the selected gateway was SecondaryPaymentGateway. And since we use @classmethod, we know that when we choose a pg_name as second and get a class handler of SecondaryPaymentGateway we invoke cls.get_payment_settings() of SecondaryPaymentGateway.

Comments

Popular posts from this blog

SSH using Chrome Secure Shell app with SSH identity (private key and public key)

Load Testing using Apache Bench - Post JSON API

NGinx + Gunicorn + Flask-SocketIO based deployment.