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

Decorators and Mixin Classes in Python

By Sagar Chamling

Decorators and Mixin Classes in Python

This presenation is about the compositional pattern in Python using Decorators and Mixin Classes. It will focus on how we could utilize compositional pattern in python using ideas of higher order functions and mixin classes. It also goes beyond decorators and into the world of Mixin classes for the advanced compositional pattern by manipulating "method resolution order" in order to achieve the abstractions. Special Thanks to Robus Gauli.

  • 357