# 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}]"}