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