# 2017 Python - decorator and with statement ###### tags: `Conference` `Python` site by Stanley Shiao from PyConTW2017 --- "不要重複造輪子" --- ## Reuse code - Functions - Class - Module - Package ---- ```python= # task 1: divide integer a/b if b == 0: raise 'error' else: print(a/b) # task 2: divide integer c/d if d == 0: raise 'error' else: print(c/d) ``` 身為懶惰的工程師, 覺得寫類似的功能很累 所以會把重複的地方 extract 出來變成 function ---- ```python= def safe_divide(x, y): if y == 0: raise 'error' else: print(x/y) safe_divide(a, b) safe_divide(c, d) ``` ---- 假如這個時候我們有另外一個 Task 要判斷餘數 ```python= def safe_divide(x, y): if y == 0: raise 'error' else: print(x/y) def safe_mod(x, y): if y == 0: raise 'error' else: print(x%y) ``` 身為一個懶惰的工程師, 覺得這兩個功能其實也很像 但是通常到這邊就會卡住了... ---- 試試看繼續把重複的地方拿出來好了 ```python= def check_denominator(deno): if deno == 0: raise 'error' def safe_divide(x, y): check_denominator(y) print(x/y) def safe_mod(x, y): check_denominator(y) print(x%y) ``` ---- 假如這個時候還要判斷結果是否大於一 ```python= def greater_than(n): if n <= 0: raise 'error' def check_denominator(deno): if deno == 0: raise 'error' def safe_divide(x, y): check_denominator(y) print(x/y) greater_than(x/y) def safe_mod(x, y): check_denominator(y) print(x%y) greater_than(x%y) ``` 身為懶惰的工程師... 不知道要怎麼繼續取出重複的地方了... ---- 我們經常設計一個 function 處理片段相同的程式, 但有沒有神奇的寫法可以處理「只前後相同」? ---- ```python= @check_denominator_and_greater_than def safe_divide(x, y): print(x/y) @check_denominator_and_greater_than def safe_mod(x, y): print(x%y) ``` --- ## Decorator ---- ### Decorator concept ```python= def f1(): print('in f1') f1 = deco(f1) ``` ---- ### Decorator usage ```python= @deco def f1(): print('in f1') ``` ---- ### Decorator ```python= def deco(func): def wrapped(): print('before func') func() print('after func') return wrapped @deco def f1(): print('in f1') f1() ``` --- ## Decorator - wrap - keep original func attribute ---- ```python= from functools import wraps def deco(func): # could do tricks here def wrapped(*args, **kwargs): print('before f1') func(*args, **kwargs) print('after f1') return wrapped @deco def f1(*args, **kwargs): '''f1 document''' print('in f1') print(f1.__name__) print(f1.__doc__) ``` ---- ```python= from functools import wraps def deco(func): # could do tricks here def wrapped(*args, **kwargs): print('before f1') func(*args, **kwargs) print('after f1') return wrapped @deco def f1(*args, **kwargs): '''f1 document''' print('in f1') print(f1.__name__) print(f1.__doc__) ``` ``` >>> wrapped >>> None ``` ---- ```python= from functools import wraps def deco(func): # could do tricks here @wraps(func) def wrapped(*args, **kwargs): print('before f1') func(*args, **kwargs) print('after f1') return wrapped @deco def f1(*args, **kwargs): '''f1 document''' print('in f1') print(f1.__name__) print(f1.__doc__) ``` ``` >>> f1 >>> f1 document ``` --- ## Decorator with argument ---- ```python= from functools import wraps def tag(name): def deco(func): @wraps(func) def wrapped(): print('<{}>'.format(name)) func() print('</{}>'.format(name)) return wrapped return deco @tag('p') def content(): print('content line') content() ``` ``` >>> <p> ... content line ... </p> ``` ---- ### Decorator assignment ```python= from functools import wraps def tag(name): def deco(func): @wraps(func) def wrapped(): print('<{}>'.format(name)) func() print('</{}>'.format(name)) return wrapped return deco tag_div = tag('div') @tag_div def content(): print('content line') content() ``` ``` >>> <div> ... content line ... </div> ``` --- ## Decorator Example ---- ### Example 1 - wrapper ```python= from functool import wraps def sub_command(func): @wraps(func) def wrapped(*args, **kwargs): try: func(*args, **kwargs) except MyException: print('some useful string') except Exception: print('please contact me: afun@gmail') return wrapped ``` ---- ### Example 2 - latest used cache ```python= from functools import lru_cache @lru_cache() def heavy_jobs(x): print('do some heavy jobs') return x + 1000 print(heavy_jobs(1)) print(heavy_jobs(1)) print(heavy_jobs(2)) ``` ``` >>> do some heavy jobs >>> 1001 >>> 1001 >>> do some heavy jobs >>> 1002 ``` ---- ### Example 3 - cache to json ```python= import json from functools import wraps def cache_json(filename): def deco(func): @wraps(func) def wrapped(*args, **kwargs): try: print('before try wrapped') return json.load(open(filename)) except FileNotFoundError: print('before except wrapped') data = func(*args, **kwargs) json.dump(data, open(filename, 'w')) return data return wrapped return deco @cache_json('heavy.json') def heavy_jobs(*args, **kwargs): # do heavy jobs print('before if statement') if 'count' in kwargs: print('in count check') return kwargs['count'] print('after if statement') return print(heavy_jobs(user='afun', count=5)) ``` --- ## Decorator Tips ```python= from functools import wraps def deco(func): # could do tricks here def wrapped(*args, **kwargs): print('before f1') func(*args, **kwargs) print('after f1') return wrapped @deco def f1(*args, **kwargs): '''f1 document''' print('in f1') print(f1.__name__) print(f1.__doc__) ``` 可以在 `# could do tricks here` 改寫 ---- ### function map ```python= from functools import wraps function_map = {} def deco(func): global function_map function_map[func.__name__] = func @wraps(func) def wrapped(func): func() return wrapped ``` ---- ### function map ```python= from flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'index page' @app.route('/hello') def hello(): return 'hello world' @app.route('/post/<int:post_id>') def show_post(post_id): return 'Post ID = {}'.format(post_id) ``` --- ## Context manager ---- ### Context manager class ```python= class my_context(): def __enter__(self): return 'in enter line' def __exit__(self): return 'in exit line' ``` ---- ### Context manager usage ```python= with my_context() as f: print('line 1') ``` ```python= context = my_context() enter = context.__enter__() try: print('line 1') except Exception as e: if context.__exit__(sys.exc_info): raise e else: context.__exit__() ``` --- ## Context manager decorator **@contextmanager** ---- ```python= from contextlib import contextmanager @contextmanager def my_context(): print('before yield') yield obj print('after yield') with my_context() as obj: print('line 1') print('line 2') ``` ``` >>> before yield >>> line 1 >>> line 2 >>> after yield ``` ---- **Generator** ```python= from contextlib import contextmanager @contextmanager def my_context(): print('before yield') yield obj print('after yield') ``` **Class** ```python= class my_context(): def __enter__(self): print('before yield') return obj def __exit__(self, *excinfo): print('after yield') return is_re_raise ``` --- ## Context manager limitation ---- ### Only one item in generator (Fail) ```python= from contextlib import contextmanager @contextmanager def my_context(): yield yield with my_context(): print('line 1') print('line 2') ``` ``` line 1 line 2 Traceback (most recent call last): File "<stdin>", line 3, in <module> File "/usr/lib/python3.5/contextlib.py", line 70, in __exit__ raise RuntimeError("generator didn't stop") RuntimeError: generator didn't stop ``` ---- ### Only one item in generator (Fail) ```python= from contextlib import contextmanager @contextmanager def my_context(): for i in range(10): yield ``` ---- ### Only one item in generator (Success) ```python= from contextlib import contextmanager @contextmanager def my_context(cond): if cond: yield else: yield ``` ```python= from contextlib import contextmanager @contextmanager def my_context(cond): try: yield except Exception_type: print('do exception handling') finally: print('do exception handling') ``` --- ## With statement - [PEP 343](https://www.python.org/dev/peps/pep-0343/) ``` Abstract This PEP adds a new statement "with" to the Python language to make it possible to factor out standard uses of try/finally statements. In this PEP, context managers provide __enter__() and __exit__() methods that are invoked on entry to and exit from the body of the with statement. ``` ---- ### factor out standard uses of try/finally statements ```python= # without 'with' statement fw = open(file_name, 'w') try: print('do things') finally: fw.close() ``` ```python= # with 'with' statement with open(file_name, 'w') as f: print('do things') ``` ---- ### Combo: `With` statement and `@contextmanager` ```python= from contextlib import contextmanager @contextmanager def tag(name): print('<{}>'.format(name)) yield print('</{}>'.format(name)) with tag('h1'): print('content') ``` ---- ### Combo: nested `with` ```python= with tag('div'): with tag('p'): print('content') with tag('div'), tag('p'): print('content') ``` ``` <div> <p> content </p> </div> ``` ---- ### Example: dump print to file ```python= from contextlib import redirect_stdout print('line 1') with redirect_stdout(fw): print('line 2') print('line 3') print('line 4') ``` ---- ### Example: simulation precision ```python= sim_mode = 'fixed_point' do_call_1() do_call_2() with sim_full_point(): do_call_3() with sim_fixed_point(): do_call_4() do_call_5() do_call_6() ``` ---- ### Example: pytest test exception ```python= import pytest import my def test_exception(): with pytest.raises(CustomError): my.do_jobs_have_exception() def test_exception_info(): with pytest.raise(CustomError) as excinfo: my.do_jobs_have_exception() assert excinfo.value.message == 'exception string' ``` ---- ### Example: timeit ```python= from contextlib import contextmanager import time @contextmanager def timeit(name=''): start_time = time.time() yield end_time = time.time() - start_time print(name, '{:.2f}'.format(end_time)) with timeit('block'): print('do something') @timeit() def fun1(): print('do somthing else') ``` --- ## ContextManager as a Decorator ([source](https://github.com/python/cpython/blob/3.6/Lib/contextlib.py#L33-L53)) ```python= class ContextDecorator(object): def __call__(self, func): @wraps(func) def inner(*args, **kwargs): with self._recreate_cm(): return func(*args, **kwargs) return inner ``` - no function information - could not modified return --- Thanks 歡迎找我討論 `Python` --- ## Reference - [functools.py](https://svn.python.org/projects/python/trunk/Lib/functools.py) - [click](http://click.pocoo.org/5/) - [contextlib.py](http://translate.sourceforge.net/doc/api/translate.misc.contextlib-pysrc.html#contextmanager) - [PEP 343](https://www.python.org/dev/peps/pep-0343/) - [Profiling by decorator and wit](http://tech.glowing.com/cn/python-profiling/)
{"metaMigratedAt":"2023-06-14T13:03:22.243Z","metaMigratedFrom":"Content","title":"2017 Python - decorator and with statement","breaks":true,"contributors":"[{\"id\":\"a4aebb74-5b02-43d6-8c4f-00e636e3a0d8\",\"add\":98,\"del\":115}]"}
    279 views