©2018 Bloomberg Finance L.P.
All rights reserved.
Andy Fundinger, Senior Engineer
EuroPython, Edinburgh, Scotland, July 29, 2018
Any decorator can be replaced with code in the decorated functions. However, decorators allow this code to be reused and factored out of the functions.
Generally decorators insert a section of code of arbitrary complexity in a single line. Debuggers generally skip over this code--for better or worse.
class Util(object):
def wibble():
print("Wobble")
wibble = staticmethod(wibble)
The @ sign was added in 2.4 as a syntactic sugar for this pattern
class Util(object):
@staticmethod
def wibble():
print("Wobble")
@null_it
def disabled():
pass
We can write our own decorators as long as we accept a function and return a replacement function.
def null_it(func):
'Replace with no-op'
def null(*args,**kwargs): pass
return null
In Python 2.6 we can decorate classes too,
@null_it
class DeadClass:
pass
DeadClass() is None
True
Decorators with arguments aren't actually doing anything all that special. They simply call the function with the arguments and that function returns the actual decorator.
def mult_arg(mult):
def deco(func):
def wrapper(arg_one, *args, **kwargs):
return func(arg_one*mult, *args, **kwargs)
return wrapper
return deco
@mult_arg(3)
def print_x(x):
print(x)
print_x(1)
print_x('Hello ')
3 Hello Hello Hello
Notice, this is two closures, one for the argument (mult) and one for the function (func).
Normally we write decorators that are closures, but there's no particular benefit to this.
import random
class trace_it:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print(args, kwargs)
return self.func(*args, **kwargs)
@trace_it
def rand(min_val, max_val):
return random.randint(min_val, max_val)
rand(10,30)
(10, 30) {}
27
import pytest
@pytest.mark.parametrize("test_input,expected", [
("3+5", 8),
("2+4", 6),
("6*9", 42),
])
def test_eval(test_input, expected):
assert eval(test_input) == expected
def fn_with_name(func):
def wrapper(*args, **kwargs):
func(func.__name__,*args,**kwargs)
return wrapper
@fn_with_name
def self_aware(name, oth):
print('{name}: {oth}'.format(name=name, oth=oth))
self_aware('here')
self_aware: here
#from sqlalchemy import func
hybrid_property= MagicMock()
class Interval(object):
@hybrid_property
def radius(self):
return abs(self.length) / 2
@radius.expression
def radius(cls):
return func.abs(cls.length) / 2
class instance_method:
def __init__(self, func):
self.func = func
def __get__(self, inst, cls):
if inst is None:
raise TypeError(f'{self.func.__name__} is only valid on instances.')
return self.func.__get__(inst, cls)
class GoodClass:
@instance_method
def simple_method(self):
print('simple_method')
def normal_method(self):
print('normal_method')
GoodClass().normal_method()
GoodClass().simple_method()
normal_method simple_method
GoodClass.normal_method
<function __main__.GoodClass.normal_method>
GoodClass.simple_method
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-40-5ffb98a5765b> in <module>() ----> 1 GoodClass.simple_method <ipython-input-34-53c5e8f52716> in __get__(self, inst, cls) 4 def __get__(self, inst, cls): 5 if inst is None: ----> 6 raise TypeError(f'{self.func.__name__} is only valid on instances.') 7 return self.func.__get__(inst, cls) 8 TypeError: simple_method is only valid on instances.
#from retrying import retry
retry = MagicMock()
@retry
def do_something_unreliable():
if random.randint(0, 10) > 1:
raise IOError("Broken sauce, everything is hosed!!!111one")
else:
return "Awesome sauce!"
print(do_something_unreliable())
<MagicMock name='mock()()' id='139994135494384'>
def infinite_retry(func):
def wrapper(*args, **kwargs):
while True:
try:
return func(*args, **kwargs)
except RuntimeError as e:
print(e)
return wrapper
@infinite_retry
def random_fail(max_value):
ret = random.randint(-100, max_value)
if ret<0:
raise RuntimeError("Invalid negative number {ret}".format(ret=ret))
return ret
random_fail(10)
Invalid negative number -79 Invalid negative number -67
6
#import pytest
pytest = MagicMock()
@pytest.mark.webtest
def test_send_http():
pass # perform some webtest test for your app
def test_something_quick():
pass
def test_another():
pass
class TestClass(object):
def test_method(self):
pass
#app = Flask(__name__)
app = MagicMock()
@app.route('/')
def hello_world():
return 'Hello, World!'
import warnings
to_qa=[]
def qa(func):
to_qa.append(func.__name__)
return func
@qa
def new_code(): pass
@qa
def refactored_code(): pass
def well_trusted_code(): pass
to_qa
['new_code', 'refactored_code']
This truly means that the code you wrote is changed--by the decorator--to some other code that is then executed. It might:
cython = MagicMock()
@cython.locals(a=cython.double, b=cython.double, n=cython.p_double)
def foo(a, b, x, y):
n = a*b
...
def replacer(old, new):
def deco(func):
source = inspect.getsource(func.__code__)
lines = source.split('\n')
new_source = lines[1]+'\n'+('\n'.join(lines[2:]).replace(old, new))
exec(new_source,globals())
return globals()[func.__name__]
return deco
def sample(a, b):
x = a + b
y = x * 2
print('Sample: ' + str(y))
sample(1,4)
Sample: 10
@replacer('b','b*3')
def sample(a, b):
x = a + b
y = x * 2
print('Sample: ' + str(y))
sample(1,4)
Sample: 26
@replacer('a','a*3')
def sample(a, b):
x = a + b
y = x * 2
print('Sample: ' + str(y))
sample(1,4)
Sa*3mple: 14
Our Taxonomy