Decorators and Mixin Classes in 

By: Sagar Chamling

What is Decorators?

A decorator is a design pattern in Python that allows to add new functionality to an existing object without modifying its structure.

def make_pretty(function):
    def inner():
        print("I got decorated")
        function()
    return inner

def ordinary():
    print("I am ordinary")

ordinary = make_pretty(ordinary)
ordinary()
  • Functions Inside other Functions
  • Functions Returning other Functions
  • Passing Functions as Arguments to other Functions
2019-02-06 00:20:32,851 - [ DEBUG ] Payload: ('1',)   (decorators.py:10)
2019-02-06 00:20:32,851 - [ DEBUG ] Result: 1  (decorators.py:13)
2019-02-06 00:26:02,132 - [ ERROR ] ValueError: ("invalid literal for int() with base 10: '1.q'",)
(decorators.py:17)
2019-02-06 00:45:54,457 - [ ERROR ] ValueError: ("invalid literal for int() with base 10: '1.q'",)
(decorators.py:17)
2019-02-06 00:48:10,048 - [ ERROR ] ValueError: ("invalid literal for int() with base 10: '1.q'",)
(decorators.py:17)
2019-02-06 00:50:56,802 - [ ERROR ] ValueError: ("invalid literal for int() with base 10: '1.q'",)
(decorators.py:17)
2019-02-06 00:51:01,052 - [ ERROR ] ValueError: ("invalid literal for int() with base 10: '1.q'",)
(decorators.py:17)
2019-02-06 00:59:28,091 - [ ERROR ] ValueError: ("invalid literal for int() with base 10: '1.q'",)
(decorators.py:17)
2019-02-06 03:34:35,567 - [ ERROR ] ValueError: ("invalid literal for int() with base 10: '1.q'",)
(decorators.py:17)

Use Case

Logging

Debugging

But your code be like

def fetch_user(request):
    data = request.json

    try:
        logger.info('Handling request %s', data['id'])
        result = UserService.fetch(data['id'])
        logger.debug('Return result: %s', result)
    except NotFoundException ex:
        logger.error('User not found: %s', result)
    except Exception ex:
        logger.error('Error while trying to fetch: %s', ex)


def delete_user(request):    
    data = request.json

    try:
        logger.info('Handling request %s', data['id'])
        result = UserService.delete(data['id'])
        logger.debug('Return result: %s', result)
    except Exception ex:
        logger.error('Error while trying to delete: %s', ex)

How @decorator helped?

def log(logger, prefix):
    def decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            try:
                logger.info('%s - Payload: %s %s'.format(prefix, args or '', kwargs or ''))
                res = function(*args, **kwargs)
                logger.debug('%s - Result: %s'.format(prefix=prefix, res=res))

                return res

            except Exception as ex:
                logger.error('%s - %s: %s'.format(prefix, type(ex).__name__, ex.args))

                raise ex  # Call custom exception
        return wrapper
    return decorator
@log(logger, "FETCH_USER")
def fetch_user(request):
    data = request.json

    try:
        logger.info('Handling request %s', data['id'])
        result = UserService.fetch(data['id'])
        logger.debug('Return result: %s', result)
    except NotFoundException ex:
        logger.error('User not found: %s', result)
    except Exception ex:
        logger.error('Error while trying to fetch: %s', ex)
def fetch(id):
    user = User.fetch(id)
    
    if not user:
        raise NotFoundException('User not found.')

    return user

This piece of code

def fetch_user(request):
    data = request.json

    try:
        result = UserService.fetch(data['id'])
    except NotFoundException ex:
        return jsonify(message="User not found."), HTTPStatus.NOT_FOUND
    except Exception:
        return jsonify(message="Invalid Payload."), HTTPStatus.BAD_REQUEST

    return jsonify(result=result)
@log(logger, 'FETCH_USER')
def fetch(id):
    user = User.fetch(id)
    
    if not user:
        raise NotFoundException('User not found.')

    return user
