<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Decorator
Basic concept
基本的 decorator 概念就是把 function 當作參數丟入另一個 function
Step1:  Problem 1 
Step2:  Solution 1
Step3:  Supplement
Step4:  Decorator with argument
前面在使用 decorator 的時候都是直接加上 @ + decorator name
但你是否有想過既然他是個包裝 function 的 function, 那我們是不是也可以像一般 functino 那樣給予其他的值？
Step5:  Decorator assignment
Python 裏面所有的東西都是 Object, 既然是這樣那 decorator 應該也可以存成其他變數名稱並且正常使用
Step6:  Decorator assignment (2)
既然 decorator 是包裝其他 function, 那 decorator 應該也可以包裝 decorator ?
Step8:  Decorator scenario
Case 1
Step9:  Case 2
Step10:  Case 3
Step11:  Case 4
Step12:  Decorator trick
以上面那些 function 為例, 其實在 wrapped 之前可以做些額外的操作
Function map
Step13:  Flask
Flask 是一種 light-weight 的 python web framework, 因為環境沒有裝我也沒有真正使用過所以就只展示程式碼
Step14:  Context manager
Basic concept
所謂 context 就是上下文的意思, 跟 decorator 同樣的地方是他可以在執行跟結束 function 的時候有額外操作
Step15:  上面 with 的使用方式同等於下面列的程式碼
Step16:  Context manager decorator
內建函式庫 contextlib 中有個好用的 decorator contextmanager 可以很簡單的將你的 function 變成 context manager
Step17:  到這邊你可能會疑惑這兩邊哪裡一樣？然後 yield 的東西呢？
其實你可以把 contextlib.contextmanager 視為一個轉換工具
Step18:  Contextmanager limitation
Generator 中只允許一個物件
Step19:  with statement
所以說到底 with 是甚麼？ 根據 PEP 343 的說法, with 基本上就是 try/finally 的實現
```
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.
```
Step20:  with + @contextmanager
複習一下
Step21:  Case 1
Step22:  Case 2
Step23:  Study 1
<ASSISTANT_TASK:>
Python Code:
def deco(func):
    def wrapped():
        print('Before func')
        func()
        print('After func')
    return wrapped
@deco
def f1():
    print('This is f1 function')
f1()
Explanation: Decorator
Basic concept
基本的 decorator 概念就是把 function 當作參數丟入另一個 function
End of explanation
def deco(func):
    def wrapped():
        print('Before func')
        func()
        print('After func')
    return wrapped
@deco
def f1():
    print('This is f1 function')
    
print(f1.__name__)
Explanation: Problem 1 : lost original attribute
當我們用 decorator 把原本的 function 包起來的時候, 以上面的程式碼為例, 呼叫 f1() 其實是執行 wrapped()
這時候如果我們希望取得跟 f1 本身有關的屬性就存取不到了, 取而代之的會存取到 wrapped() 的參數
End of explanation
from functools import wraps
def deco(func):
    @wraps(func)
    def wrapped():
        print('Before func')
        func()
        print('After func')
    return wrapped
@deco
def f1():
    print('This is f1 function')
    