# logs
2019-02-06 03:34:44,282 [ INFO ] FETCH_USER - Payload: ('1234') 
2019-02-06 03:34:44,282 [ DEBUG ] FETCH_USER - Result: {id: 1234, name: "Sagar Chamling"}

@api.route('/users/<string:user_id>', methods=['PUT'])
@authenticate(allow_all_users=True)
@validate_form(USER_PUT_SCHEMA)
def update_user(user_id):
    ...

@Decorator

  • Compositional
  • Modular
  • Loosely Coupled

But wait

@validate(Type, Integer)
@transform(response_payload)
@log(logger, 'FETCH_USER')
def fetch(id):
    user = User.fetch(id)
    
    if not user:
        raise NotFoundException('User not found.')

    return user
  • Validate Payload
  • Transform its result
  • Do Logging

If I want to

Can we avoid this decorator hell?

Yes

Mixin

It's special type of multiple inheritance

from jsonvalidate import (
    NullContract,
    EnumContract,
    FloatContract,
    RangeContract,
    RegExContract,
    LengthContract,
    StringContract,
    IntegerContract,
    BooleanContract,
    KeyMissingContract,
)

class String(KeyMissingContract, NullContract, StringContract, RegExContract, LengthContract, EnumContract):
    pass


class Integer(KeyMissingContract, NullContract, IntegerContract, RangeContract, EnumContract):
    pass


class Float(KeyMissingContract, NullContract, FloatContract, RangeContract, EnumContract):
    pass


class Boolean(KeyMissingContract, NullContract, BooleanContract):
    pass

It's superb right?

Understanding inheritance

class A:
    def __init__(self):
        print('Initialize A')


class B(A):
    def __init__(self):
        print('Initialize B')


class C(B):
    def __init__(self):
        print('Initialize C')


if __name__ == '__main__':
    test = C()

Output?

Initialize C

Understanding inheritance

class A:
    def __init__(self):
        print('Initialize A')

class B(A):
    def __init__(self):
        A.__init__(self)
        print('Initialize B')

class C(B):
    def __init__(self):
        B.__init__(self)
        print('Initialize C')

if __name__ == '__main__':
    test = C()

Output?

Initialize A
Initialize B
Initialize C

Understanding inheritance

class A:
    def __init__(self):
        print('Initialize A')

class B(A):
    def __init__(self):
        A.__init__(self)
        print('Initialize B')

class C(A):
    def __init__(self):
        A.__init__(self)
        print('Initialize C')

class D(B, C):
    def __init__(self):
        B.__init__(self)
        C.__init__(self)
        print('Initialize D')

if __name__ == '__main__':
    test = D()

Output?

Initialize A
Initialize B
Initialize A
Initialize C
Initialize D

"deadly diamond of death"

class A:
    def __init__(self):
        print('Initialize A')

class B(A):
    def __init__(self):
        super().__init__()
        print('Initialize B')

class C(A):
    def __init__(self):
        super().__init__()
        print('Initialize C')

class D(B, C):
    def __init__(self):
        super().__init__()
        print('Initialize D')

if __name__ == '__main__':
    test = D()

Output?

Initialize A
Initialize C
Initialize B
Initialize D

Understanding inheritance super

 C3 linearization a.k.a MRO

  • Python creates a list of classes using C3
  • Children precede their parents
  • Kept in the order specified in the tuple of base classes

Understanding super MRO

class A:
    def __init__(self):
        print('Initialize A')

class B(A):
    def __init__(self):
        super().__init__()
        print('Initialize B')

class C(A):
    def __init__(self):
        super().__init__()
        print('Initialize C')

class D(B, C):
    def __init__(self):
        super().__init__()
        print('Initialize D')

if __name__ == '__main__':
    test = D()

Output

Initialize A
Initialize C
Initialize B
Initialize D
In [1]: D().__mro__