print(f1.__name__)
Explanation: Solution 1: Using the build-in functools.wraps
可以透過內建的 functolls.wraps 把原本 function 屬性保留下來
End of explanation
import time
from functools import wraps
def deco(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
        print('Before func')
        print('---')
        print('{} argument:\nargs = {}\nkwargs = {}\n'.format(
            func.__name__, args, kwargs
        ))
        func(*args, **kwargs)
        print('---')
        print('After func')
    return wrapped
@deco
def f1(*args, **kwargs):
    print('This is f1 function')
    
f1(1, '2', [3], {'4': 4}, time=time.ctime())
Explanation: Supplement: pass argument with decorator
如果執行 function 需要參數, 記得在 decorator 裡包裝 function 的時候也一並要傳入
End of explanation
from functools import wraps
def tag(name):
    def deco(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            print('<{}>'.format(name))
            func(*args, **kwargs)
            print('</{}>'.format(name))
        return wrapped
    return deco
@tag('p')
def content(*args, **kwargs):
    for i in args:
        print(i)
content('Hello World.', 'This is second argument.')
Explanation: Decorator with argument
前面在使用 decorator 的時候都是直接加上 @ + decorator name
但你是否有想過既然他是個包裝 function 的 function, 那我們是不是也可以像一般 functino 那樣給予其他的值？
End of explanation
from functools import wraps
def tag(name):
    def deco(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            print('<{}>'.format(name))
            func(*args, **kwargs)
            print('</{}>'.format(name))
        return wrapped
    return deco
tag_p = tag('p')
@tag_p
def content(*args, **kwargs):
    for i in args:
        print(i)
content('Hello World.', 'This is second argument.')
Explanation: Decorator assignment
Python 裏面所有的東西都是 Object, 既然是這樣那 decorator 應該也可以存成其他變數名稱並且正常使用
End of explanation
from functools import wraps
def tag(name):
    def deco(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            print('<{}>'.format(name))
            func(*args, **kwargs)
            print('</{}>'.format(name))
        return wrapped
    return deco
tag_div = tag('div')
tag_p = tag('p')
@tag_div
@tag_p
def content(*args, **kwargs):
    for i in args:
        print(i)
content('Hello World.', 'This is second argument.')
Explanation: Decorator assignment (2)
既然 decorator 是包裝其他 function, 那 decorator 應該也可以包裝 decorator ?
End of explanation
import time
import logging
from functools import wraps
LOGGER = logging.getLogger(__name__)
def func_profiling(func):
    @wraps(func)
    def wrapped(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        time_spent = time.time() - start_time
        fullname = '{}.{}'.format(func.__module__, func.__name__)
        LOGGER.debug('{}[args={}, kwargs={}] completed in {}'.format(
            fullname, args, kwargs, time_spent
        ))
        return result
    return wrapped
@func_profiling
def test_func_profiling(msg=None):
    import random
    sleep_sec = random.randrange(1,3)
    LOGGER.debug('random sleep in {} sec'.format(sleep_sec))
    time.sleep(sleep_sec)
    LOGGER.info(msg)
    LOGGER.debug('Wake up')
if __name__ == '__main__':
    testing
    import sys
    logging.basicConfig(
        level=logging.DEBUG,
        format='%(asctime)s %(filename)12s:L%(lineno)3s [%(levelname)8s] %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S',
        stream=sys.stdout
        )
    test_func_profiling('Hello World')
Explanation: Decorator scenario
Case 1: Profiling Log
可以透過 decorator 寫出漂亮的 Pythonic code (e.g. link)
Task: 重複操作「紀錄當下時間為起始時間」->「執行 function」->「當下時間減掉起始時間為 function 耗時時間」
搭配 logging 還可以設定 log level 為 DEBUG,　這樣可以非常輕鬆的 enable/ disable 這些訊息
End of explanation
from functools import wraps
def author(email):
    def sub_command(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            try:
                func(*args, **kwargs)
            except ValueError:
                '''you can use your own defined exception'''
                print('some useful message to debug')
            except Exception:
                print('Unexpected exception, please contant author: {}'.format(email))
        return wrapped
    return sub_command
@author('afun@example.com')
def divide(a, b):
    print('{}/{} = {}'.format(a, b, a/b))
    return a/b
divide(6, 2)
divide(6, 0)
Explanation: Case 2: Top function exception
如果你是一位有良心的工程師, 在你產品裏面應該會留有你的聯絡方式, 讓你的顧客或是交接的人在遇到 exception 的時候可以聯絡的到你
End of explanation
from functools import lru_cache
@lru_cache()
def heavy_jobs(x):
    print('do some heavy jobs with input {}'.format(x))
    return x+1000
print(heavy_jobs(1))
print(heavy_jobs(1))
print(heavy_jobs(2))
print(heavy_jobs(1))
print(heavy_jobs(2))
Explanation: Case 3: Least recently used cache
當我們的 function output 是可預期的 (同樣的 input 會有同樣的 output), 可以透過 lru_cache 把結果儲存起來, 下一次遇到同樣的 input 時就可以直接從 cache 取出結果而不用重新計算一次
詳細細節可以參考文件
End of explanation
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):
    print('do heavy jobs')
    if 'count' in kwargs:
        return kwargs['count']
    return
print(heavy_jobs(user='afun', count=5))
Explanation: Case 4: Cache to json file
對於快速的 debug 非常有幫助, 會把 exception 內容都存成 json
Hint: 這邊 demo 的時候可以操作連續兩次看哪裡不同, 試著觸發 wrapped 內的 exception
End of explanation
from functools import wraps
function_map = {}
def deco(func):
    global function_map
    function_map[func.__name__] = func
    
    @wraps(func)
    def wrapped(*args, **kwargs):
        func(*args, **kwargs)
    return wrapped
@deco
def f1():
    print('This is f1')
@deco
def f2():
    print('This is f2')
print(function_map)
Explanation: Decorator trick
以上面那些 function 為例, 其實在 wrapped 之前可以做些額外的操作
Function map
End of explanation
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 {}'.format(post_id)
Explanation: Flask
Flask 是一種 light-weight 的 python web framework, 因為環境沒有裝我也沒有真正使用過所以就只展示程式碼
End of explanation
class my_context(object):
    def __enter__(self):
        print('in enter')
        return 'enter'
    def __exit__(self, *excinfo):
        print('in exit')
        return 'exit'
with my_context() as f:
    print('Hello')
    print('World')
Explanation: Context manager
Basic concept
所謂 context 就是上下文的意思, 跟 decorator 同樣的地方是他可以在執行跟結束 function 的時候有額外操作
End of explanation
context = my_context()
obj = context.__enter__()
try:
    print('Hello')
    print('World')
except Exception as e:
    if context.__exit__(sys.exc_info()):
        raise e
else:
    context.__exit__()
Explanation: 上面 with 的使用方式同等於下面列的程式碼
End of explanation
from contextlib import contextmanager
@contextmanager
def my_context():
    print('do things in enter')
    yield 'It is a feature, not a bug!!!'
    print('do things in exit')
with my_context() as obj:
    print('Hello')
    print('World')
Explanation: Context manager decorator
內建函式庫 contextlib 中有個好用的 decorator contextmanager 可以很簡單的將你的 function 變成 context manager
End of explanation
@contextmanager
def my_context():
    print('do things in enter')
    yield 'It is a feature, not a bug!!!'
    print('do things in exit')
# 透過 contextmanager 轉換, 與下面這段程式碼等價
class my_context(object):
    def __enter__(self):
        print('do things in enter')
        return 'It is a feature, not a bug!!!'
    def __exit__(self, *excinfo):
        print('do things in exit')
        return 'exit'
Explanation: 到這邊你可能會疑惑這兩邊哪裡一樣？然後 yield 的東西呢？
其實你可以把 contextlib.contextmanager 視為一個轉換工具
End of explanation
from contextlib import contextmanager
@contextmanager
def my_context():
    yield 'Hello'
    yield 'World'
with my_context():
    print('line 1')
    print('line 2')
from contextlib import contextmanager
@contextmanager
def context_loop():
    for i in range(100):
        yield i
with context_loop():
    print('line 1')
    print('line 2')
from contextlib import contextmanager
@contextmanager
def context_condition(cond):
    if cond:
        yield 'in condition'
    else:
        yield 'else case'
with context_condition(True) as f1:
    print(f1)
    print('line 1')
    print('line 2')
with context_condition(False) as f2:
    print(f2)
    print('line 3')
    print('line 4')
from contextlib import contextmanager
@contextmanager
def context_try(cond):
    try:
        yield 'normal'
    except Exception:
        print('exception')
    finally:
        print('finally')
with context_try(True):
    print('line 1')
    print('line 2')
Explanation: Contextmanager limitation
Generator 中只允許一個物件
End of explanation
fp = open('test.txt', 'w+')
try:
    fp.write('Hello world')
finally:
    fp.close()
    
# 上面這種寫法透過 with 實現的話會變成下面這種程式碼
with open('test.txt', 'w+') as fp:
    fp.write('Hello world')
Explanation: with statement
所以說到底 with 是甚麼？ 根據 PEP 343 的說法, with 基本上就是 try/finally 的實現
```
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.
```
End of explanation
from contextlib import contextmanager
@contextmanager
def tag(name):
    print('<{}>'.format(name))
    yield
    print('</{}>'.format(name))
with tag('div'):
    print('content 1')
from contextlib import contextmanager
@contextmanager
def tag(name):
    print('<{}>'.format(name))
    yield
    print('</{}>'.format(name))
# nested
with tag('div'):
    with tag('p'):
        print('content 1')
print()
        
# multiple
with tag('div'), tag('h1'):
    print('content 2')
Explanation: with + @contextmanager
複習一下: 我們可以透過 with 來做 try/finally 的實現, 還可以透過 contextmanager 來實現上下文管理, 那如果同時使用這兩個呢？
在 try/finally 架構中實現上下文管理
先經過 __enter__ entry, 然後 try/finally 本文, 最後再透過 __exit__ 離開這個 block
End of explanation
from contextlib import redirect_stdout
with open('test_redirect.txt', 'w+') as f:
    with redirect_stdout(f):
        help(redirect_stdout)
        print('afun defined message')
Explanation: Case 1: redirect stdout to file
重新把原本會輸出在 stdout 的訊息導到 file descriptor
End of explanation
from contextlib import contextmanager
import time
@contextmanager
def timeit(name=''):
    start_time = time.time()
    yield
    elapse_time = time.time() - start_time
    print('{} - completed in {:.6f}'.format(name, elapse_time))
# note: comprehension speed seens slower when range(10)
test = [i for i in range(100000)]
with timeit('afun 1'):
    a = [i for i in test if i % 2 == 0]
with timeit('afun 2'):
    b = []
    for i in test:
        if i % 2 == 0:
            b.append(i)
@timeit('afun 3')
def func(test_list):
    result = []
    for i in test_list:
        if i % 2 == 0:
            result.append(i)
    return result
c = func(test)
Explanation: Case 2: timeit
前面有提到透過 decorator 來做 function 的 profiling, 但假如我今天不是要對 function, 而是單純要對某個 block 的程式碼做 profiling 呢? 是否要把這個 block 包成 function 再做 profiling ?
這個時候上面提到的 @contextmanager 就派上用場了, 透過 contextmanager 在 entry point 跟 exit point 都紀錄時間來計算耗時
End of explanation
from functools import wraps
# contextmanager 原始碼
# 這邊就是我們上面提過的 decorator 寫法
# 雖然他是回傳 _GeneratorContextManager 這個 helper class, 但其實這邊主要是透過 ContextDecorator 實作
def contextmanager(func):
    @wraps(func)
    def helper(*args, **kwds):
        return _GeneratorContextManager(func, args, kwds)
    return helper
# ContextDecorator 原始碼
# _recreate_cm 會回傳自己的 instance
class ContextDecorator(object):
    def _recreate_cm(self):
        return self
    
    def __call__(self, func):
        @wraps(func)
        def inner(*args, **kwds):
            with self._recreate_cm():
                return func(*args, **kwds)
        return inner
Explanation: Study 1: Detail of contextmanager
contextmanger 是一個為了 with statement 做出來不用特別建立具有 __enter__ 跟 __exit__ class 的 decorator
我們可以觀察原始碼近一步了解內部運作機制
contextmanager
ContextDecorator
End of explanation
<END_TASK>