Out[2]: (<class '__main__.D'>, <class '__main__.B'>, 
<class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
from jsonvalidate import (
    NullContract,
    EnumContract,
    RangeContract,
    RegExContract,
    StringContract,
    KeyMissingContract,
)

class String(KeyMissingContract, NullContract, StringContract, LengthContract, 
             EnumContract):
    pass
Example from: https://github.com/RobusGauli/jsonvalidate
class NullContract(Contract):
    def __init__(self, *args, **kwargs):
        self.nullable = kwargs.get('nullable', False)

        super().__init__(*args, **kwargs)

    def check(self, val):
        if not self.nullable and val is None:
            return True, err(NullError())
        return super().check(val)
USER_SCHEMA = Object({
    'name': String(required=true, nullable=false)
}

It's easy to extend

Example from: https://github.com/RobusGauli/jsonvalidate
class RegExContract(Contract):
    def __init__(self, *args, **kwargs):
        self.regex = kwargs.get('regex', False)

        super(RegExContract, self).__init__(*args, **kwargs)

    def check(self, val):
        if self.regex:
            try:
                regex = r"{}".format(self.regex)
                if re.compile(regex) and not re.match(regex, val):
                    return True, err(RegExError())
            except re.error as error:
                raise ValueError('invalid regular expression')
        return super(RegExContract, self).check(val)
USER_SCHEMA = Object({
    'email': String(regex='^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'),
}
class String(KeyMissingContract, NullContract, StringContract, RegExContract, 
            LengthContract, EnumContract):
    """Composition/Mixins for String"""
    pass
Example from: https://github.com/RobusGauli/jsonvalidate
from jsonvalidate import String, Object, Integer, List

schema = Object({
    'email': String(regex='[^@]+@[^@]+\.[^@]+'),
    'name': String(max_length=3),
    'age': Integer(range=[18, 25]),
    'address': Object({
        'permanent': String(),
        'temporary': String(min_length=3, enums=['asss', 's'])
    }),
    'friends': List(Object({
        'name': String(),
        'nick_name': String()
    }))
})

payload = {
    'email': 'sgr.raee@gmail.com',
    'name': 'r',
    'age': 18,
    'address': {'permanent': 'sd', 'temporary': 'asss'},
    'friends': [{'name': 'robus', 'nick_name': 'sd'}]
}

if __name__ == '__main__':
    print(schema.check(payload))
False, {
    "email": null,
    "name": null,
    "age": null,
    "address": {
        "permanent": null,
        "temporary": null
    },
    "friends": {
        "0": {
            "name": null,
            "nick_name": null
        }
    }
}
Example from: https://github.com/RobusGauli/jsonvalidate
from jsonvalidate import String, Object, Integer, List

schema = Object({
    'email': String(regex='[^@]+@[^@]+\.[^@]+'),
    'name': String(max_length=3),
    'age': Integer(range=[18, 25]),
    'address': Object({
        'permanent': String(),
        'temporary': String(min_length=3, enums=['asss', 's'])
    }),
    'friends': List(Object({
        'name': String(),
        'nick_name': String()
    }))
})

payload = {
    'email': 'sgr.raee@gmail.com',
    'name': 'r',
    'age': 11,
    'address': {'permanent': 'sd', 'temporary': 'asss'},
    'friends': [{'name': 'robus', 'nick_name': 'sd'}]
}

if __name__ == '__main__':
    print(schema.check(payload))
True {
    "email": null,
    "name": null,
    "age": {
        "range_error": {
            "actual_val": 11,
            "valid_range": [
                18,
                25
            ],
            "type": "range_error"
        }
    },
    "address": {
        "permanent": null,
        "temporary": null
    },
    "friends": {
        "0": {
            "name": null,
            "nick_name": null
        }
    }
}

Takeaways

  • Compositional Pattern improves Readability
  • Use Decorator but Avoid Decorator Hell
  • Use mixin only if you understand MRO

Thank You