{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Python Advanced" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "\n", "def table(table_name): \n", " return pd.read_csv(f'./tables/{table_name}.csv').fillna('')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Slicing Time Complexity\n", "\n", "* NumPy slicing is view, while native python list and str slicing is copy\n", " * 跑 `arr[j:i]` 時 NumPy 不會建立新的資料,而是建立一個指向原始陣列的 view\n", " * 這個視圖只改變了 shape 和 strides(步幅),不會複製底層資料:`O(1)`\n", " * 若你使用 advanced indexing 如 `arr[[1, 3, 5]]`,就會建立新的陣列:`O(k)`\n", "\n", "| 類型 | 切片結果 | Time | 備註 |\n", "|-----------|---------------|------------|--------------------------|\n", "| `str` / `list` | copy | `O(k)` | 複製 `k` 個元素 |\n", "| `numpy.array` | view | `O(1)` | 不複製資料,只改 metadata |" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Multithreading and Multiprocessing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* See [this SO post](https://stackoverflow.com/questions/3044580/multiprocessing-vs-threading-python). Threads run in the same memory space, while processes have separate memory. 一個 process 有自己的獨立的記憶體,甚至 IO\n", "* 在 windows,multiprocessing 會很慢,因為每個 process 都重新 new 一個 python interpreter session,在 Unix-like systems 不用。看[這個 SO post](https://stackoverflow.com/questions/28744046/multiprocessing-python-not-running-in-parallel)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
MultithreadingMultiprocessing
0light weightheavy, more memory overhead
1share memoryisolated
2easy to communicatehard
3safety concern (race condition, deadlocks)safe
4good for I/O bound tasksgood for CPU bound tasks
\n", "
" ], "text/plain": [ " Multithreading Multiprocessing\n", "0 light weight heavy, more memory overhead\n", "1 share memory isolated\n", "2 easy to communicate hard\n", "3 safety concern (race condition, deadlocks) safe\n", "4 good for I/O bound tasks good for CPU bound tasks" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "table('multi_threading_processing')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Global Interpreter Lock (GIL) and Multithreading\n", "\n", "* GIL is a mutex,同時只能有一個 thread 執行 Python bytecodes,所以 Python 沒有真正的 multithreading\n", "* Python multithreading 適合處理 **I/O bound tasks** 因為要花很多時間等外部資源" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Race Condition in Multithreading" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* 兩個 Thread 同時跑 ++counter,等兩個都跑完 counter 的值還是 1 (example from [wikipedia](https://en.wikipedia.org/wiki/Race_condition#Example))" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
Thread 1Thread 2Integer value
00
1read value0
2read value0
3increase value0
4increase value0
5write back1
6write back1
\n", "
" ], "text/plain": [ " Thread 1 Thread 2 Integer value\n", "0 0\n", "1 read value ← 0\n", "2 read value ← 0\n", "3 increase value 0\n", "4 increase value 0\n", "5 write back → 1\n", "6 write back → 1" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "table('race_condition')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* 如何避免: \n", " * Locks: Use mutexes \n", " * Atomic Operations: Use atomic indivisible operations\n", " * Thread-Safe Data Structures: Use data structures designed to handle concurrent access\n", " * Immutable Data Structures: Once created, they cannot be changed" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Regex\n", "\n", "* `re.match` 只抓 string 開頭的 match,`re.search` 在整個 string 裡找 first match\n", "* `\\number`\n", " * 把一個 regex 放在括號裡 `(...)`,就會變成一個 capturing group. \n", " * `\\number` 是第 number 個 capturing group,以 opening parentheses 出現的順序決定\n", " * For [example](https://docs.python.org/3/library/re.html), `(.+) \\1` 抓到 `'the the'` 或 `'55 55'`,但抓不到 `'thethe'`,因為 group 後面有一個 space\n", " * 把 group 放在 [findall](https://docs.python.org/3/library/re.html#re.findall) 裡的時候要小心。有 group 的時候 findall 會只抓 group 而不是 full match,例如" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "['museum']\n", "\n" ] } ], "source": [ "import re\n", "\n", "s = \"the world is a museum museum of passion projects\"\n", "\n", "print(re.findall(r\"(.+) \\1\", s))\n", "print( re.search(r\"(.+) \\1\", s))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* In ASCII (遇到 Unicode,例如中文字,規則有點不一樣):" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
符號意義等價於範例
0\\d數字(digit)[0–9]re.findall(r\"\\d\", \"A1B2\") → ['1', '2']
1\\s空白(whitespace)[ \\t\\n\\r\\f\\v]re.findall(r\"\\s\", \"a b\\tc\\n\") → [' ', '\\t', '\\n']
2\\w英數底線(word char)[a-zA-Z0-9_]re.findall(r\"\\w\", \"_Hi123\") → ['_', 'H', 'i', ...
3
4\\D非數字[^0–9]re.findall(r\"\\D\", \"A1!\") → ['A', '!']
5\\S非空白[^ \\t\\n\\r\\f\\v]re.findall(r\"\\S\", \"a b\") → ['a', 'b']
6\\W非英數底線[^a-zA-Z0-9_]re.findall(r\"\\W\", \"!@#^\") → ['!', '@', '#', '^']
\n", "
" ], "text/plain": [ " 符號 意義 等價於 \\\n", "0 \\d 數字(digit) [0–9] \n", "1 \\s 空白(whitespace) [ \\t\\n\\r\\f\\v] \n", "2 \\w 英數底線(word char) [a-zA-Z0-9_] \n", "3 \n", "4 \\D 非數字 [^0–9] \n", "5 \\S 非空白 [^ \\t\\n\\r\\f\\v] \n", "6 \\W 非英數底線 [^a-zA-Z0-9_] \n", "\n", " 範例 \n", "0 re.findall(r\"\\d\", \"A1B2\") → ['1', '2'] \n", "1 re.findall(r\"\\s\", \"a b\\tc\\n\") → [' ', '\\t', '\\n'] \n", "2 re.findall(r\"\\w\", \"_Hi123\") → ['_', 'H', 'i', ... \n", "3 \n", "4 re.findall(r\"\\D\", \"A1!\") → ['A', '!'] \n", "5 re.findall(r\"\\S\", \"a b\") → ['a', 'b'] \n", "6 re.findall(r\"\\W\", \"!@#^\") → ['!', '@', '#', '^'] " ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "table('regex_special_char')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* `\\t\\n\\r\\f\\v` 分別是 Tab,換行,回車,換頁,垂直定位" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Set Instance Attributes on the Fly\n", "\n", "* `__getattribute__` is called for all attribute access, regardless of whether the attribute exists\n", "* `__getattr__` is called when an attribute is not found in `__getattribute__`\n", "* Example from [here](https://medium.com/@satishgoda/python-attribute-access-using-getattr-and-getattribute-6401f7425ce6): " ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "class Yeah(object):\n", " def __init__(self, name):\n", " self.name = name\n", " \n", " # Gets called when an attribute is accessed\n", " def __getattribute__(self, item):\n", " print('__getattribute__ '+ item)\n", " # Calling the super class to avoid recursion\n", " return super(Yeah, self).__getattribute__(item)\n", " \n", " # Gets called when the item is not found via __getattribute__\n", " def __getattr__(self, item):\n", " print('__getattr__ '+ item)\n", " return super(Yeah, self).__setattr__(item, 'orphan')" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "__getattribute__ name\n" ] }, { "data": { "text/plain": [ "'yes'" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "y1 = Yeah('yes')\n", "y1.name" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "__getattribute__ foo\n", "__getattr__ foo\n" ] } ], "source": [ "y1.foo" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "__getattribute__ foo\n" ] }, { "data": { "text/plain": [ "'orphan'" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "y1.foo" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "__getattribute__ goo\n", "__getattr__ goo\n" ] } ], "source": [ "y1.goo" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "__getattribute__ __dict__\n" ] }, { "data": { "text/plain": [ "{'name': 'yes', 'foo': 'orphan', 'goo': 'orphan'}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "y1.__dict__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Singleton" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "class Singleton:\n", " _instance = None\n", "\n", " def __new__(cls):\n", " if not cls._instance:\n", " cls._instance = super(Singleton, cls).__new__(cls)\n", " return cls._instance\n", "\n", "o1 = Singleton()\n", "o2 = Singleton()\n", "\n", "o1 is o2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* `super(Singleton, cls)` returns a temporary object of the superclass, which in this case is object as every class in Python inherits from object by default\n", "* `__new__(cls)` is a special method in Python classes that is responsible for instance creation. It takes the class (not the instance) as the first argument followed by any additional arguments if needs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [AsyncIO](https://www.youtube.com/watch?v=K56nNuBEd0c)\n", "\n", "* The `async` keyword makes a function (subroutine) a coroutine\n", " * Subroutines block the process, coroutines don't\n", "* An `async` coroutine can have awaitable statements (starting with the `await` keyword) which specify where in the coroutine is safe to pause and yield control to other coroutines\n", " * `await` can only be put in front of a statement that is awaitable\n", " * `time.sleep(3)` is not awaitable. Its awaitable version is `asyncio.sleep(3)`\n", "* `brew_coffee()` is not a regular function call. It returns a coroutine object which can be gathered with other coroutines\n", "* Can either create a batch with `asyncio.gather` or a single task by `asyncio.create_task`\n", "* To run the coroutines:\n", " * `await` the created task or batch, or\n", " * `asyncio.run` it (doesn't work in Jupyter)\n", "* main function has an `await` statement now so it must become an `async` coroutine" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Start brew_coffee()\n", "Start toast_bagel()\n", "End toast_bagel()\n", "End brew_coffee()\n", "Result of brew_coffee: Coffee ready\n", "Result of toast_bagel: Bagel ready\n", "Total execution time: 3.00 seconds\n" ] } ], "source": [ "import asyncio\n", "import time\n", "\n", "async def brew_coffee():\n", " print('Start brew_coffee()')\n", " await asyncio.sleep(3)\n", " print('End brew_coffee()')\n", " return 'Coffee ready'\n", "\n", "async def toast_bagel():\n", " print('Start toast_bagel()')\n", " await asyncio.sleep(2)\n", " print('End toast_bagel()')\n", " return 'Bagel ready'\n", "\n", "async def main1():\n", " start_time = time.time()\n", "\n", " #########################################################\n", " batch = asyncio.gather(brew_coffee(), toast_bagel())\n", " result_coffee, result_bagel = await batch\n", " #########################################################\n", " \n", " end_time = time.time()\n", " elapsed_time = end_time - start_time\n", "\n", " print(f'Result of brew_coffee: {result_coffee}')\n", " print(f'Result of toast_bagel: {result_bagel}')\n", " print(f'Total execution time: {elapsed_time:.2f} seconds')\n", "\n", "async def main2():\n", " start_time = time.time()\n", "\n", " #########################################################\n", " coffee_task = asyncio.create_task(brew_coffee())\n", " bagel_task = asyncio.create_task(toast_bagel())\n", "\n", " result_coffee = await coffee_task\n", " result_bagel = await bagel_task\n", " #########################################################\n", "\n", " end_time = time.time()\n", " elapsed_time = end_time - start_time\n", "\n", " print(f'Result of brew_coffee: {result_coffee}')\n", " print(f'Result of toast_bagel: {result_bagel}')\n", " print(f'Total execution time: {elapsed_time:.2f} seconds')\n", "\n", "\n", "# asyncio.run(main1()) # RuntimeError: asyncio.run() cannot be called from a running event loop\n", "\n", "main_task = asyncio.create_task(main2())\n", "res = await main_task\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# simple version\n", "\n", "import asyncio\n", "\n", "async def brew_coffee():\n", " await asyncio.sleep(3)\n", "\n", "async def main():\n", " coffee_task = asyncio.create_task(brew_coffee())\n", " result_coffee = await coffee_task\n", "\n", " # same way to call main(): await a asyncio created task\n", " # or asyncio.run(main()) which doesn't work in Jupyter" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### AsyncIO and Multiprocessing\n", "\n", "* Asyncio enables concurrency, but not parallelism by default\n", "* You can achieve parallelism by integrating thread pools and process pools\n", "* **Asyncio shines for I/O-bound workloads, like network calls and file operations**\n", "* For CPU-bound tasks, multiprocessing may provide better utilization" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "# Example by ChatGPT, working when run by python but not in Jupyter\n", "\n", "import asyncio\n", "from concurrent.futures import ProcessPoolExecutor\n", "\n", "def cpu_bound_task(n):\n", " import time\n", " time.sleep(2)\n", " return f'Task {n} result'\n", "\n", "async def main():\n", " loop = asyncio.get_running_loop()\n", " with ProcessPoolExecutor() as executor:\n", " tasks = [loop.run_in_executor(executor, cpu_bound_task, i) for i in range(5)]\n", " res = await asyncio.gather(*tasks)\n", " print(res)\n", "\n", "# if __name__ == '__main__':\n", "# asyncio.run(main())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### AsyncIO and Decorator\n", "\n", "* [Need two decorators?](https://stackoverflow.com/questions/42043226/using-a-coroutine-as-decorator)\n", "* [A decorator that can wrap both functions and coroutines](https://stackoverflow.com/questions/42043226/using-a-coroutine-as-decorator/77604618#77604618) -- using `inspect.iscoroutinefunction`" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### AsyncIO for Fixings Registration" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "# __init__.py\n", "\n", "import asyncio\n", "\n", "data = None\n", "data_ready = asyncio.Event()\n", "\n", "async def get_data():\n", " global data\n", " # 模擬抓取資料的耗時操作\n", " await asyncio.sleep(3)\n", " data = {\"key\": \"value\"}\n", " data_ready.set()\n", "\n", "def init():\n", " asyncio.create_task(get_data())\n", "\n", "# 初始化\n", "init()" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "# client code, bar is the pricing function\n", "\n", "import asyncio\n", "from concurrent.futures import ThreadPoolExecutor\n", "\n", "data = None\n", "data_ready = asyncio.Event()\n", "executor = ThreadPoolExecutor(max_workers=1)\n", "\n", "def prepare_data():\n", " # 模擬一個耗時計算\n", " import time\n", " time.sleep(5)\n", " return {\"key\": \"value\"}\n", "\n", "async def get_data():\n", " global data\n", " loop = asyncio.get_event_loop()\n", " data = await loop.run_in_executor(executor, prepare_data)\n", " data_ready.set()\n", "\n", "def bar():\n", " loop = asyncio.get_event_loop()\n", " if not data_ready.is_set():\n", " loop.run_until_complete(data_ready.wait())\n", " print(f\"Data is ready: {data}\")\n", "\n", "async def main():\n", " await init()\n", " print(\"Doing other tasks while waiting for data...\")\n", " await asyncio.sleep(1)\n", " print(\"Still doing other tasks...\")\n", " bar()\n", "\n", "async def init():\n", " asyncio.create_task(get_data())\n", "\n", "# 執行範例\n", "# asyncio.run(main())\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Computing Grid Summary\n", "\n", "* Python features required:\n", " * Packaging\n", " * Consistent venv in all computers\n", " * Dashboard\n", " * Entry point (CLI apps)\n", " * Config (ini) file\n", "* 3 packages + workers env: \n", "* `qmagrid_server`\n", " * `multiprocessing.managers.BaseManager.register` shared data structures in the network\n", " * Shared `multiprocessing.Manager().Queue()` and `multiprocessing.Manager().dict()`: \n", " * `waiting_q` (Queue)\n", " * `working_q` (dict)\n", " * `result_q` (dict)\n", " * `status_q` (dict)\n", " * `machine_q` (dict)\n", " * `multiprocessing.Manager()` data structures have lock so is safe\n", " * monitor by Plotly Dash displaying status queue contents\n", "* `qmagrid_client`\n", " * Depends on the `Job` class in the worker package\n", " * Implements context manager `QMAGridExecutor` to send cloudpicked jobs to `waiting_q` and wait to collect results from `result_q`\n", "```python\n", "with QMAGridExecutor() as executor:\n", " executor.map(f, args_list)\n", "```\n", "* `qmagrid_worker`\n", " * cmd commands to run `start_one_worker`, `start_pct_workers`, `start_n_workers` and `stop_all_workers`\n", " * `start_n_workers` (`start_pct_workers`) simply `subprocess.Popen`s `start_one_worker` n times and start sending status report\n", " * `start_one_worker` checks `waiting_q` constantly and if there is a job, do the following\n", " * pop from `waiting_q` and push to `working_q`\n", " * run the job\n", " * pop from `working_q` and push the result to `result_q`\n", " * It does so as long as the corresponding status report remains in the `status_q`\n", " * Parses a config file to determine (WIP)\n", " * Server IP (which grid?)\n", " * Percentage of all logical cores to contribute\n", "* The `workers` env\n", " * Turn on workers in this env to make sure of consistent package versions\n", " * A watcher process watching a commands.txt on shared drive. Once the file is modified, execute the commands in it" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The QMA Python Package Summary\n", "\n", "* Conveniently call requests on trades: \n", " * `Swaption().NPV()`\n", " * Default instruments swaption is 1y10y\n", " * Can call Delta, Gamma, Vega, etc.\n", " * Live trade support\n", " * `Trade(12345678).NPV()`\n", " * Flexible requests\n", " * `BermudanSwaption().CalibrationInfo()`\n", " * Trade spec attributes\n", " * `Trade(12345678).notional()` or currency, etc. \n", " * From trade JSON, not from the core library, but users don't need to know\n", "* Singleton `MarketEnv` context manager class\n", "* Config file:\n", " * Quants default `MarketEnv` to previous day EOD, while traders default to today LIVE\n", " * Default books and products for trade population, extendible to other businesses\n", " * Default env: prod, dev or pat\n", "* Job scheduler: \n", " * Which computers run which functions at what times with what arguments specified in a `scheduled_jobs.csv`\n", " * Examples:\n", " * Copytree\n", " * Check if a file exists at certain time and send email notifications\n", "* Debug sheet generation\n", " * `Trade(12345678).excel_render()`\n", "* `auto_spreadsheet()`\n", "* AsyncIO for fixings registration" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## List Comprehension With Multiple For Loops\n", "\n", "The following are [equivalent](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions): " ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "combs = []\n", "for x in [1,2,3]:\n", " for y in [3,1,4]:\n", " if x != y:\n", " combs.append((x, y))\n", "combs" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Virtual Environments\n", "\n", "* `python -m venv \".myenv\"` to create\n", "* `source .myenv/bin/activate` to activate on linux\n", "* `.myenv/Scripts/activate.bat` to activate on windows \n", "* `deactivate` to deactivate" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [redbull.py](https://github.com/christophercylai/python/blob/main/redbull.py)\n", "\n", "```python\n", "r\"\"\"\n", "This script keeps the computer awake by pressing right ctrl key every SEC seconds\n", "Put this script in $USERPROFILE\\Downloads for easy access\n", "To setup using PowerShell:\n", "> cd $env:USERPROFILE\\Downloads\n", "> python -m venv .venv\n", "> .venv\\Scripts\\activate\n", "> pip install --trusted-host files.pythonhosted.org --trusted-host pypi.org pyautogui\n", "> python redbull.py\n", "\"\"\"\n", "import pyautogui\n", "from time import sleep\n", "\n", "SEC = 180\n", "pyautogui.FAILSAFE = False\n", "\n", "while True:\n", " sleep(SEC)\n", " pyautogui.press('ctrlright')\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Where Is My Python? " ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'/srv/conda/envs/notebook/bin'" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import sys, os\n", "\n", "os.path.dirname(sys.executable)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## pip\n", "\n", "* `pip uninstall` 只能在 shell 裡用,notebook 沒辦法用,因為會有 `continue? (y/n)`\n", "* Configure pip to install from other server\n", " * `pip config -v list` 查 config 都去哪裡找([so](https://stackoverflow.com/questions/28278207/python-cant-find-pip-ini-or-pip-conf-in-windows))\n", " ```\n", " For variant 'global', will try loading 'C:\\ProgramData\\pip\\pip.ini'\n", " For variant 'user', will try loading 'C:\\Users\\foobar\\pip\\pip.ini'\n", " For variant 'user', will try loading 'C:\\Users\\foobar\\AppData\\Roaming\\pip\\pip.ini'\n", " For variant 'site', will try loading 'C:\\Python38\\pip.ini'\n", " ```\n", " * 去這些 folder 建一個 `pip.ini` 裡面貼\n", " ```\n", " [global]\n", " timeout = 60\n", " index = https://repo.abc.com/repository/pypi-all/pypi\n", " index-url = https://repo.abc.com/repository/pypi-all/simple\n", " trusted-host = repo.abc.com\n", " ```\n", " * 也可以直接在 shell 執行下面四行,log 會自己顯示 config file 存到哪去了\n", " ```\n", " pip config set global.timeout 60\n", " pip config set global.index https://repo.abc.com/repository/pypi-all/pypi\n", " pip config set global.index-url https://repo.abc.com/repository/pypi-all/simple\n", " pip config set global.trusted-host repo.abc.com\n", " ```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Code Packaging\n", "\n", "* install locally\n", " * `cd` to the top project directory where `setup.py` is \n", " * `python setup.py install` or `pip install .` or `pip install -e .` [for developer install](https://stackoverflow.com/questions/19048732/python-setup-py-develop-vs-install)\n", "* [All license badges](https://gist.github.com/lukas-h/2a5d00690736b4c3a7ba)\n", "* Upload to PyPI: `cd` to the top project directory where `setup.py` is \n", "\n", "```sh\n", "git checkout 0.0.1\n", "python setup.py sdist\n", "twine check dist/*\n", "twine upload --repository-url https://test.pypi.org/legacy/ dist/*\n", "twine upload dist/*\n", "```\n", "\n", "* 每次 implement 一個新的 function,如果 test 裡需要 import,要記得先加進 `__init__.py` 裡\n", "* Remove a package from PyPI\n", " * Login > your projects > pyminimax > Manage > Settings > Delete project\n", "* [Deploying a Cython Package to PyPI](https://levelup.gitconnected.com/how-to-deploy-a-cython-package-to-pypi-8217a6581f09)\n", "* [Building a conda package and uploading it to Anaconda Cloud (medium)](https://giswqs.medium.com/building-a-conda-package-and-uploading-it-to-anaconda-cloud-6a3abd1c5c52)\n", "* [How to put a swig/pybind11 C++ project on pypi (so)](https://stackoverflow.com/questions/42656388/how-to-put-a-swig-pybind11-c-project-on-pypi)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## C and C++ Extensions\n", "\n", "* CPython is the [reference implementation](https://en.wikipedia.org/wiki/Reference_implementation) of the Python programming language\n", "* [Python Bindings: Calling C or C++ From Python (Real Python)](https://realpython.com/python-bindings-overview/)\n", " * ctypes\n", " * CFFI\n", " * pybind11:改自 Boost.Python,較快但只支援 c++11 或更新的版本\n", " * Cython\n", " * Other Solutions\n", " * SWIG\n", " * [這裡](https://github.com/diegoferigo/cmake-build-extension)說最常用的是 SWIG 和 pybind11\n", "* [Building C and C++ Extensions with distutils (Python Doc)](https://docs.python.org/3/extending/building.html#building-c-and-c-extensions-with-distutils)\n", " * `python setup.py build` 會編譯 `ext_modules` 裡指定的 c code,但指定在這裡的 c code 需要是處理 PyObject 的才能在 Python 裡不透過 `ctypes` 直接呼叫\n", " * 如果 target machine 有 c/c++ compiler(linux 都有)可能可以直接 source distribute C extensions\n", " * [Mac OS 自帶 clang](https://stackoverflow.com/questions/33007724/can-a-c-program-run-on-mac-os)\n", " * Windows 不一定有 compiler 所以至少需要 Windows Wheel\n", "* [cibuildwheel](https://github.com/pypa/cibuildwheel)\n", " * GitHub Actions building wheels in all common platforms\n", " * by [Python Packaging Authority](https://www.pypa.io/en/latest/), also see [Python Packaging User Guide](https://packaging.python.org/)\n", "* [cmake-build-extension](https://github.com/diegoferigo/cmake-build-extension)\n", " * Setuptools extension to build and package CMake projects\n", " * CMake 自帶 SWIG 和 pybind11 support。這個包把 setuptools 和 CMake 接起來,可以直接在 setup.py configure CMake project" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### SWIG\n", "\n", "* 要先 `sudo apt install swig`\n", "* [Official Doc](http://www.swig.org) 和 David \"Mr. Swig\" Beazley 寫的 [PyCon 2008 slides](http://www.dabeaz.com/SwigMaster/SWIGMaster.pdf)\n", "* 可以把 c/c++ 接到多種語言,只要寫同一份 interface files (*.i)\n", "* 如果 Extension source files 裡有 interface file,distutils/setuptools 會自動跑 SWIG。看 [Python doc](https://docs.python.org/3.6/distutils/setupscript.html#extension-source-files) 和 [PyCon 2008 slides](http://www.dabeaz.com/SwigMaster/SWIGMaster.pdf) 第 22 頁\n", " * 應該是這台機器上要先灌好 SWIG 才行\n", " * 所以應該沒辦法直接 source distribute *.i\n", " * 如果一台機器上有 c/c++ compiler,倒是可以 source distribute SWIG 產生的 wrapper\n", " * [Python doc](https://docs.python.org/3.6/distutils/setupscript.html#extension-source-files) 示範了怎麼 package SWIG:在 setup 裡放 `py_modules=['foo'],` 和 `ext_modules=[Extension('_foo', ['foo.i'], swig_opts=['-modern', '-I../include'])],`\n", "* Python module [必需是 so 或 pyd file](https://docs.python.org/3/extending/building.html#building-c-and-c-extensions-with-distutils),而且原碼的 c 函數 input/output type 要是 PyObject。SWIG 只負責看著正常的 c 函數寫 wrapper\n", "* [這裡](https://www.youtube.com/watch?v=g8--GrdlqGw)有講怎麼接 numpy array\n", "* [也有可以從 c++ 呼叫 Python 函數](https://stackoverflow.com/questions/12392703/what-is-the-cleanest-way-to-call-a-python-function-from-c-with-a-swig-wrapped)\n", "* 更多 c++ class 相關看[這裡](http://www.swig.org/Doc1.3/SWIGDocumentation.html#SWIGPlus)\n", "* Example\n", " * `swig -c++ -python libswig.i` 產生 `libswig_wrap.cxx` 和 `libswig.py`\n", " * `libswig.py` 是 module frond end\n", " * `libswig_wrap.cxx` 是 wrapper code,裡面有 input/output type 都是 PyObject 的 c 函數\n", " * 這兩個檔是 portable,和平台無關。所有有 c 編譯器的機器上都可以編譯這個 Python module,也不需要 SWIG\n", " * 如果只是 c code 而沒有 c++ 可以省略 `-c++` flag:`swig -python libswig.i`,產生出來的 wrapper 會是 `libswig_wrap.c` 而不是 cxx\n", " * 用 g++ 編譯 c++,用 gcc 編譯 c\n", " * 把 `libswig_wrap.cpp` 和 `libswig.cxx` 一起編譯。`-I` 是 include,後面的 path 裡放了 Python 相關的 header files,例如 `Python.h`\n", "\n", "```cpp\n", "// libswig.cpp\n", "\n", "#include \"libswig.hpp\"\n", "\n", "std::vector my_range(int n){ \n", " \n", " std::vector vec = {};\n", " for (int i=0 ; i\n", "\n", "std::vector my_range(int n);\n", "double square(double x);\n", "double cube(double x);\n", "```\n", "\n", "```cpp\n", "// libswig.i\n", "\n", "%module libswig\n", "%{\n", "#include \"libswig.hpp\"\n", "%}\n", "#define __version__ \"0.0.1\";\n", "std::vector my_range(int n); // or simply %include \"libswig.hpp\"\n", "double square(double x);\n", "double cube(double x);\n", "```" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "!swig -c++ -python libswig.i" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "!g++ -fPIC -c libswig.cpp libswig_wrap.cxx -I/srv/conda/envs/notebook/include/python3.7m" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "!g++ -shared libswig.o libswig_wrap.o -o _libswig.so" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('0.0.1',\n", " 25.0,\n", " 125.0,\n", " *' at 0x7ff1047bcea0>)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import libswig\n", "\n", "libswig.__version__, libswig.square(5), libswig.cube(5), libswig.my_range(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Cython\n", "\n", "* Example from [here](https://github.com/shekhar270779/Learn_Pandas/blob/master/Cython%20in%20Jupyter-notebook.ipynb) and [this tutorial](https://www.youtube.com/watch?v=CC0IkiNByV4)\n", "* `pip install Cython`\n", "* `%%cpython` 開頭的 cell 會被 cython 編譯,`%%cpython -a` 可以看哪一行有回到 python\n", "* Python code 寫好之後 type 所有變數。type casting 用例如 ` i`\n", "* 函數可以宣告成 def,cdef 或 cpdef\n", "* [不好 debug](https://stackoverflow.com/questions/32888501/debug-cython-code-pyx-when-using-the-python-debugger-pdb-best-practice)\n", "* 實測不能 decorate cpdef 函數(為什麼?)\n", "* 實測非 level one function(例如函數裡的函數)不能 cpdef" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "%load_ext cython" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "# Python version\n", "def pyfac_loop(n):\n", " r = 1.0\n", " for i in range(1, n+1):\n", " r *= i\n", " return r" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", " \n", " Cython: _cython_magic_f1b0bfaa9dd99dd25796948e61b32169.pyx\n", " \n", "\n", "\n", "

Generated by Cython 0.29.24

\n", "

\n", " Yellow lines hint at Python interaction.
\n", " Click on a line that starts with a \"+\" to see the C code that Cython generated for it.\n", "

\n", "
+1: cpdef double cyfac_loop(int n):
\n", "
static PyObject *__pyx_pw_46_cython_magic_f1b0bfaa9dd99dd25796948e61b32169_1cyfac_loop(PyObject *__pyx_self, PyObject *__pyx_arg_n); /*proto*/\n",
       "static double __pyx_f_46_cython_magic_f1b0bfaa9dd99dd25796948e61b32169_cyfac_loop(int __pyx_v_n, CYTHON_UNUSED int __pyx_skip_dispatch) {\n",
       "  double __pyx_v_r;\n",
       "  int __pyx_v_i;\n",
       "  double __pyx_r;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"cyfac_loop\", 0);\n",
       "/* … */\n",
       "  /* function exit code */\n",
       "  __pyx_L0:;\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "/* Python wrapper */\n",
       "static PyObject *__pyx_pw_46_cython_magic_f1b0bfaa9dd99dd25796948e61b32169_1cyfac_loop(PyObject *__pyx_self, PyObject *__pyx_arg_n); /*proto*/\n",
       "static PyObject *__pyx_pw_46_cython_magic_f1b0bfaa9dd99dd25796948e61b32169_1cyfac_loop(PyObject *__pyx_self, PyObject *__pyx_arg_n) {\n",
       "  int __pyx_v_n;\n",
       "  PyObject *__pyx_r = 0;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"cyfac_loop (wrapper)\", 0);\n",
       "  assert(__pyx_arg_n); {\n",
       "    __pyx_v_n = __Pyx_PyInt_As_int(__pyx_arg_n); if (unlikely((__pyx_v_n == (int)-1) && PyErr_Occurred())) __PYX_ERR(0, 1, __pyx_L3_error)\n",
       "  }\n",
       "  goto __pyx_L4_argument_unpacking_done;\n",
       "  __pyx_L3_error:;\n",
       "  __Pyx_AddTraceback(\"_cython_magic_f1b0bfaa9dd99dd25796948e61b32169.cyfac_loop\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return NULL;\n",
       "  __pyx_L4_argument_unpacking_done:;\n",
       "  __pyx_r = __pyx_pf_46_cython_magic_f1b0bfaa9dd99dd25796948e61b32169_cyfac_loop(__pyx_self, ((int)__pyx_v_n));\n",
       "  int __pyx_lineno = 0;\n",
       "  const char *__pyx_filename = NULL;\n",
       "  int __pyx_clineno = 0;\n",
       "\n",
       "  /* function exit code */\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "\n",
       "static PyObject *__pyx_pf_46_cython_magic_f1b0bfaa9dd99dd25796948e61b32169_cyfac_loop(CYTHON_UNUSED PyObject *__pyx_self, int __pyx_v_n) {\n",
       "  PyObject *__pyx_r = NULL;\n",
       "  __Pyx_RefNannyDeclarations\n",
       "  __Pyx_RefNannySetupContext(\"cyfac_loop\", 0);\n",
       "  __Pyx_XDECREF(__pyx_r);\n",
       "  __pyx_t_1 = PyFloat_FromDouble(__pyx_f_46_cython_magic_f1b0bfaa9dd99dd25796948e61b32169_cyfac_loop(__pyx_v_n, 0)); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 1, __pyx_L1_error)\n",
       "  __Pyx_GOTREF(__pyx_t_1);\n",
       "  __pyx_r = __pyx_t_1;\n",
       "  __pyx_t_1 = 0;\n",
       "  goto __pyx_L0;\n",
       "\n",
       "  /* function exit code */\n",
       "  __pyx_L1_error:;\n",
       "  __Pyx_XDECREF(__pyx_t_1);\n",
       "  __Pyx_AddTraceback(\"_cython_magic_f1b0bfaa9dd99dd25796948e61b32169.cyfac_loop\", __pyx_clineno, __pyx_lineno, __pyx_filename);\n",
       "  __pyx_r = NULL;\n",
       "  __pyx_L0:;\n",
       "  __Pyx_XGIVEREF(__pyx_r);\n",
       "  __Pyx_RefNannyFinishContext();\n",
       "  return __pyx_r;\n",
       "}\n",
       "
+2:     cdef double r = 1.0
\n", "
  __pyx_v_r = 1.0;\n",
       "
 3:     cdef int i
\n", "
+4:     for i in range(1, n+1):
\n", "
  __pyx_t_1 = (__pyx_v_n + 1);\n",
       "  __pyx_t_2 = __pyx_t_1;\n",
       "  for (__pyx_t_3 = 1; __pyx_t_3 < __pyx_t_2; __pyx_t_3+=1) {\n",
       "    __pyx_v_i = __pyx_t_3;\n",
       "
+5:         r *= <double>i
\n", "
    __pyx_v_r = (__pyx_v_r * ((double)__pyx_v_i));\n",
       "  }\n",
       "
+6:     return r
\n", "
  __pyx_r = __pyx_v_r;\n",
       "  goto __pyx_L0;\n",
       "
" ], "text/plain": [ "" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "%%cython -a\n", "cpdef double cyfac_loop(int n):\n", " cdef double r = 1.0\n", " cdef int i\n", " for i in range(1, n+1):\n", " r *= i\n", " return r" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.37 µs ± 26.6 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)\n", "74.3 ns ± 1.12 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)\n" ] } ], "source": [ "%timeit pyfac_loop(20)\n", "%timeit cyfac_loop(20)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Units of Measure Less Than a Second \n", "\n", "| Multiple of a second | Unit | Symbol |\n", "|----------------------|---------------|--------|\n", "| $10^{-9}$ | 1 nanosecond | 1 ns |\n", "| $10^{-6}$ | 1 microsecond | 1 µs |\n", "| $10^{-3}$ | 1 millisecond | 1 ms |" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Integral Types" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0 1 -3 1 True 1.0 -2.0 100000.0 abc\n" ] } ], "source": [ "%%cython\n", "\n", "# cdef is an directive , telling objects are c objects\n", "cdef:\n", " int i = 0\n", " unsigned long j = 1\n", " signed short k = -3\n", " bint flag = True\n", " long long ll = 1LL\n", " float a = 1.0\n", " double b = -2.0\n", " long double c= 1e5\n", " str s = \"abc\"\n", " \n", " \n", "print(i, j, k, ll, flag, a, b, c, s) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### cimport" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "In file included from /srv/conda/envs/notebook/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1969,\n", " from /srv/conda/envs/notebook/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n", " from /srv/conda/envs/notebook/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n", " from /home/jovyan/.cache/ipython/cython/_cython_magic_2014508b603b08191838a4a9c4c94518.c:648:\n", "/srv/conda/envs/notebook/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n", " 17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n", " | ^~~~~~~\n" ] } ], "source": [ "%%cython \n", "\n", "import datetime \n", "cimport cpython.datetime # 用這個取代上面那行\n", "\n", "import array\n", "cimport cpython.array\n", "\n", "import numpy as np # gives access to python functions\n", "cimport numpy as np # gives you access to Numpy C API ---> 有 warning?不能用了?\n", "\n", "from libc.math cimport exp # 用 c 函數會比 numpy 版本快很多\n", "from libc.stdlib cimport rand \n", "\n", "cdef extern from \"limits.h\": \n", " int RAND_MAX" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ctypes\n", "\n", "* 在 Python 自帶的 standard library 裡,不需另外安裝\n", "* 用正常的 c code 就好,signature 不需要用 PyObject\n", "* 編譯成 shared object(*.so)再手動在 python 端指定 input/output type\n", "\n", "```c\n", "// lib.c\n", "\n", "double square(double x){\n", " return x*x;\n", "}\n", "double cube(double x){\n", " return x*x*x;\n", "}\n", "```" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "!gcc -fPIC -shared -o lib.so lib.c" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(25.0, 125.0)" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import ctypes\n", "\n", "lib = ctypes.CDLL('./lib.so')\n", "\n", "lib.square.argtypes = [ctypes.c_double]\n", "lib.square.restype = ctypes.c_double\n", "\n", "lib.cube.argtypes = [ctypes.c_double]\n", "lib.cube.restype = ctypes.c_double\n", "\n", "lib.square(5), lib.cube(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Entry Points\n", "\n", "* 用 python 寫像 pytest 一樣的 cli command [還沒看](https://amir.rachum.com/blog/2017/07/28/python-entry-points/)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [Always Use](http://jaredgrubb.blogspot.com/2009/04/python-is-none-vs-none.html) `is None` instead of `==None`\n", "\n", "* `==` 比的是值,`is` 比位址(`None` 是 singleton)\n", " * 所以 `is None` 比 `==None` 快一點點\n", "* `==` 可能被 overload(`__eq__`),使得 `==None` 出現不可預期的結果" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Import in a `with` Statement\n", "\n", "出了 `with` 仍然有用" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 0 1 2 3 4\n", "0 0 1 2 3 4\n", "1 5 6 7 8 9\n", " 0 1 2 3 4\n", "0 0 1 2 3 4\n", "1 5 6 7 8 9\n" ] } ], "source": [ "import numpy as np\n", "\n", "class context:\n", " def __enter__(self):\n", " pass\n", " \n", " def __exit__(self, exc_type=None, exc_value=None, traceback=None):\n", " pass\n", " \n", "with context():\n", " from pandas import DataFrame\n", " print(DataFrame(np.arange(10).reshape(2, 5)))\n", "\n", "print(DataFrame(np.arange(10).reshape(2, 5))) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [Bytes and Str](https://betterprogramming.pub/strings-unicode-and-bytes-in-python-3-everything-you-always-wanted-to-know-27dc02ff2686)\n", "\n", "* Unicode 是 ASCII 的 superset,把字元映到數字(或[碼位,code points](https://zh.wikipedia.org/zh-tw/%E7%A0%81%E4%BD%8D))\n", " * 例如 ASCII 表有 128 個碼位,從 16 進位的 00 到 7F\n", " * Python 3 開始 `str` 是 Unicode string\n", "* 如果所有 code point 都統一用一樣大的空間來存,會很浪費空間。最原始的 ASCII 就只需要 7 個 bits 就存的下了\n", "* UTF-8 是把 code points 存起來的 standard\n", " * by far the most popular,世界流量排名前 1000 的網頁中有 97% 是用 UTF-8\n", " * Python 3 default for `str.encode()` and `bytes.decode()`\n", "* 所有的 string 都要 specify encoding 不然就沒辦法讀\n", "* Python 3 `bytes` is a binary serialization format represented by a sequence of 8-bits integers that is fit for storing data on the file system or sending it across the Internet" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [Making Command Line Commands Using Python](https://dbader.org/blog/how-to-make-command-line-commands-with-python)\n", "\n", "* This app can git clone multiple repos with token: \n", " ```\n", " gc repo_name_1 repo_name_2\n", " ```\n", "* Need pycrypto to run, which requires gcc: `apt-get install gcc`, `pip install pycrypto`\n", "* Step by step: \n", " 1. create a new file named `gc`\n", " 1. copy and paste below into `gc`\n", " 1. `chmod +x gc`\n", " 1. make sure the path of `gc` is in `$PATH`: `export PATH=$HOME/binder:$PATH'` if `gc` is in `$HOME/binder`\n", "* Implementation details \n", " * ```#!/usr/bin/env``` tells the shell this script should be run by python\n", " * no matter where python is installed, ```#!/usr/bin/env``` will lead the shell to the right location\n", " * Both the key and the initial vector of ```AES.new``` need to be 16 bytes\n", " * Both encrypt and decrypt output binary which needs to be decoded to string\n", " * When calling ```gc repo1 repo2```, ```sys.argv``` will be ```['gc', 'repo1', 'repo2']```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#!/usr/bin/env python\n", "from Crypto.Cipher import AES\n", "from getpass import getpass\n", "import subprocess, sys\n", "\n", "password = getpass()\n", "# o1 = AES.new(password.ljust(16), AES.MODE_CFB, '*'*16)\n", "# encrypted = o1.encrypt(LONG_AND_HARD_TO_REMEMBER_TOKEN)\n", "\n", "encrypted = b'!>k\\x98%6\\x9e,j\\x88\\xd8\\x13\\xa85Z#\\xdb\\xa5Q\\xb2\\xfc^\\x15\\xd6\\xe6mH=\\xb9\\xe4~\\x88\\xea\\x8f\\xe2M\\xc1\\xf6\\xec\\xcd'\n", "aes = AES.new(password.ljust(16), AES.MODE_CFB, '*'*16) # key and initial vector both need to be 16 byptes\n", "token = aes.decrypt(encrypted).decode()\n", "\n", "for repo in sys.argv[1:]:\n", " subprocess.run(['git', 'clone', f'https://{token}@github.com/beginnerSC/{repo}'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [Bare Asterisk (*)](https://stackoverflow.com/questions/14301967/bare-asterisk-in-function-arguments) and [Bare Forward Slash (/)](https://stackoverflow.com/questions/56514297/bare-forward-slash-in-python-function-definition) in Function Arguments\n", "\n", "* ```def foo(a, b, *, c, d):``` 強制呼叫函數時傳入 ```c``` 和 ```d``` 一定要寫 ```c=``` 和 ```d=```(named arguments)\n", "* ```def foo(a, b, /, c, d):``` 強制呼叫函數時傳入 ```a``` 和 ```b``` 一定不能寫 ```a=``` 和 ```b=```(positional arguments),只能照順序把參數傳進去\n", " * python 3.8 以後才有,所以這個 Jupyter 環境目前沒有:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3.7.8 | packaged by conda-forge | (default, Nov 27 2020, 19:24:58) \n", "[GCC 9.3.0]\n" ] } ], "source": [ "import sys\n", "print(sys.version)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Immutability and Hashing\n", "\n", "* [SO discussion](https://stackoverflow.com/questions/2671376/hashable-immutable)\n", " * immutable object 初始化之後就不能改變了,mutable 的可以\n", " * mutable object 例如 list 預設不能 hash。可以自己寫 ```__hash__```,如果 list 的內容被改變了 hash 也要跟著變\n", "* primitive types 之中 mutable 的只有 ```dict```,```list``` 跟 ```set```。下面的表來自這個 [medium post](https://medium.com/@meghamohan/mutable-and-immutable-side-of-python-c2145cf72747)" ] }, { "attachments": { "907113d4-c125-46ec-8326-790717f0208c.png": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAApIAAAE0CAYAAACfJdMIAAAgAElEQVR4nOy9308bV/64P3/A3HDJBVIkyxIXkarI4iJR9Ra+IGqFBNWuEEoaWU7UCKJuBOkq0FSBVA1k1UzU1mhb0jZWCko3dBuLfYf2HdqNkw1ZQbZ4G0cBZd027EITd4EA4ePyNcEwz/fCPxgbG2ywgcDrkebGHs8cnzPzOs+cX6MgCIIgCIIgCKtA2egECIIgCIIgCM8nIpKCIAiCIAjCqhCRFARBEARBEFZFTCQVRZFNNtlkk0022WSTTbYVt6QiKWwvpMwFQRC2LhLjhVwhIikAUuaCIAhbGYnxQq4QkRQAKXNBEIStjMR4IVfkViRD0wx7vsbZVIutrJSyAzZsh6uorn2bVlcvvvEgOtN4HWdw+UPAFN62GorVxX53k+YhlJ3UCMuwmjKfH+rmvZM2itTE8RJmiisOYKuwYimuoKpOw9l1h4dTczlI+XqhE/S5qC/dian0JC5fIMfnC+BznaTUtJPSehe+oJ7j8wmCsJVZVb0+5aG96Q0qLPkJMV7FZD1Mw9k/4Zmaz35itxgL/lu0VFtRY/lXjOZZqQ7Jpg/phKZ8uNveo6nxOIdLLaiqhdLDjZz/ystYaG31S45EcoHgsBuHfQ+qUoC1/goDk1GJCP+h6x8doSjPQulvizEpVRGRBJigp2G3iOQ6s/oyn2ei+zj5sRukgDLng0iZ6YSmHnC14eXwDWQqp/5S/5ov2o3hv3TXvBC7LvNqupnIxmH1KbwtNUuDykQ3NXnRPH2Bmu7/ZuNsgiBsU1Yf43VCw19gzzOI5O536ZveLgI5T8D7IZVrdZFfe2goyEQkIVs+pE/eptlauFg3678y3Pl7CiMPBeaDHTycW329nAOR1Jl72MFBs4qiKKhlHzOQtDVlFv/101hVBSVOJAN4tGIRyXVmLWUe8miYYiJpwuYaSdhhCFe1JSaa1sZv8T93MjlFX1P0ulTZ2dTLzJqPGWLsVjNWNUlQmemlaacaCzpNfVNrPpsgCNuXNdXrIQ+aySCSNhf+7CVtU6OP3eSUtWDtLhKXh+mKZDZ86BnDHYdQFQW17EO8gcgDwGw/2gvROuZFGnqeZHzkKFkXSX36DlpJQSRxu6jt/oXUyhDgX04beSKSG05ORRKducHzlMT2sVDtGnruylUP+PjG2YLD+S2+wFqfxucJDHyG3aymCCrzBHzf4nS04PzGR+B5825BEDYVIpKZowfu4bTvyo6LbJhI/orX8VLkGLtp6In2pY3gspmy4lpZFskZfM79i+MA8k/gnl5Y/ifBe7SWHRWR3GByK5KA/iPt5QWLgajwbXpWuja2LEaJzCSoCIIgrA4RycwwSuTzLZI6cw9dHLMWYq78kP7JSCOI/m86KndEjp3PXucDVlsjZ1ckn93FsSdv8WKr7GBkxZaUeSY9Nw0DdpfLuHkCQzdpa3qD6qrXsB0opaioFFutxuWeh0tbbUKjeF3nqC6xUnrgNapqTqA5L3Gh7h06o+Kazj7bgJyLJDMMtpYbBmtbqHOPGw4yirezhbrKMsoO7KOi8ijNHZ4k4ylnGfN2olWXUlz6KoeraqjXLnD5QgP1ncbzRgcXn+FoZQWVFVYsxXYanDcZirQmLh0AXYXLP0vA9xVa9V5MahGVmhv/7CN6Wl5PMuh5gcDgFRoqXjD8rypcw8P0OU+GB6ibrNgaPuP2yK+LLfOT16mLSaRxC+ddegOz55gc+IqW+kNUVLzKgYoK9tW+h8s7arhfnjJ45W0qjOeyufAHh7ltTF/jF3gnn+eJUIIgrETORHIhRXwMPKBLq6HMko+iFlFxso0+/yzh3pZIjFXysZTV0uL+N9ERcMnj39Pwb2pewaIqqJZ9nHT2hodI6U/xdb1HtdWMolooq/kQ9/BivF0Y+Za3E2O0PwQLfvouNiyNjwCM4q7bnSRGJ/x3fZqhG2001R6h6rCdA6V7KCq1UatdpmdoOr43NlEke3/E0/EHqqxmFMVMse1tLt15RPxIwBVEMu16MwnTburyI+lR9+P0rX6wVlZFcsHnZK8hw/Pr3ExnfJRUGTfPdP/7lKh57G7uZVqPfNb3LrsVBUUppLzlu8jngD5Bv1aOmmfH+a8AOqAH/4O7+RXU6IWUzj7bhNyLZAi/q8pwQ6rscniZBwj9zPXGl1GVQuwdPxGKlWsB1ubbTMbuieg1YGafc4AZHdB/ZcT9LqWq8bw6If+3NFoLUAqP0/V4FvQx3PVFLBlYHDcA+jB/unWJI6XFWFRjEAuw3KBnfaid8tj/epmjtUd58/JNvrv1OfXWSCus+QgdDw036kpPp8sNzNafMtB+lCJVpbC2i8chHULDdNXuRlF2YXfeMzxUzTLU/upivr+q4TxVTU1TM3Wx4KpiPnIVv3SfC8KWJbctkvHxccfxc7Qc+R0NZ88Y4oxC3r7P+P77No5Vv8XZ01WL8qm+gsNrsIW4+Leb4x+c5Uj1Sc6ePW4QPzP72vr4/uKbVJ88w+mqRflUS1rwziwGNH2kg8pEkQSWxMe4/7WCxEX9QdlLc99EWBr1Cfqa94Z/Y9pHi2diUSbj8nAH/1NxEqf7Dv3GekIppr57xHCeZdKQdr2ZjDkeu46Qpygo6ss0Xv95Ta2tWRTJBabdJwyzd1fbDJsq4x7TVVW49CIxFo5aHbtA9NEuqvMUlIJT9PxqyNG5QZzl4a70dPbZLuReJBP3U1CruhhjDn/XMcyKgqIepWssOhC4j6YdCopSisMbkSj9EV3VO4kf5wHhIRWHsEfPG7xHa9kOFCWPPY67PAPir6sKnL5gNFGGm7uAwuovGQ7N8LDjSDhNeUdwPZ5j2Rva78IW+1+7qe0ajj38GGe05x108Th6ma0kkim/15nxtlCiJrbqGu4/tZTm3vFIAEsQ+KKTdPtnw0eKXv+KgqK8SvvQbLpFLgjCc0ZuRTI+Pubtu8RPkYf1+DhjnGwZwOsojf2moKGHX5Oez8y+dh/hPpMQo101YQFKlKC4HtGEOiIuRhtFMiE+ZiKSY11UqeH/VOL4Z2zypbGeU+2uxQf0uP9knD+SMIfAXEf32EpD/TKoN5Mx8x3a7jzUoiO09j5e8/DBLIpkYotTlkVSH8ZlN0c+P0jHSKQrLkWFu1iYBVgbrzEcjPb+hxjv+yueqfm09tkubIRIKjYX/oUHOPdG1igzaXhi93e0XFVe0PqZjftMQbW+Q7eh+0If/46vPE+IDzRG0TI8gRWexD0xn3CehP31ACP3/ol3aDJyDacrkvEt2XFPwoYHnVWLpD6M62Bh5PNyWgcXWznnB1t5cYm0LhMoVzVmRxCE55H1FMn4lrNUcWZ1sSmuHjHWGXGTRxLqoRyIpO53YY+2qBqG8aVM33Lx1lgPKmbsruFI3ZYiDZnUm0tYYNr9Fi/YnVkb0pRFkZxnovv3i08KWW+RnCcw+L+0aO9x/qsHka47ndDTGzTsWFo4+nAH+wxjNtSiQ2guD/7g4nDSdPbZLmyESObXuXlqFC3jDaEbBgKXtzOkA/oQHfsWZ5kp6h7smguPf8YwFsW43mPCzRqaZmRwgCHjwuhpy9TqRHLxCTHhKXm1Ihl7Ck5yLmM68n5P98Q8IpKCIICIZNZbJPWnDHZ+hKZ9yle+p7EeoKfuRnZkKpKM466zxM612DqbPA16JvXmEgJ4tJrFXrkskEWRjG8RUZTVLtqcxiyl0CQPe1201B2k8vABXs5LUjj6OL3NpYYBuxFZLD5Ox2Ck0NPZZ5uQe5FMHPqQx4ut95mNCwrFVNhs2Gw2bLbfUmxSE26UeSZ7z0bWHjVsagm1HffDDxfzXhy7MpgNnWuRjDu+IW9WKZLxeb2MSMZ+IyIpCIKIZNZFMoZOaOonel0t1Nle5bBt72KDWtoiGX+uxXQkT0Moo3ozkQAerSGrQ/eyKpLxiygrKHud+DJu3Fum8PRphtwfUVWUj2I5Srv3CaHlCif0mN7WI0tf4Wc+Rpd/Lv19tgG5F8mEQc2RRbbjfps4VjUps/h7Pw5fA3EPABaOdI2gx10PabwRZl1FspCqrsfpnVdEUhCELCIimX2R1AM/4f7oCEVqHpbqdryTc6vr2k44V3j+QOo0ZF5v5pbsiiRP8WgvLRaKcUzYcsyHWJytnqrwAvjaq8KDS40DVVesDBcI+j24tEMGWUxcMymdfbY2uRfJ+KZ7tdyJb05n3utgV+y3hrGvK6AHH+FxadiNQrnXiW/euDbWcuNEYolfx67tl3B4f03vvKm+j3t14jIiGRuALSIpCIKIZNZFMjhIe3SdyfzjkaFEqxwjGXeucG/d/DJpWG29mSuyLJKgT/6NxqLozKkdlDsHWfYv6k8ZcL7JKffosoNL42d+Va04aSHkaaXBuE4hc0x6nbFFoE2ah2Aa+2yXedu5Fkl9opva6JpVipXGnrFweRvXslJ2Ut31KGFIgU4oOBsuh5CH9xqMS0rphCb/ubhorEnDE0pYr3LnGfpmlnlay7FIxo1lMS7Qn5XJNvEzrY3Lb8lkG0EQjIhIZlMkE2aPG363OpE0pv0lNM/T5dOQSb0Z9/FTBi79HqupEGvt5wys+Q1tYbIukkve2mE+jHMg+XhDPfADXQ2vUBL37uU0mnJj6zbNE7j/KZVJxkiGPBqFxuVWAHhKX9OLRLs809lnu5BTkdSn8LZURMaiJq5zGN+KrVpPc90flaMFgsPdnDn1vwyH9PCNWBhdjid2cGb7mtmhLI7J1f1XORJbayzx3d7zBAZcXLw9mmaLdpR0RdKwpphxpriSx27tu8X3c8edNzIJR5+g749teGb0ZdJlHCdqXOIizeV/RCQFYVuyrUUybl1Kw/JvzOBz7ktLJMMTYOaZ7rvIx55H8WMad79L3/Q86E+5f/5A5mMkY+lLXNM3Vb5mUG8aiF/rO3u9rjkQSYB5Ag+70Sojs2fVEmpar9LnG8bv9zPi83D98rvYd79M7WUvk3F/Nn5h09jspelbNBQaxl+aSqk6+iqVDWc5aY12bxZgPa7RfvuXyMW2C7vzn7Hj6wEPLWU7YutQprPPdmH1ZR6/VqKi5FPSej/WCq0HH9F3/jXMioJa9BofdP+45A1Eia+iUkx7qWo4Q9PxfVgPfrb41BTyoJlUzMZlC6KSGreg7Sz+66cNk3JUTKVHadLO0nT8EPYz1xfFMi7ALPdu+BTXJSSI5AtYbW9xofvv9F1zYIsIrWo9S++k4ekvtiZmJBAUV/FOXRXHou8gT1iQN25NNP0pA87DmJVVLEhunMkXd44Xaep7iiAIW5PVx3id0PAX2GMNNgrK7nPhB94Yy8THVCtXMMdIx8HFYxrfhJcy/i02HCiKEj8+UDcOa9pBZce/DbF8gr6mklh9YK48TduVNhzHjlB14H8M/+sMPRPR2itxzUorVe8cx36sk+HQPNM9b1MYi/sqptLXOFppo+GDeqyx37zEce1zbo+FYiKpFu3j8G8tiy9WMchnvBAun69p15sGnjORjBCa5GFfF86mWmyllkiLlJniyhqanF8nLNsCMIW3rSbuVUuKWkLN5fsECDHpvUxDRRGqYqbY1ojT7WMqNMdYz/tUWvLjXpsU8nxEdWsXf+1w8ObxWo7a91Na/BK2xs/xjIULKp19tgurKfP5oW7eO2lbOlFJtVB6wIbtQBnF1gqq6jScX3kYDqQeKKAHh7ndfoaayuLIa7N+R1Pb4usMAQh5aKn+kGt/7cDx5u+pOXqIA6VWrLa3ueT5b0Iz/hxTvm9orbeFZ7GpFsqOnqHtxk8xyVr6Ki4FtcjO6Yu9+I13V5JXgC1elyzpNrnie0C343WsJhXV8go12lUGlqzXpRPy9+I8uQ+Lmo+lrAatK7ysVdJ0Ff+ey4NG0Yu8KrL2QGavSFReoPIPN/GHhrn2RonhHCrmire5MigyKQhbkVXV61Me2hpeD7/m0BjjFRWT9TANZ/+EZ2I4dXwM3KejrtzQY6ViKn2b7pEA/p6PqC4uMPzGSnXLLX7+OVX8myQw+AV1pYWGdBRSeuL/GAktjdFq8eu09DyKiZJueGWjainjcIOTG0MT4VZRUzEVVXU0ae/R2u7GF4gOQXoce91tOJZ/hS9aJ+lP8H7eaHgVrhO3b4KQ/gs92oFwXK84ibMvsuB36HvO1zrpH5sNH7f9XeqqXwu/VjFZfbesD0WSkE69aUR/yuAXb1G6+bu2hecRKfM1sNxkG0EQhE2AxPhk6MxOTRBI593UQkpEJAVAynxNiEgKgrDJkRgv5AoRSQGQMl8TIpKCIGxyJMYLuUJEUgCkzNdC3BI/cUs3CIIgbA4kxgu5QkRSAKTMV4dO0N/PFe0N7LFXVNmw1/2Rq7d/YkqG3QiCsEmQGC/kChFJAZAyFwRB2MpIjBdyhYikAEiZC4IgbGUkxgu5QkRSAKTMBUEQtjIS44VcISIpAFLmgiAIWxmJ8UKuSCmSsskmm2yyySabbLLJttKWVCSF7YWUuSAIwtZFYryQK0QkBUDKXBAEYSsjMV7IFSKSAiBlLgiCsJWRGC/kiuyK5Mw/cZQUoCj5FDXcZHKtqcsFgTto1gIURcV85Cp+WTQa2PpBRg9O8MvUs41OxiZhgeCTUaZCcvELwnZhq8f4dUGfZujGn7jY8xiJnotkVySN7xze5cA7v+b0ZR9jGk0aHnktMrCaMteZG7lDZ8cnnLbtQY3kqWop5YDNhs1WQWlpBbaqejRnJz2+CdY3q+cJjHyP2+VEq7NRbCqizj2+rinYVOgBRjxuXE6NOpsVU/4J3NMLG50qQRDWCRHJtaEHHuCqL8da28FgYDPKzcaRO5HcrJL2PKRxA1hLkNEfuziYp6Ao+ex1PiCmJ6FphvsvU28tQFF2YXfeI7Buj3E6ocAT/I/v0Fq+A0WxbG+RJERg/Bce97dSriooIpKCsK0QkVwtOiH/dZrLrNjP32FMenKWICK5GdO4AawpyEy7qctPIpIA6AQHPqZMVVCUPdS7R9e5S2Acd51FRDJKtKxEJAVhWyEiuRp0Qv5vaSwpp971YB0bQp4vRCQ3Yxo3gNyJJPDsLo49eSiKys6mXmbWktCMEZGMQ0RSELYlIpKZEpFI6x7s7YMENzo5mxgRyc2Yxg0gpyK58ADn3nwRyc2AiKQgbEtEJDNDn7xNs7WQEu0O09ISuSw5FMmz9Prv0tlygsOlFlTFTHHlMTTX3RRjDBYI+j10tpyg6kApFjUycaOqHq39Br6puRQnzfB3IpJJyaVILo6hLKK225+ka3uOKd8N2rW3qGs4Q1NdNfaat2nt9OAPJpOdTPZfRiRDo3g7W6ivslFRvJti25u0dCa/PvXAT9xoO0vDqT/SfvkTmmp+R33L1/hig651QoGfGbj9v5w/eYD9zgfMB36k2/E6VpOKatlHwxf3V+gamScwdB2HbVf4+lRfov5yH8PRc4RG8Xadw2bOo7DqU77zzxp+d5O25kZOfdTG5QvN1FSfoOWrJF0xS0RynsDIP7nmOIhZSfz8e9xXNGxmdc35JwjCxiIimQGhIVzVRZgPdvBwTuLZSuROJNUdmEy7qTh+Gq3pDSos+ZFX6RRgPXWTsbiymcXvfpcykxqZ+VvJ8aaznG04jDX6WdFR2geeJkjIKn4nIpmU3IjkAkF/L+ftu8Ll3vgt/iWSMYv/+mmsO39P5/CvkXKaJzDwGXZzHmb7ZwzEzZDLdP8UIjn3I1der0LrHQ3PJten8LZUoCoFWJtvM2lIph64h9O+i7zqLkYjn+sT16kvzFtcQmruFwZvu2gq3YGi5GM9eQ7tzfe5cusf9N/6PDLhKJVIG9EJDbuoNqso6iE6hhOWLHp2F8eLB3D6ou260f/+AtVdj2L5MeE+SaFi4UjXSPz5UrVIpmypXHv+CYKw8YhIpkuAgdZKVHMNrmHp0E6H3IlkwRt0PpqJVWJ6wMv5SnNEJktxeAOxn+n+qxwxh8Uvr/JT7htbefzXqC/KC0thSQveGX1tvxORTEp2RFLFVPxbbDYbNtsBKorNkc/sNCZtjYtOxDGxr2MoQbDm8Hcdwxy33mem+0NyEZrB53yVnfXXmTAK42gX1XkKirrfIGo6M31n2KkocSIZ667Pq6FrNHoRBfE5K1AUlcLaLh7HpDnFMVKhj+Ku34Oi7KDcOchim7rOnO8ir75pTPcUfU3FKMpOg0jCgs/J3mTny4pIZpJ/giBsBkQk0yFax5jZ1+4jVT+oEM86jpEM4XdVR9YbVNnl8BLWvmcMdxyKfP4CNd3/TTjoDIOt5REBLaapbyry+Sp/JyKZlGy1SJY4/s5jvx+//xFDg3e4fvld7EX5KOoe7NpXhq5ggCf0NLy45MEiSrjVTzWISab7Q1IRmumlaad5qYzGxnImiGrIT3/nFb71GVq2Y/tW4PRFn1qjIrm0iz8qdspeJ74VhybqzPmclKsK6t7zDDyLnvUpHu01wz0Q3jfk76fzC3dc3qY8XzZEMtP8EwRhwxGRTIO5QZzlO5Y0WgnLs66TbUIeDVN04eqqLsaAxYoqLHyaJ1EQQvhdVZHvjRX0Kn8nIpmUnI6RDHhoKduBoqjxY05is7mNMmb84RAd+0yLYpLp/sBSEdJ55m1hj1JAcdUpNE1Lur3X5SPZe3D04CiDt/7M+dZ3OVqUmJZsiSSgD+M6WBj/EDTTS3PFxwaxTGSB4PgDbnV8Sqv2OkU5Ecm15Z8gCBuDiKQBfZqhG04am79mONZzFO3Viu/dEVZmw0RSsbnwAzCCy2ZKUwgVTJon8oaUVf5ORDIpOZ21HRuzl9A6HPtd8hbGRYmJHDfT/eM+i4rQAtPuE+RnOItbDz7C43Jw6txVBibnctwiCYt5plJYf50JfZ4J9zu8GtfVHTs6Qb8H17kmzl29z2RIz2GL5OryTxCEjUVEMsriUCPFOI587j6tJfkou8/hkdbIjNgwkcyr6WYCgP/SXfNCmkKYx4ut9yNd4qv8nYhkUnIrkgaRMu4Ta2FM9QSYIDGZ7p/0s6gI5bHHcTetVrPwZBtLROiifyjXIgnMfIe2Ow8l7wiuRz/ieu33uB4namR0ss2LcYu9514k088/QRA2HhFJA8F7tJbtCHtA4UncE3NM97xNoZJPSet9GRuZIRs0RtJY0UYr3/CM7vL2HxMEYYKeht2R742CsMrfiUgmJbciGe0OVRIkMDpRJMX6ktGuanMd3WOhVewPqbtmowEkyTtT9acMfPkV92Z04FcGWn+LmtgCtx4iyQw+535UZQdlx49hfzN+cgsAz+7RurdgiQDmTiQzzT9BEDYDIpJGdGa8LZSo4YamogYnF6p2ph42JSxL7kQy/yiuR9F17nRCk3dwRJ8AEmbFRicWKIqCan2H7tiyLnNMep3YIzOz4yuuVf5ORDIpORVJfYK+5r3hPI+TPJ25hx0cNKso5mN0+eOfA8PrT+6grPVe5K0Cme4PSVspo/KlFGBtvMawce3J0BO8l0/T2DkUWdYmOu6ykIOu4cWHlaTjNbMtksY1OIsTJtlEvh/uYJ+qhFstY62VBtlLVyRnemnamWTJocjg81XnnyAImwIRyQSM9VJ0e7GVwSTPxsLy5HYdyV0vUdXwh7h1HRVlF3bnvYSlYIIMX62jSI0WqJniigMcKLVEWjAVFNM+WjwTCa2Oq/idiGRS1hJkYsu+LOnuXCA4Psj1j46Ey0h9mcbrPycIRnQt0B3x60yGfuZ64yuULFl7MtP9k3V3R7uD49cf1c6epMpanHB9RlskFdTiY1zovk1Pl5PmZo3TBwpRlN3UtN/g1tf/4OeFVCK5jNitnLm46/ckzN42YJC64ppP6O79G13OszR/cIoDeQpKYS3tf7/J13cehdOTSiT1cXqbS1EVFbPtHFd7+ujp+pjGxgtcPluOoqiYbWe5/M0gU3om+ScIwmZARHIpsZU+IqvJvKD1M7vyz4QEsiuSc//B/fF74Zmbl+4w/K+vaam3UWxSUS1lHK5vodPziGDSSmaOKd8tOhyLb6hRTMVU2Gtpcn6Nxz+TYhZVhr8L3EGzFoQrxkNXGJa3xAGrKXOduZE7dF75jLP2PYviHpF5m20/pZaC8LqSVfVoy5bhAsGR27Q31WK319Bw9jT1dX+g7cZPKYQknf3Db2a5frmJSrMaEaFzXO25x0hgnujbYJwNdopNKopqoazmPVxJrk899oaawvDbW7ruMxmaY6znfSotJiyVzXTe/4EB92fUWQsiLeT1tF3/npHADOODbtrqXgrnkfoSdW1uBsfTDVc6M14nb33xQ4pxO/MEHn6Do3ovJpMVW/0f6Rp4Qkj/hR7tAJa8Iiqb/oLv/z1lxPNXLjftD7/BRtmFTeukxzsSy7Pw/6ylzGLCUnqYBudtRoIz+NrrqXV04PaOEIhJevr5JwjCxiMimYwQo9314Ymgahla/+RGJ+i5JLsiKTy3SJkLgiBsXSTGpyD0iJ6LTjq9ozIcZ5WISAqAlLkgCMJWRmK8kCtEJAVAylwQBGErIzFeyBUikgIgZS4IgrCVkRgv5AoRSQGQMhcEQdjKSIwXcoWIpABImQuCIGxlJMYLuUJEUgCkzAVBELYyEuOFXCEiKQBS5oIgCFsZifFCrhCRFAApc0EQhK2MxHghV6QUSdlkk0022WSTTTbZZFtpSyqSwvZCylwQBGHrIjFeyBUikgIgZS4IgrCVkRgv5AoRSQGQMhcEQdjKSIwXcoWIpABImQuCIGxlJMYLuSKrIrkwdJn9JhVFUVBLWvDO6GtPobAuSJDZCuiEAn5GnsxudEKErU5oml/Gf0Ui/PODxPgsoE8zdONPXOx5LNe+gayKZMijYYrN4qnC5Q+tPYXCurD6IBPAd7mGIvNvOdPzi9xcK6FP87D7IhfvjGctr/TAf7hz7XMcdZVYVAt17vE0fiXl9vyyMWWnB4bxuF1caPodZRYTe50PWMjoCPNM9n3Guc4HBOSCW3dEJNeGHniAq74ca20Hg4H5jU7OpiKrIjk/2BcwdYYAACAASURBVMqLUZHM+z3dE5LZzwurDjK6n+7aIhSlgBLHP5nJbrK2FqGfcZ97l0sDT7Nb+YcCjPvv0m4vRFHSFEkpt+eXjSq7UIBx/wO66vagKPmrEEmAeQIDl3jzzF/xh8Qm1xMRydWiE/Jfp7nMiv38Hcbkul1CdsdI6kHGf7iHx/M9gyPTrLk9cvrvNO3OR1EKKW+9K5VdDll9kNEJTT3Ec2cAfzDzamUJwXt80nKL6bUfaXMR+pnrjXbe6M5Vl8g47jpL+iK5Urlt1XLYEmxk2QXxOSvWIJIAIUa7T/Ib7Q7TUievGyKSq0En5P+WxpJy6l3Skp6KzT3Zxu/CFm3htLnwb3R6tjCbosz1Cfq1cgrq3FtMYGZ42PE6u49cxZ+zQJSpSC7Dli2HbUDOyy4bIgnoI3QdKedYzh6shEQ2RYx/rohIpHUP9vZBghudnE2MiKQAbIYyn+Vx13EKFYX8LSYwuv8qR8xWmvqmcniWbInk1i2Hrc96lF2WRJJ5JtwnKSysp3tUxtKvBxsf458v9MnbNFsLKZGW8xXJrkgG7qBZC1AUFfOhKwxHo8zMfdqPHcJmO8Sx9vvMMMuYtxOtdh/FJhXV8gpHm7/AMxaZbar7cTeWGibuGLd8LK9+jk+GX2aVNXVtB4bpd2m81nxzsfIKTTMycJuu829Rsf8zfPPTPOxuodpqRlGLqGj40jBgOcRYXztNVVZURUEtruK0pqFpF+nxz0V2GcXb2UJ9lY2K4t0U296kpfNu8vEqkX3frGvgdF019pq3+ajjFr6puZT7Ln/cBYLjD7jV8QG1r3+Ob/YxvedrsZp2Unbmb4wtG2Sm8TpeQS18m57pZNXuPIGhm7Q1N3LqozYuX2impvoELV8ldKPoQcYH/0aH4zivtz9g1v93zte8hMkUnXBhFMlH4fur5hUsaj6Wshq0q/eZjPtPycpthXJYMQ2gB37iRttZGk79kfbLn9BU8zvqW77GFzc4fbn8vMb97100V74Qvt/NdrSr/2A4+ns9wHCvk+rCPAqrPuU7/3Iz1EME/D481ztw1DXS8UOQ0JQPt/MUh0uLMBfbafzifjifo9eBzYrJZMXW+GWSAfVpllXs9OlchyECI/e53fUxJyuqcfp+JfDwGxzVezEp+VgqTvHFYOKY2kzK7lO6evu45jiIWVFQ8k/gnl4I/5eR73Ff0bCZ1RQPIAsER27T3nSM6rp3aKp7gwbn13Rp5clFMpN7FNBHu6jO20G5c5Akd6aQZUQkMyA0hKu6CPPBDh7OiUWuRHZF0tiCaNLwhJJ8bmvjVscRCotsnDz7DjWlhRFBVDHHuv4C+LpaebemlLzo7yw2GjQNTdPQLvatUHkLmbKaMtcDP3HDWU9pZMmnxVaQIP7Bv3O16RVURUGx1uPQTqFduUl//00u17+EqqiYa68ZynGBafcJ8pO1psz9yJXXq9B6R8PjbvUpvC0VqEoB1ubbTMb50S/cOlXKzvrrTOjGfQ0PI3ud+BbSPK7+BM+l9zkdqaCVkndwfvoeH114h0qzirLzDH3LLXM100vTTpW86i5Gl+w2T2DgM+zmF6juehSRhUhLjWLhSNdIWM4mPVzSTlFVXICi5FPy7id8euaPXGjaj1lR2dnUy0xMJHdy4Phxqo+9T/uVK3S01kXKZ/E/pS631OWQThp+DdzDad8V91/1ievUF+Yt3ttp5ecCM94WSlQlef4+u4vjxQM4fcuNmtaZ8w9yp/dP1BXloSgv03DhQ5rPfUnPwBCPH97mvH0XirKHuivXuKSd52rfA0b8P/Gd83UKFZXC6DWUQVlldh1e4P6jQW5fbaZUVVCUlznpeJc3tS+51f8Pbl0+gVVVUMx1dI+Fg+lqyg6AaTd1+UaRjJKqJTvyf3dX4YxNDovmgbpUJDO5R2N5NETHPhPKnha8zySg5xoRyXQJMNBaiWquwTUsHdrpsP4i+cJuyk58xXBkkLg+0U1tfuS7hCAXt5yQdG3nlNUHmTkeu46Ql6TyWvA52asoKIXH6XpsaDmKyJWSV0NXrFsrVSU4g8/56mKFHCHcmqGgqPsNQrFYsRsrxbDMJJ4vk+Ma/ovJjnMwACwQ9P+LeyOBZcZ46TzztrAnZVfjFH1NxSjKToOcLJ4rXj6jXYoqpkong0Ed9Bn8g4OMBOZZFIKChK6YeQLeDylTFRT1FRzeaCpSldsyMrJsGkLM9J1hZ2K6Fx7g3JufkPdp5OfcIM7yHSjKHurdo4Y8Dufpi/s6GE7LPaL5Uoi94yfDBEA9ll5l97v0TRtaH5/dxbEnL0FiMymrTK5DQx4pu6ntGjakMfk5V1V2GYpkuFtvZ5LWwgBeR2mCSGZ2Ly09dykObyDJ90I2EZFMB53gwMeUqWb2tfukpTxN1l8k1UN0DD9b/E3Ig2ZKvvakiOT6sfogk7ryislCtAUw9kW04qzA6Qsuf5yZXpp2mtnXMRQvbLFjmAzfRSvexNaVaIVVvDhOMaPjLvNf0sqbVOPJdEL+fjq/cMd1/SY/10pj05YbIxnNl2jrpTFtqxHJFGkI+envvMK3PkNXbNKyTic/ozKmoJY78cW6lwJ4HfsTxGo5UudLZtdnJmWVwXW4TB6lzu9VlF1GIjmDz7kfdUl6UqQpw3tp6bGyMEFMWBERyTSIPMDKC1UyY/1F0vg5iEhuEjanSEZb9AoorjoVHtaQZHuvy0f40WSxezdeNKIVVrQCz/S4qxXJTCYmRMcNfkqr9jpFWRXJxbxVYy15ORBJA3pwlMFbf+Z867scLcpbhUgCz+7RurcgviX12V0cLzfiTnuN2myJZNwvVyirdK/Dlc63QSIZ7XJesm+yNGV+L638/4RcICJpQJ9m6IaTxuavGY6N4Z3D33UM85L7VlgJEUkB2KwiGf0s3RaLEGPddZjjxttCrDuu8GREQDI9bi5FcoGg34PrXBPnIhNist8imSz9uRFJPfgIj8vBqXNXGZicW0OLJCwG9uh4Wp2Zvj/wPwldqMuTTZFMt6zSvQ5XOt8GiWR06ElaIpn5vbTy/xNygYhkFMOwFuP45rn7tJbko+w+h0daIzNCRFIANrtI5rHHcTdJi0YSQo+4pb2KWX2Zxms/EgjNExy+RmP564ZJA5kfd3UiGWK0q4a8lBVldPLCi3HjAHMnkrnt2tYD93DaLfGTVNYkkixKjbofp+8RfU22DJdRypZIZlJWpHkdLp9HGyaS0X0ThyElTdMq7tElx5IWoPVARNJA8B6tZTvCblF4EvfEHNM9b1Oo5FPSel/GRmaIiKQAbFaRXJyssqQVJ4r+lIEvv+Ke8Qly7l90vPk2re0XcGgtOF23E5Zcyfy4qxPJ6JOvUeAMRLtuEyr37ItkNB1F1Hb7E2Q6WyL5KwOtv0VNPP9aRTI2Xk9l5xtv8UZppjN8sySSGZVVhBWvw2XOB2yYSC78QPsrySY6JUvTKu9RYHEsaXGO11gVQEQyHn1xZQglj6IGJxeqdi4znEVYjk0tkrEgrSiLS0SEJvB1/y+3/fLMkE02k0jGzYCNVuBKAdbGa7HZ/gCEnuC9fJrGzqHYTFc98ADX23/kxkpj6DI87upEcnGmbrLlf/ThDvapCkreEVyPo9ezoWLOmkg+xaO9jNl+CV8w1pa2oowsTfMyaYiOq1MKOegaXpSP6AzoVYukYbazspo1B7MjkpmVVQbXYYrzhVm9SC4pu1jLbkIrY2x2fLLJNmqSdfSSpCnDe2mRSNkkzmIXcoKIZAL6BH3Ne4lbp/rFVgZljeqM2dQiGQt+ioKi5GMpraDUkp+y+05YPesqkinkIlZZm1+n3TvEw94/8cmNEaZja9cpqJZKjjedRTt7kiprMXbnPcNi0LM8bD+Aaiqj1tHGFZcLl+svdPf8A4/Hg9fnJxAbWG1cE2+l4xr+S6Zr3umjuOv3JP+doQIurvmE7t6/0eU8S/MHpziQp6AU1tL+95t8fecRC7EKPFUXYrR1pxD7pX8R80XmmPRe5MjBD7gVt3h36nJLXg5+9GXTEG2RVFCLj3Gh+zY9XU6amzVOHyhEUXZT036DW1//g58XMs3PiNikXEZmOdIQycQ0LNcimVZZZXIdpjgfkHypHVhV2enj9DaXhuXQdo6rPX30dH1MY+MFLp8tR1FUzLazXP5mkCk9OkxhV1gOG/7C4HgQXQ8yPniVprLC8P72j7jef5+RwLOM7qUYz+7i2JOfsJ6skCtEJJey+JCqoCgqL2j9LPeKAyE56/Nmm+m/07Q7f+nnAAv/xnVoVzgAWd+jPxA30Ijxfic1VjOKYqa4ooo67SJfeR4ZKkohG6xuQfJhPO4/o9nC5Rd+C0kP3pEnjA26aat7KbwAs/oSdW3f4hmZZm58EHdbfXihZaUAa91nuAfHIosYP2Xwi1NUWExYKuo5f+PfkXIOv1HE2WCn2KSiqBbKat7DteQ60An5/4bDvid+4efYpmKu/JD+yegjZxrH1acZuu3io5qSyDF3YdM66fGOJK8cl+ZSpAsl2Vp584tvMTFZsdX/ka6BJ4T0X+jRDmDJK6Ky6S/8y/8Dt11/pKa4ICGf49ew1IOP8HS20lhzCJu9hoazf6ChroGWTg9+QytR6nKLHC9JOfx/gYcrpkEP/Ei343WspsLwW0267jMZmmOs530qLSYslc10/muEhxnnZ1icCtJeOzKc76FxH3euOSJvblEprGrlr3d8jM9NM+L5Nr3rs/5zbnlHCOjplVV4aaB0r8MQoST3w3XPMIG5MQbdn1FnLYjExnra3IOMTWZedtFrOVw+tZRZTFhKD9PgvM1IcAZfez21jg7c3pE4wQ2/pegMNbZSLOZiKqpO4bxxH8/lExxt/JCO6x5+GA8uLlae1j26WD5zPiflarLucyEXiEgmI8Rodz2FioKilqH1T250gp5LNve7toV1Y2uUebgCbzn3fwwHJvnFP4zP68Hj8eDx9NPn/l/amuz8pvVehpMC1pqsCfq137BH+27pOEkhDQJ4HXbD+M7Nzia9DjcV03gdv2HnsWtJ3vgk5IKtEeNzQOgRPReddHpHkwy/ENJBRFIAtkaZ62M3OVViX777c/oGb/8hycSXHKMH7uE8clJeubUK9Inr1L/YkMHakRvLZr4ONwc6cw87OFR+hltjUnWvF1shxgubExFJAdgKZR4ZT7bswP15pj1/4pO+8Q1o2dIJ+f/KmTcvMRB4PoRow9ADDHvucGfwZwLBMfodB3gl40k2G8Vmvw43Hj1wj4tvvs91v4xGW0+e/xgvbFZEJAVgK5R5dLJHAcU1n+Ie/Jmp6LjAUICxobt842yi8VKKgf/rgk5o7B9cPPc5/WNSiSbHMBM6sqllH+J9buT7ebgONwqd0Nh3XGr5Ao9c/+vO8x/jhc2KiKQAbJEyDz2mz1lPqUmNn9ygFlFx8hO6B59sjjEwoWnGp7bv6LiV0AP3+aK+HJNSSGn958+fdDwv1+G6oxN6+pRft51Abw62RIwXNiUikgKwtcpcD07ys+9eZKmVEcYD27PaFjYWuQ6FzcRWivHC5kJEUgCkzAVBELYyEuOFXCEiKQBS5oIgCFsZifFCrkgpkrLJJptssskmm2yyybbSllQkhe2FlLkgCMLWRWK8kCtEJAVAylwQBGErIzFeyBUikgIgZS4IgrCVkRgv5AoRSQGQMhcEQdjKSIwXckUORFIn5O+jvamGymIziqmYyt+7eDgP+pSXLxwamvYeH7v/85y88mx7kLsgM8fUkI+R5+bNJML2RScU8DPy5DlbAF0Q0kBEMgvo0wzd+BMXex5vy9ebpiLrIqlP3qbZWoCiKKjFVbxz0kaRWcMTgpBHwxSd5WNz4V9b2oUssvYgM0/A9xXnGt/m3ZM2ilQFxVRMRUUxJqUCpy+YlXSuHp2g7zLVRbsoO/M3xrZNFAjgu1xDkfm3nOn5RYJfEvTAf7hz7XMcdZVYVAt17vGNTpIgZB0RybWhBx7gqi/HWtvBoDSMxJFlkZxnovv35CkKirKDyo5/ozOL/x9efl4QkdzMrC3IzBMY+Az7ngbcE/PALP5bLdh3F2LaoaJsCpEMMdZdh1lRUEta8M5sE6XS/XTXFqEoBZQ4/snMRqdnMxIKMO6/S7u9EEURkRS2JiKSq0Un5L9Oc5kV+/k7jIW2Sd2RAVkWyQAerTiyrpAJm2sk7tuNF8kJepv2oioqpvLz20cm0mBNQWZuEGf5DvKquxg1ZunCA5x789dfJIP3+KTlFtOJn4cmeej5jnv+mW3UMqcTmnqI584A/uDC2g+XKm+fe8Zx11lEJIUti4jkatAJ+b+lsaScetcDAtun4siIbSaSI7hspkj6qnD55d23UdY0nGG4g32qQn6dO14wNkIk9Qn6tXIKEtMirJ0tnbciksLWRkQyUyISad2DvX2Qje5T28yISArAWspc55m3hT3KZhDJWR53HacwWVqENbLV81ZEUtjaiEhmRni+RyEl2h2mpSVyWbInkgv/puuNlxZFMbblY3n1c3zzK4nkAkG/h86WE1QdKMWiKqiWUg5U1aO138A3lTjHe4Gg/x7ujg9prDnEgVJLuMu6+LfYa87Q5vYxFRvLEGLMfRqrSU36ah/VUs3lDR/Dt7GsqsxnBnGdO8vpKitqZHLVaU1Di25nj1NhTjVGUic05cPdrlFf18jZpjqq7DU0tv4FT7KuZ32aoRttNDc08VH751xoOkZ1/R/5yvc0sm+Isb52mpak5SI9/si1E5pmuN+F9prGjeloN2+IwMh9bnd9zMmKapy+Xwk8/AZH9V5MSj6WilN8Mfg0SVf4LGPev9DyZj0NpyNp/+jP3PJNkNHjSWiaEa+by9pRXmt/wKy/F2eDnWKTimrZx0nnbUaWdElnknc6ocAw/S6N15pvLgpgaJqRgdt0nX+Liv2f4Zuf5mF3C9VWM4paREXDl4YB5WnkbfI/l37e6gFGPF/jsO1CMcqqHmDE4+aKZsesKCj5J3BHyy4UwO/zcL3DQd0bX/DDwhxTPjfOhsOUWl6g2PZO5Pg6obG7dLa8ia24EFOxncYv7id0UxlF8hFj3k60mlewqPlYymrQrt5nMnFsVGgUb2cL9VU2Kop3U2x7k5bOuwljqBYIjj/gVscH1L7+Ob7Zx/Ser8Vq2rnNJn0JG42IZAaEhnBVF2E+2MHDOblJVyJ7IqlP4P3iPWpKoy1+eVhsb6FpGo7P+xnXlxPJWfzudymLiJ5qqeR401nONhyOyZ9adJT2AUOFHvKgmSKiWvY6DR846bjSxgfV4cpOUQqwNt9mMlxLEfR9Tcu7NZTmRQVyN7aGdyPSc4m+se3dOrn6ILPAtPsE+Rm1SEa7DIqp7fyJYKRQ9cA9nPZdKObDOI1lrT9lwHkYc14NXaORctJHcdfvQTEfoysmMynSok8zdMNJXWlhuOxjMqIz5x/k9tVmSlUFRXmZk453eVP7klv9/+DW5RNYVQXFXEd33PURYuxWM9adJyOTi+YJeD+kTDU+oKTRCjs3Ql/nx9RZC1CUfIqPHqf2yLu0XXFxpf0c1cUF4eu48Vv8MTlJP+/0wE/ccNZTGrmHFvMkiH/w71xteiV8r1jrcWin0K7cpL//JpfrX0JVVMy11wyis0w5J2U1ebvMOabd1OUbyy6If7Cf3o56ihQFxfoWF1rPcu5KDwNDIzzs/Ri7WUUprOfKzc/RWv5Cn28Y/8NenNVFKMoe6t2jBumOiuRODhw/TvWx92m/coWO1rpI/hnjCTD3I1der0LrHQ0/OOhTeFsqUI376U/wXHo/9qCllLyD89P3+OjCO1SaVZSdZ+iTcdrCOiEimS4BBlorUc01uIa3dwNTumyKrm3df5Uj5nBll1f5KfdjLSE6If816ovywjJpnG2r/5sO+6s0dT0wtDwCz+7i2BPeX1Gr47uvY/IpXduJrKtIBu/RWrYDdV8Hwwn1aOxaMAriTC9NO1UUo0gSxOesQFF2Ut31KCIEy8uO/tjFwbyEVq24dO6mtmvY0KI4RV9TccI5QJ+4Tn2hmiCrEbFN2Hdlov9DpbCum1GjMA67qDar8dKTad4xx2PXEfKS5MmCz8leRUEpPE7XY8PaiUnzO1ORjP4sk7zNRCQTPs+roiMu6EePn8fu5l5D11R0KIbKzqZewyz2qEgWJHRlGR4S1FdweKeBGXzOV9lZf50JQxnoo11U5yko6n6cvsUjx/LZZMc5GCDcm/Iv7o0EttGkL2GjEZFMB53gwMeUqWb2tftkres02QQi+YzhjkORVsQXqOn+b8IxZxhsLY8cs5imvqnI5wvMBp8lCcTGcZDFaJ6AMQEikilYP5FcYLrnbQqVPPY47vIs8XAxIdtBuXMwciPP4u/v4otvfYbuyKiA5bPX+YCFldICqWUkZctpsnPozPSdYeeSc0TPnSgoK5HsHFECeB2lKLFjribvUudJTHD2OvEZT5yq3NYkkunk7RpEMvHzZfI1+f9eboxkVEoj5TDTS9NOM/s6huLjT+y/muK+S5nPgrCOiEimQWQFkm21RFwW2AQiGQ3gScQv/Cv8rqrYeMslla0eZPwHL33ur3B1OHFob2Gz5IlIZsj6iWRUjpKJExgfLJK1uqEHGR/8Gx3nP0Q7+j8bIJKL50hc7igsDNkUyUVpDUvIavJORHLtIrmYLnXfZX6428IepYDiqlOL44ETtve6fDHRF5EUNgMikgYiw50am79mONYLNIe/6xjmjHuVhE0gksu0IIZ/ZRBJBZPmCXePhZ4wcPW98OQAJZ8i2ztc+OIaPf1fo5UWiEhmyPqJZLTCTtGqZjheXMWrz+D3uDh3qoWrA08IbViLJOhj16hd0oUc7TJNGHsXPWfiJK/Yf1tOJBMlZDV5JyK5dpE0/uZj/vHtCfIzmN0tIilsBkQkoxge0BULR7pGwvF67j6tJfkou8/hkdbIjNgEIvlfumteSFMk83ix9T7z0cGwioKimKk87zV0eUrX9mpY/xbJpS16KY8XnWxTGJ3cAhvXtQ3hN/e8T6V5B9bGr3kYCKEH/01346scdN6Lnw08N0JfpwuXK2H7ZpApfblzRJIW18q5irwTkcyiSKrsbLqN332C/JQynyQLRCSFTYCIpIHIWHNFUVAKT+KemIsMG8qnpPW+jI3MkE0gktGAH55pXd7+Y0KT8gQ9Dbsj30eC/LwXxy41uRDq/6ajcoeIZIasn0gangaTzlqNds8WUdvtR0fn2cB59i5Z8HwjRRJgBl/HaRpaP8PpOIfD2UlPpkv/rHiO6JjIxbzILO+WzxMRyXRFMprvRdR2P2Y2sm6qEvdgY9z9KQNffsW9SPmISAqbARFJIzoz3hZK1HADVVGDkwtVO5PEKSEdNoFI6sz5nJRHlk5Rre/QPfxrpBKcY9LrDC/jYQzc+hAd+6KtjlYae8Yi+ycuw5IgkrFKTUFRSnF4A8AcU76/8ufbj7f1mIh1nbU99wMdB3fFdytE0YdxHSxELfuYgaCOcdxf3kEXj2M7JxsvmHr8IpA9kdSf4nNpnLsxmoVrZhmRjEyeiRv4nVHexedJtkQyeWtoClY5kSl+fKwhRmyISD7Fo72M2X4JX1CHZ/do3Rtdmukaw8Z1PkNP8F4+TWPnUOyhQkRS2AyISCagT9DXvDd+yNGLrQwmeTYUlmcTiCRAkOGrdRTFBNBMccWByCLjkc9M+2jxTMSEcbL3bHgdOkVBUfdgO9lEw9HfYD34NqcPFKboKo/OvoxIq6WUVyPn2Jpv60if1Zd5iNGumqTLyyy/juR1mssKUa2nue6PLj0zi//6aUpKjJ8ttkgqagk1F/6P3p6rOJv/wAenXyVPUSmsaePvt/7KnZ9nY69rVMyv0+4d4mHvn/jkRqR1LmORTD65ZeFhO6+ohZTWvk/7lXBXdWd3D/0eDx7vD/gDmbRLLi7/Y7Y78U4uLtsz2d9CWWHCmpoZ5R2sSiRjS2jF58eyeZuKDPM2/DaJAhRlFzatk57vbtF1/jSNbW2cLcmPfP453wxOLF+msXxd2gWd/H9HY0Mh9kv/iq3PGX6YvciRgx9wK5av8wQGPos94EbXvdXOnqTKWow9YXhD7Hx7WvA+286Pq8JGIiK5lOhSbmEnUHlB62d25Z8JCWRZJGcZdv0u/AYKtQytfzLu24XhKxwyRxb31e4QPxpyjinfLToci2+2UUzFVNhraXJ+neSNHbP4+z6n6egrWFQzxZXH0Dr6GAnOMtZ3KTx70nEF71T844U+foePa17CpKiYiiuoqtNwfvVP/EveHrK9WFWZh8bx3e5Ei7yNRDHvp+nyDQbHZwiMfM/1tvqI7Bdgrf+cHt94XNevHhzmdvsZauyHqGk4Q1P9mzS13WQokPBIqEffulKIqdhGfctXDEzOoY/dQqssIs/yKk2dD8KVt/6UwS9OUWExYamo5/yNfxNMfDtKVFK8w0yODeI2prPuM657hgnMjTHo/iyyWLiCaq2nzT3IeEiH0CNuOV4zPPgkbOYDtPQ/SbO1crHlzHraSVvj0XBe1P2OWq0T71jysJZO3umBYTzuPxvKx452tQfvyBPGBt201b0UflBTX6Ku7Vs8I9PMjS/ND/fgWGTR7WR5m+p/6YSSHGvFvGU+/Pabmlew5FkoPXwK5+1hgvP/ov314zg63HhHpgkxy7jvDtccByNlWkRV6zXu+MaZCwzjuZ5w/OvfMxKYYTzhf9dfvok3sp6jHnyEp7OVxppD2Ow1NJz9Aw11DbR0epLEh3kCQzdjbyFSVAtlNe/h8jxazBN9mqHbLj6qKYk8EEevu5GEt+oIQu4RkUxGiNHuegpTOIuQHlkWSeF5Rco8A0KPuNXyIV8NTzH1i58R3z08Hk9463Nzte00+37zMQNptT4tP9lGEAQhG0iMT0HoET0XnXR6R1cxxl0AEUkhgpR5mui/cOvUb3gltuB3MsZxv92S5uvvRCQFQcg9EuOFXCEiKQBSOyYBFgAAIABJREFU5ukRXSty+QVr9WkPFz/pW3wv87KISAqCkHskxgu5QkRSAKTM02Nx4o9afIwL7vv8PBWMCGWIwNgQ3m+cNDZeZiBxnGdKUk8KEQRByBYS44VcISIpAFLm6TOLv+8z6kqjKwNEt3wsFW9xvnuQyVB6Myn0hEkhink/TW1f0tk3IgviCoKQVSTGC7lCRFIApMwzRg8y9bMPr8eDx+tjZDyQ+UDtUIBxvx9/wvZLrJVTEAQhO0iMF3KFiKQASJkLgiBsZSTGC7lCRFIApMwFQRC2MhLjhVwhIikAUuaCIAhbGYnxQq5IKZKyySabbLLJJptsssm20pZUJIXthZS5IAjC1kVivJArRCQFQMpcEARhKyMxXsgVIpICIGUuCIKwlZEYL+SK7Ipk4A6atQBFUTEfuYo/shjewtBl9ptUFEVBLWnBm9Y7iIX1ZFsHmdA0P49MZLAO5ALBJ6NMpbnwuCCsjhCBnx/xRK4zIQts6xgv5JTsiqTfhS06+NKk4YnUzCGPhik2KLMKlz/jpZuFHLPtgoweYPjO/3HJUUeFJZ/8OjfTK+w/4nHjcmrU2ayY8k/gnpY3Y29n9OAwty+1oDUdpdRcRNnRs7gGn65xMfl5AsPfce3SB9RVFKGmfZ3pBH2XqS7aRdmZvzEm7ikksO1ivLBurItIzg+28mL087zf0z2R7nuIhfVi+wWZEIHxR9xvP0yeoqwskoQIjP/C4/5WylUFRURyexMcpN1eTr17FJ15AgOfYTerqPs6GF6TxOmEAk/w32/HnpfJdRZirLsOs/T6CCnYfjFeWC/WRSTRg4z/cA+P53sGR6Yzf5UcC0z3/oHdqoJi2k+rd/kqX8ic5zbIBO/xScutFSQwFQtMu0+Qn5ZIRph2U5cvIrm9mcPfdQyzUoHTF4x8tkDwl2F+DmSpt2U111lokoee77jnn1mHV2zqBAc+p+XGeM7PJGSH5zbGC5ue9RHJNRPC76qKdI2bsLlGsnVgIcJzGWT0Cfq1cgrSlcAliEgKq2Ecd50FJU4ks8wmv8706TtoJS9S5xaRfF54LmO88FwgIikAz2OQmeVx13EKM5HAJYhICqtg4QHOvfnbVyRDw3TV7kZRLCKSzxHPX4wXnhfWRySNs7kPXWE4FhdnGfP+hZb61yi15IdndVtKsdW8TWunB39wAX3sOo1Wc/LV1NUiXr3sQ0Zcrp3VlXmIgN+H53oHjrpGOn4IEpry4Xae4nBpEeZiO41f3CegA6FRvJ0t1NusmExWbI1fMhiIlJweYMTzNQ7bLhSj1EUmuFzR7JgVY6UaYqyvnaYqK6qioBZXcVrT0LSL9PjnIof8iRttZ2k49UfaL39CU83vqG/5Gl/AeLUYRfI6T8bu4tJqKLPko1peoUa7ysDkXPxfXq6Cj/7HKhsVxbsptr1JS+ddxtKddatP8/Cbj2lsdNB+5TIXGg5SVlHJofZ/EXemjM4Tvcd+R03DHzjb8AbHNBf9Pi/XHC18NfIMPTCM55oDm1mNkwM9MIzH/We0xHLJJB2haUYGbtN1/i0q9n+Gb36ah90tVFvNKGoRFQ2G6yAuL2bwezppqT9GXdMZGmqqqGlq48bQdHy37VrzPHyy8HXbrlFf18jZpjqq7DU0tv4Fj7GbWH9Mz8fn0M4ep8KsoigvUFF3Bk3T0M51MrjiuMQ0zwPx19kTP17Xe9SUWVBVC2U173N14EnCEKEFguM++ro+5uT+d7lhvDYzyqMFgn4PnS1vUVN3mrMNNRysOUPbjZ/C97H+X/ouNlFVXICiFFBcdSr8/z/uiazSMU/g4be0Nr6Do/3PdFxoxFb2WyoOXcK3yXx4uyEiKeSK9RHJpJ/rzHhbKFHDQlhx/DSa9gcajr6CRVVQlGI0TwCCPrpamqkpNUUEMg+L7a1w8NLe52Lff9dhPNDWJ/My15nzD3Kn90/UFeWhKC/TcOFDms99Sc/AEI8f3ua8fReKsoe6K9e4pJ3nat8DRvw/8Z3zdQoVlcL660zECm+Z1sGk8pZ6fz1wD6d9F3nVXYxGjq9PXKe+MC9uWaq4Yxx4gxPVdTjav8TV8SF1pYXhBxvrWXon51dICzD3I1der0LrHQ1X8PoU3pYKVKUAa/NtJle8SIMMu2rYedDF4+i++gT92m95yflgUSQzOY/+lAFnFbvtTrwxIZ4n4P2QMlVJaFGLdtcmtjKlyOe00hHEP/h3rja9gqooKNZ6HNoptCs36e+/yeX6l1AVFXPttfhZxqFH3NIOUt74Lf6o7Mz00rRTRTFX0e4LZCnPAXRC/m9ptBZT2/kTwej1ErmGFPNhnAMJM7FX1SKZ4Xli19k+jp84yjFHG1dcf6K1rjy8AoZaSnPveHh//Qmezz/mo9P7Ex64Ms2jWfy33mdfeRPX/bORz6boaypGUXZhbx8k/G9TXSs6oWEX1Ttfx/V48Xqb7n+fkpecIpIbjIikkCs2UCSjwSifktb7LLb7LBD099NxqoUbsQo8gEcrlq7tHLL6IBMtx0LsHT8ZWkl0ZvrOsFNRUHa/S9+0Qcae3cWxJw9l5xn6Yq042RLJxfMaRTJW+efV0DUaWnIMteR9+g1p1AMeWsp2oCgFlDj+ycyyaZnB53yVnXFiDPpoF9V5Coq6H6dvhmXRh+jYZ4pPM6CPfs07F6Mimcl5Qozdasaal+TcSUUoE5HM7P8u+JzsVRSUwuN0PZ5d/EFUDuPKJCzU5sKTuONWd5jG63gFVTGxr2MIPRt5Dvz/7L3fT1TX/v+//oB9wyUXJCZkEvKNSWMmXGiad5gLTBsSMKeZEGtDsKkB09MM7TfO1EawKYNN2aZ2SE/Rnk4sRI/YMuF8HHukFvCA7wAt0yNGJp5pC+eAOhZQlO+UzyAD+/G9mF97hhlgkFGE9Uj2hTh7r597red+rfV6LYI3aS7dkdLbWvNf4rBBQRjexe3XWabXIyQzTSfaz5Qy1KGHOqtoVAim8M5e1jczqaOICDS8HPFEj/069sEfz3u6vvKE8bY3URLaE9Du4v7ovBSSzxkpJCXZ4jkKyXu4q8JWnxzzSX7w3uFRMN1II4Vktnl6Ibl8v1RMQOxNskaknIg3ziJJyM9QRztXfToLT6Zp6oWwXvCmystcP/adhojASVXO/OX/l4x2F3f1ToTyKrXuX8LLiABagPv3A+F7M0lnwYuzbMfyuk9bFxkIyQzLm1E/eHIDx55cdtr7SZaBWtCP9+dfmA5pG1PnLDHb9yEFIoc9jhs8Sf5vbZJu2x6E2EGZ0xv/2M1YSK4jnZW2UEQFuCjCPvAo/vfkezKqowDDjpKkj7to/ubwe2/im45+BKTrKyEm3RZyRB6mWjejsS0LiwTuT8b7tOS5IIWkJFs8RyG5wD3XYXIS9j0aKCq3YG+5xljCvikpJLPNlhKSOrTgJN7ebzjd/AnvFOZkICR16Spv0jb+JE1eNJ4MN7FHv18sxfWp27dcQCSgX3LOpbDyM674HiZYeDNJRxtvY7+SplxPJSQzL28m/SD821z26pfzl7fqBtV5RDylTS9iYRMi0ZKYsZBcRzorOttE2ypJLCfcs5hZHUXLlOrDYxnp3/m4JV+gFL7FZ1duyxOgNglSSEqyxXMUkkT2Qh0I7+tJuhL3pkkhmW22mpDUgnfxuBwcPxlxmMnYIpkmn8vyEn3GRniwLjAz/DdqivIiff0lytXOiGUnk3TWUa41C8nMy7v2fhB99mpCcqPqPFrmNJZCXdkT8p6xkFxHOisKySA+p3m5ME24J5RZHUXvfUohCRqhmWEu1BSH98UKBUO5Sufo7CrWYUm2kUJSki2er5AEIERgYpieS+doVo9RFfPQ1g+6Ukhmm60kJMMODMZEZ56nEZIrLm1Hn5FOJGSOFrzL4LmjlOQr4Ym48hy+4GIG6ej2fqY6aWVDhOTay7v2fpBmf2va8j1tnUcthenSS9M/1m2RzCCdNQnJlZa2Q5nVUcr9qulYSUhG0ObwD57DFnFaS3CSkjwXpJCUZIvnKCRDTN8cSgrHAmgzeNTS8G8rXPgBKSSzz9YRkn8w0vwaSnJ+1iMk5/qx78xJ9CpOu7QtEMscRCJojxn59jI3VwoPo/2X787/mOA0FJq6jlq6IyIYZjJKJ7q0LXIO6zxoV6qLTJe2117eTPrBqs4yoTtc6/iJ+xtR5+n2wcaILjkXUtPpX2W/7UqsI50VhWTEk9pgpXNKJ/pSLm2vsY6ie3ST94PGmMd/7R8MPlxkJWebie9cDCSEHrpPv/oailBS7nuVPDukkJRki+coJH+n07ILQ7nKFe9kLByGXkjm1fbxBxD/AtdbKjVCj27T+U2/LpyLZL08fyEZn2wTrWgaCz5nivOt4wInwcoT8X4WooCDrvH45B/1FM/E2cZzkt3JlpRUE/yTmzTvDcfVM9VdYVzvNBZ6wPCFeuo6xlY+GnTpNk5zY5LIiDovRCxPmaQTdbYReRSrg8zqvXZn+2nYnVwX0TAvyU4qc/icr6Po6yjD8mbUDzQ/nTWFEUvsGQb08RVDvzN0phHnSGBj6hxg4RfaDu5CCCOH3ROJy6/aOK6DBSilZxgJ6v5nPV7bmaazorPNT6i7C3XheEh9T0Z1FD+rWxje4vTA3fiYzDxTQ2c57ryZFP5nJ9Xuu7qyBPE5LYlWUqIfB1JIPm+kkJRki+cnJJd+oXXfjvieSGMJb1S8gbkosrSt7MMRO1Nb90UvBEIxUnKgJBxvcjOe/PACklUhuaeJ4SerT8TazHUaTHkIsYsKtYO+n3pxn66nrqWFxuLcyN/P8703HA4lZnUzvE3r8Bij/X/jyx4ft5pfiwQqf5evOq/T53bS0KBS/0YBQuzG0tpD73c/cmdJt5RaeZ7fYhOtRmjmZ5yH30btvZsoRlJO8IsERr6m0qBE+nI5R+yNqI3HqDIVUem8ubrH6tJtnHt3c7DtF501KBy+peBgG6MLWobpLDLT34hJEQhRQEnNKVrbXbS3fsb7x45waJnjke73hkrUS7381Pd/OF3XQMuFBopF5O8XfsD76ElG5U0rJFOK+2i8xcg+0WiMWfsRKkxm6rruRNpjA+o8ll4XDaUFKKZ6XfzEefxd9RQX6/+WnO8SHMNrXa7NMJ3YUvMhzv32h05MP2DY+R4H1WvxGJtRlvXNDOsodIeuulcjextzMZrfw67WY614lVJ9TM+YBVXBUNXK8Pgv9H/dQs9kAJ+zHEOsv4bLveBzUlZwmLZRKSOfJ1JISrLFxgrJdCfYpPy7RigwjufyWVRrVUxAKsZSDtk+x518coM2ydCZGkz5CiK/CHOVDdX53fITISTrYj0ByUPTPgZjJ6IoFFQ188Ogj+mFWSY8V2mxvhKelJRXsLZcxTMxy8K0l+4WW0Tg5GGynad3eCIyoS0SGP0eh2UfxhwjJYeO47w+TnDx37S+fQRHWzfDE7PxfqE9xnvxOGZjPkazjdM9/yGogRb4lU7H25jyC8KneLhvMRNaYKrvFOXGfIzlDXREQwNpc/g9f6e5zkJlxZuR019svN/UkXSqSYAJzw9csEeCPkfFrj7vY9dw1lZSlK8gFCOllk9xefSWnZWq879cPlHLsarXMNecorX9W1odH1HXfFUXRiXTdOaZ8pyn1lyIInIxllpQXTeYmk9jUdPCp85YSo3kGEs5VPs11yf+YNF3nrdrPqOte5iJQCiDfMwz7e1eWz+wfk23dyrSthqhmVu4m45yqCSSl1RjwtPWub76g+Ncbz2BpfJNLLUnsNveXx49QgswMdzHpehJS0LBUKHS3v2jLjTOBqQDxE6Yaf4QS2UFlZZaGhvrsL7fREe68qX7yMmkjkIPGHF/ju1QaeQdPBp5fxJ/rAVucbF2P8acQszWL+kZ/wONJ0xc/pRjx6ooNb+Ho/Vb2lsd1NWd4XvpbPPckUJSki02VkhKXlhkm28jnsVZ0ZJnz2Y+n1vy3JFjvCRbSCEpAWSbbyukkNyaZBTCR7LdkGO8JFtIISkBZJtvK6SQ3DJooRCxRfHZbqy5GxeGSrK1kGO8JFtIISkBZJtvK1I6uUheNMJndO/AFHGECTu1WHCNyzaVLEeO8ZJsIYWkBJBtvi0ITeMbvKpzGDJS1fovptOecS/ZzGiBW1ysq6QofyclluPY1RZ6xqRTiyQ1coyXZAspJCWAbPNtQWgan8eDJ/nyTa8eb1EikbzQyDFeki2kkJQAss0lEolkKyPHeEm2kEJSAsg2l0gkkq2MHOMl2UIKSQkg21wikUi2MnKMl2SLtEJSXvKSl7zkJS95yUte8lrtSikkJdsL2eYSiUSydZFjvCRbSCEpAWSbSyQSyVZGjvGSbCGFpASQbS6RSCRbGTnGS7LFFheSMwyppShCIAzv4vYvPO8MbVq2TptvUkKz3Jl4mFG8Ri34kPuP5GF3kmyiEQr4mXgw/7wzIskycozfALRZxnr+xtm+ezLwv44tLiQncFXkRzaDFqF6As87Q5uW9bd5AN8FC4WG1zjRd1++XHq0AOOD/+Ccw4rZmEuutZvZFW9YJDDxL7pdTlRrBUX5hVi7p59RZiWbES04zvVzTaj2dygxFFL6TiMu7+Onfs+0wH8ZvHIeh7Uco2JcYz/TCPouUF24i9IT/2RKvuwvFFtnXn8+aIHbuGxlmGra8AYWV79hGyGFpAR4ijbX/HTWFCJEHsWOn5nb2Gy94IQITN/lVushcoRYg5DUCAUe4L83SHPZDoRY6wQv2ZIEvbRWlmHrnkRjkcDI11QaFJT9bYw/rYgLBZj236C1siCDfhZiqtOKQQiU4iaG56SSfJHYOvP6s0Yj5O+iodRE5elBpkKy3yezwUJyidn+j9mtCET+6zQPrzxtZh8pJNfK+ttcI/RoFM/gCP6temZz8CZfNvWuIgLTscRs91Fy1yQko0zTbTVKIbmtWcDvfheDMOP0BSN/WyJ4f5w7gY060HId/Sw0w6jnJ276557N6sNTvXsSPVJIrgeNkP8qdcVl2Fy3CUgNmZINFpIh/K6qiHDLp8I18bT5e0qkkFwrcpBJg/aQIbWMvDWLwGSkkJSsh2gf0AvJbKWxSfvZU797Ej1yjM+UiIg07aGy1Uu23sKtgBSSEkAOMqmZ5577CAUZicBkpJCUrIOl2zj35m5jIbkR755EjxzjM0ObuU6DqYBidZBZaYlckQ0TktpUF3UmQ+qo50ohBy74CG9P1S1/i1286foP+gXRpbELvJ6vIEQeJnWQmPRb+C/dZz5FVVWa3P8m8MhHd8sJLOVF5ItcjKV/xt56nYmE5dWVhaQWvIuno5k6y0HMRQaEYqTkkA21tQffo+3l4f1US9uBcYZcKm81XIsP+KEAfp+HrjYH1vcu8svSAo983ThrD1FifImiio+46H2MhkZo6gYdTe9TUVRAflEldRdv6ZYQwl6lPs8PtDmO8l6bj6XQQ3zdTmoPlWI0mKio+zay+XmeqeG/02SroCi/QJcGhB1ZfuaK4yAGIRC5R+meXSLm4NKuUmFQdJNqiKmBVuxVJhQhUIqqqFdVVPUsfVHvf22WsZ4WGmrtfNF6nq/s71Jt+5zLPr0zhF5IdvFg6gYu1UKpMRfFuA+LeomRmeS+tsIEH5pkuKMJW1UF5qLdFFW8T1PHjQz27SwSGL1Kc91HOFq/oe2rOipKX8P85jl8+lcno3Si9f5nLLUf01j7Hu+qLoZ8w1xxNHF54gloASY83+Go2IXQiwMtwISnm3a1MqldMslHiMDELa67z3DMXI3T9weB0e9xVO8Njw3m47p+oEObw+/poMn2Llb7CWotVVjsLfSMzSb+9qnrPPqch/i6z6HabNQ21mOtegtLXTMdnrsEo4/S7tF35iRq4xHMBgUhXsJsPYGqqqgnO/Cuui9RI/TIR3eris1aR6PdSlWlhbrmv+NZthyt72d3mRruQLXsw6jkYiy1oF66xUxCGZcITvsYcJ/h2Ouf0DObtJVlzfW0RNDvoaPpAyzWehprLRy0nKCl57fIe7/6u6cFfuX75nrqHC20t52htuJPmM0WWrMmul98pJDMgNAYrupCDAfbGF2QKnI1Ns4iGfThbmrAUhIVbjkYKz4ID4DqKc4O/B4ZxPRWS0G+6kkIiRLyqORHBWiFC3/0P/wuKqJ/N5ZhLtpBftFrVJiL4r8XCgXvXmEy1u7phGTUZJ0XE7rmI/Wo9vcwG3MRQqCYGuid2qi9SJufdX08BH6jx2mjJF9JFAcE8XuH6G+zUSgEwvQBXzU3crK9j5GxCUb7z1BpUBAFNtqvnUdt+jsDvnH8o/04qwsRYk/EwQBYuI93sJc2axFC5GKq/YLmhiba+24xdu9X+k+/hUEoFFjbuHbOQdOlAXwTdxn96SzVBQqi4BjdD3UedrPdWHNTCJaU4m0Fa6L2mBHnIQw5FtyTkX6iTdJt25MUakr3jDfe42i1FUfrt7ja/oK1pCDS1xrpn9F7AaYRkgu/0v52FWr/ZPid0R4x3GRGEXmYGq4zs+p4pxEad1G9821c96L5W2R26BTFrzjjQjKTdLTHjDir2F3pZHgm/szA8F8oVUSSRW2F+kzVLmvKh8aC38v1Sw2UKAIhXuWY4xPeV7+ld+hHei8cxaQIhMFKp/59Dt2lVz1IWd1V/FGhM9ePfaeCMFTR6gtsUJ1H07tDV10JO2s6GI9+7Eb7kNhFpfNm4v6rdVkko+NaETUdv8XEqRa4ibNyF8JwCOeIXlBH+9lO3jhyhOp3T9Ha3k5bszXyTuvKqD3Ac/4MX9S/nlrwr7me5vH3nmJ/mZ0ufzTk0CMG7EUIsUu3hLhCXwmN4aou5qBrPFYWbXYQtfhAFq23Lz5SSK6VACPN5SgGC65x2Z/WwgYvbQfwqEWrLG1vgJDMPUzr7WhMvgVmhp1hYRIRjPaBR5Gb0ghJbQL3YWP47zlvcPpWdHDVCD3sxb47ByEUdtr7t40X8vrbfIF7rsOpvZKj4iCniraEFzI6ceSwu6Fft2yg8WS4iT3L6j4+qeRUXmRcb+GITv5iLw0DD3WTZIBhR0lSf2DjhGQ0Xb2QJIjPaUaInVS770byEn+GUnyKodm4YNQCHppKd6TweE+Vlzl8zgPstHXxUFd8bdJNdY5AKK/j9K3WW58w3vYmSkKeAe0u7o/OR4RkJumEmOptwJSTIu2UQigTIZlheWPp7abGPa4bU6J9Td8mQcZdFgzJHxnMMuzYhyLy2d82hrYhdQ6xiUl5k7bxpLigsbHIyGH3RLz/rkdIBm/SXLojpVe35r/EYYOS9JET7Wd5Sct3ug8BZR8OvdNkyvdnrfUU+ZAxvBz/UAz/krnhJooVoct7+r6ijbexX9G3J0CISbeDs1JIpkUKybWgERw5Q6liYH+rj+21Lrl+Xkwhma/iSTAW/oG3+U8xq+Qux3BkGT21kNTGWimLPCuvto8/EvI3h7e5LHzPjgYG5reHWfupPPUzsTIBccGVy17n7cStDT4ne4VA7NVZyFZKI+2EmyaNjRKSzOMfcnPxqk9nSUqV5krP0JgbOMFOIRA7TzAQW7ZMkZe5fuw7DRGBk6r8+cv/bxkhJt0WckQeplo3o7FYaIsE7k+Gy5FJOgtenGU7ktoq+ffrFJKZljeTfvDkBo49uSk/FLWgH+/PvzAd0jaozoHZXmoLFMSeJoafJP96kYfdxygQAqXMiS+6jJaxkFxitu9DCkQOexw3WBbGPmotFzsoc3ojE+RKeySjAjzpoy7V+7Pmeop83CX09Wj+5vB7b+KbjlopVxCSEYGqmI7jHo1vQ9ACk9yX8f3SIoXkGoiMaTK8VWZsESGpMT/QwI7I/ytVbqaA1EIyPkAJkUeJ+h0ej0d3DdKpRkVpFS7/9ljelkIyUyGpQwsy7f0nbaf/gvrO/2QgJHX5SbBWJeclaqnNo6jqeGS7yPLrU7dvuYBIzmrMCipQCt/isyu3eRSz8GaWTtgylKZcTyUkFzMvbwb9INzHlve9pJraoDqPPie5T+t+EanHhD6QsZCMWuDTlStijRZ6q99KQlJnSddbOJe9PxnUU7RMaeohXfrLt5VEl80FQtlD5WdXtt2e9vUghaQObZaxHid1Dd/pVriiIbeSrd2S1dgiQjLdfamEZGL66S+F/LLT2+arRArJdQhJbQ6/x8XJ401cGnlAKGOLZLr8J+cl+oyN8K7VCM0Mc6GmODwRCwVDuUrn6CxaRumsp1xr7SuhzMu75n4QzcNqQnKj6lz34ZrSIkm87Pq8Zywko30mjUVSn4/Yu7Wy13bKd3HZ+5NBPUXvfVohCRB6wPCFIxQpkfHacAC181cZ528FpJCMolsJ0m8pWbhFc3EuYvdJPNtk3t8otoiQTLRI5lg6eQisbpHcDCGKNgdSSK7T2SZhn91TCMkVl7ajz0gnEtaBNod/8By2iMNP2MFkNoN00lisksv1VEIyg/KuuR/EJ5GcarfOMS9d+Z62znUWyeS9qVFS9cl1WyTTlStV3a9FSK62tJ1BPaXcV5yOtawGLBH0D3LOVhaZM3bJeH8rIIWkjsh+YiFExCFzIbI1JJfi5ltyb2SGbDIhqRd5690jqZ/E0+yRjC4lpdwjGc9LMDDH9ljYlkIyMyGp8WTkNHuXLeuuQ0jO9WPfmYOh5oru7OJ0S9tiuRd6LEuPGfn2MjdX/JJ+wsR3LgYSwuvcp199DSUiGGYySCf2HuUc1nmBR6tuI5a2MyhvBv1gVWeZ0B2udfzE/Q2pc3QOYUmOX9HHjLexX1ES+0DGQjLdftso0aXtQmo6/ZFlu5WEZPR5+t+zwtL2WurpDu7qnUn7NPXM47/2DwYfLrLiHsmJq5wfeJhw31T/qbBzUMqyS0AKyUTiDl5C5FBY6+Srqp0ZvG8SPRssJKODttAtsWiEHt2m85t+/BrAIg87/19yUgnJ0G+0VRbEl5fTem2/g+tufFN2cLyDmoKI13bC5JDOazu68Vwgct6g+caLKTv2AAAgAElEQVSDBMGoBe/iuXic0n1rWYLZGmwbIRmd1JM9aKOOI2mEZKKVJ77fLOegi3uxv6fap7aKs43nJLv14WaAlBP8k5s0781DiDxMdVfiIWQgssxXT13H2CofPkF8TssyMRMWVhHLUybpxOos2esXtNl+GnbnJLVLXOwkWjE1FnxOyhRdX8m0vJn0g9j58AqGyjMM6OMrhn5n6EwjzpHABtU5wByjbYcxCAXD4UuRcTBKJOqBUk7ziK4PrMdre+EX2g7uYpkHOIA2jutgAUrpGUaC6Szfeh7jUV/FUHkOX1D3pFTv9JrrKX5WtzC8xekBXfxM5pkaOstx581l4X+SLaxLPifmZEcp7W5YpEohmRYpJJPQHjLQsDdxS9vLzXilv1bGbLCQ1O89EOEA3wdKMCqJA492z8XBnOjelkrUC+242r+m8eCbfPSZNfx1u5KQVHaQv+sVqmpPYLdWUJQfDf2THI8tfUBybWaQpvKXIv9noKjiCHb1Y2ot+2PPU15x4tsmner5CMnly2HrF5IlOIb1YiyNkNSm6W8oQREKhoqTXOoboM99hrq6r7jQWBYWFxWNXPjeyyNNZ702vE3r8Bij/X/jy557zEcskkIpxvLVP+jvu4Sz4WM+qz9AjlAosLTwv70/MHgnGF9KrTzPb7FJViM08zPOw2+j9t5NEiOpJvhFAiNfx8JcKcZyjtgbURuPUWUqWh6HMCVBfM7ypCC7ERFXcJi20bkM01lkpr8xHKdRFFBSc4rWdhftrZ/x/rEjHCpMFpLR0yLyEGIXFWoHfT/14j5dT11LC43FuZG/n+d77xT/XyblTSu8Uon7dHFkj1BhMlPXdSfSHhtR5xFCd+hueI185VXd86Pn+O7T/S3Ckxs49uSk6NcroRHyd9FQWoBiqtfFaZzH31VPcbH+bxD3zC6g8ty/daJugZnhsxw++Bm9Cb8nzTudQT2F7tBV92pkf24uRvN72NV6rBWvUqqP6Um6d8/Pos/JXkO0v0az7MVZ9jIH236Ry5JpkEJyOdrDLmxRI5RQeEkdYn712yRJbLCQBLRJhs7UYMpXEPlFmKtsqM7vkk5VmA+folATFW0GiirqcA7cIxT04W4Ke/o1dY7GB4WEpe0TdI18FznBREExlnLI1pR4QgQAMwyppeFBy/BnXONJXSQ0hff7VlRrNW+UGFFELsaSCix1f6Gt+yb+4DYxR7LegOTjeLq/QY2cViIMlaiX+hieeMCUbzB+iowopKr5CoO+aRYC43i6vsYamcQVk42Wrn8xEZhj2ttNi/WViDfmK9guXGN4dAzf4OXYiSiioJrmH37ENz1HYOJfdLXYIkImD5P1a7o84wQWpvB269M4yoXem0xEQoNogV/pdNRQaszHWHKIWud1JoJz+Fpt1Dja6B6eIBCd0LTHeC8ex2zMx2i2cbrnP+E+ps0y2tlEtamA/KIKbE2XGZlZQJvqRS0vJMd4AHvH7fAEqs3h9/yd5joLlRVvRk5/sfF+U0fSexE+Zafrgp1ygxIRtSe51BfN+yKBsWs4ayvD741ipNTyKa5l/T4dT5i4/CnHjlVRan4PR+u3tLc6qKs7w/ejs4n5WHM680x5zlNrLgy/P6UWVNcNpubTCbvF8Kkzln0Yc4yUHDqO8/o4wcV/0/r2ERxt3QxPzMaF3Kr50AhNe+leUz+w0dLtDYf2QSM0cwt301EOlRjJMZZyyPY57pEHSaL+aetch/YHE9fPYbe8SaWllkb7UazJJ+loASaG+7gUPelHKBgqVNq7f9SFxlklmeA411tPYKl8E0vtCey297G3XGMsRWic+Oleb1JRaaG28WNqrbU0dXhSj39pPw4zqKfQA0bcn2M7VBrpA0dpciefokPad0+b+I4Tx96nqnQ/NY4W2ttbcNTV0/y9dLZZCSkkUxFistNGgRAIpRR1aOZ5Z+iFZOOFZLZYxWtb8nRsyjaXvLg8k7OiJc+ctEJSstmRY3waQnfpO+ukY3hy2/hEbDRSSEqATdrmkhcXKSS3JhmF8JFsJuQYL8kWUkhKgE3a5pIXFykktwxaKERsUXy2G2vuBoahkjwz5BgvyRZSSEqATdrmkheXmLOIFJIvMuEzundgijjChB2zLLjGZZu+aMgxXpItXhwhOfczjuK88ObzZSE0JE/LpmxzyYtHaBrf4FUu2F+POIsYqWr9F9PbyHFtK6EFbnGxrpKi/J2UWI5jV5OcgyQvDHKMl2SLF0dISrKKbHPJhhCaxpdwdn3k8k3LjewSyXNEjvGSbCGFpASQbS6RSCRbGTnGS7KFFJISQLa5RCKRbGXkGC/JFlJISgDZ5hKJRLKVkWO8JFukFZLykpe85CUveclLXvKS12pXSiEp2V7INpdIJJKtixzjJdlCCkkJINtcIpFItjJyjJdkCykkJYBsc4lEItnKyDFeki2kkJQAz7DNQ7PcmXgoYwpKJBLJM0TO6xuANstYz98423dPBuXXIYWkBMhym2sBxgfdONX/F7Mxl1xrN7PZS03y1ATwXbBQaHiNE3335YApkWwB5Lz+dGiB27hsZZhq2vAGFle/YRshhaQEyHabhwhM3+VW6yFyhJBCcrOj+emsKUSIPIodPzP3vPMjkUieGjmvrxeNkL+LhlITlacHmQrJT+tknpOQXGK2/2N2KwKR/zrNw1JWPG+eSZt3HyV3SwnJACNf/pWe2U18jnTwJl829WZY3xqhR6N4BkfwyzOyJZItgRSS60Ej5L9KXXEZNtdtAlJDpuQ5CckQfldVJP5QPhWuiSynJ1kNKSQzZZHZoVMU5x2le7MKSe0hQ2oZeVuiviUSydMghWSmRESkaQ+VrV6Czzs7mxgpJCWAFJKZoRG656amQEHkblYhOc899xEKtkR9SySSp0UKyczQZq7TYCqgWB1kVloiV2SDheQ8U8N/p8n2FiXGXIQQKMYSKiwf0tzhwR9cQpvqos5kSB0dXSnkwAUf4W2sGnPDpynLVxD55Tg8MxC6S2/T25jyDVJ8bjBPN8gsEfR76Gj6AIu1nsZaCwctJ2jp+U23FJAoJB8HfqPHeQyzMRehFGKu/Xb5BmZtlrGeFhpq7XzRep6v7O9Sbfucy77HCQ4gWnASb+9FHDU2Wn2P8ff/FYupgPxSlb6psH+4FviNnpZGao9/TuuFL7Fb/oyt6Tt8KTZNa8G7eDqasFls2BtrsRyswd5yjbHIb7WpAc7aqyhSBEIxUVXfiKqe5EzUky80yXBHE7aqCsxFuymqeJ+mjhvL9tasJd/p0AK/8n1zPXWOFtrbzlBb8SfMZgutviAQYmqgFXuVCUUIlKIq6lUVVT1Ln38BtCDT3n/S5jjC2623mff/L6ctr5CfH3Wu0QgFxhlyqbzVcC0uQkOzTIxcx336A8yvf41vcZbRziaqTYb0bQjExoX3bdTWW6mqtFD3xTf0+qT3vkTyrJBCMgNCY7iqCzEcbGN0QarI1dhAIakxN9xEsRIWhOYj9ajqx9S+sw+jIhCiCNUTgKAPd1MDlpL8iIDMwVjxAaqqoqqnODvwe0QkJFkt2wfofr9IWjGzxNN8PPh7T7G/zE6Xfz7yt0cM2IsQYpduSUAnJA81cMpWx1ed/XiGrnHB9gqKUDDUXGEq+s5qjxlxHsKQY8E9GZEb2iTdtj0Iw7u4/QvAIjOeC6j1EVEnyvjE2cyJL5qxl7+EEEXYBx6hBW7irNxFTrWbycjztYdd2ApyMBy+hD82TmiE/NdQ979OXdediMjRmBs4wU6hYKg8hy8Y+fFsN9ZcsdwiufAr7W9XofZPhu/XHjHcZEYReZgarjOjseZ8pyU0hqu6mIOu8Zig1mYHUYsP4PRFF2BSW4C1GQ/n1ONUFeUhRC7Fn3zJX098zlf21zEIhf/H8iX/x2mjJF9BJNwbxO/9Xy7Z96EIgTDZcKjHUduvMZSuDcOZZaq3AdPOY3Q/XAQWCQz/hVJF/xFp1uVbIpFkAykk10qAkeZyFIMF17gcl9bCBgrJabqtxvDk1HyLhdjflwj6h2g73kTPTNRaEcCjriYK9UIyl5dL9lKgmKh2tNM7/Av+gLRlbCTr/XgIjbuoNryMrXtSZyWMf1Qo+9sY10AvbJTSvzCss1yFRV3SMvFcP/adCkIvJAnic5oRYifV7rvx9JZu49ybixA7KXfeIghoQT/emxMEtKgQFAlCMnaP/vmRr9ACWxcP9WJo7mccxXkI5U3axp+E/5ZSSM7hcx5gZ9L92qSb6hyBUF7H6dP5QK+Y7xVqfbyN/UpSHRBi0u3g7CpCMrEeFfLLnXiDGmhz+L1eJgKLwAL3XIdTetgv+ZzsFQJRcAT3vfn4f6Rsr3jbJjwn+kGQ3I4SiSRrSCG5FjSCI2coVQzsb/XpdIxkJTZQSN7DXVWAEIIc80l+8N7hUVqPz0yFpEAopTT0+uVSWJZYX5sHGHaUIHaeYGAuSQ5oc/i9N/FNR8XGCsImJqj0lql5/ENuLl716URVVADlstd5m6UV79cR8jPU0c5V/ZL4sns0ngw3sSelNXCJoP82P/um4/0vlZCc68e+08D+trFEcRRLKz/x/1bLdxqiwlQxHcc9Ohu3SgYmuR8T6GsRkkn1qCtvuntjQnKvE5/+xpRliYv4xOdEn6+w094vwwtJJM8AKSTXwIIXZ9kOlOImhpPnNElaNlBIxq0Y8SUrA0XlloT9ZWEyF5IJ1iTJhrOuNo+Kh2RRkfrHGQpJHdE9faf/gvrO/2QuJBMeNYm39xtON3/CO4U5unui4mqNom6ZkIwK0TyKqo5Htmosvz51+3iyjnwnFiK6XC4Qyh4qP7uC71Hyt/NmEJLx5yS/v+HnSCEpkTwrpJDUoc0y1uOkruE7xmN71xfwu9/FIFdKMmZjnW1Cd+lVD2BI4UijmBrpX/fStiBf9UhrZBZZV5tHxVS2hKQ2h9/j4uTxJi6NPCC0XoskEQcal4PjJy8xMrOQ4p7o1oz1Cslo+YxYu6dXv3+N+U5L6AHDF45E9lgKhOEAauevaZ2bno+QBG3qCjUGRbevFeKie0/SlgiJRJItpJCMEl8pEcLIYfdEeAxauEVzcS5i90k80hqZEVkI/xMiMDFMz6VzNKvHqIp5aOewx3EjYo2RQnKzsa42T7MvLjUZCsmos01B1EkD1ru0HXa2MSbufVx2T9RBaI1fo2mFpL6fr1YlTyEkww8g6B/knK2MfCHSOzc9RyEZdcYqN+zAVPcdo4EQWvA/dNYd4KDzpgzwK5E8I6SQ1BG8SXPpjrC+KDhG98MFZvs+pGCZj4dkLWygkAwxfXNoeTgVbQaPWhpusAoXfkAKyc3Hutpcu4u7eidC7KDM6U3x8s3jv/YPBh8ukpmQ1Hgycpq9SvJv1yMk/2Ck+TWUZEvhsntCTLot5AiBUubEtyzkg0bIf52OwamwyEy7tB0dmFKEwdEeM/LtZW5Gv3bXu0dy4irnBx7q/jLPVP+psCd0bL/qZhGSAHP42uqpbf4ap+MkDmcHfTL0j0TyTJFCUo8uyozIobDWyVdVO2UEiXWygULydzotuzCUq1zxThKMeQDEhWRebR9/APGJTG+p1Ag9uk3nN/2RcCxSSD5L1vvxMNVpDW9lMLzF6YG78XZnnqmhsxx33lzdQrZMhDxhvO1NFCHIOejiXuyZEeeeTISkNkbb/nyEKEgIl8OTGzj25CTcE1uGFbuoPN2vOx5QIzQ1yJnj5xhJDv+jt8Y+uUnz3jyEyMNUd4VxvbNZ6AHDF+qp6xiL9+N1CsklnxNz8t7CqKhPISSX7y9+hkJSe4zPpXKyRy5hSyTPEykkk9AeMtCwN3Eb3svNeFOFwpWsyMYJyaVfaN23I74n0ljCGxVvYC6KLG0r+3DEztTW71EQCMVIyYGScLzJmIVHCslnyfr3xd6hq+7VsOOHyMVofg+7Wo+14lVK667ij21kXkHYLBN1cYukUIqxfPUP+vsu4Wz4mM/qD5AjFAosLfxv7w8M3pnXiZgSHMOBpAxGLZICpehdvuq8Tp/bSUODSv0bBQixG0trD73f/cidpXn8XfWYlGgfLueI/RPs1gpMpfo4megEqpGqVg/jo9f5+ssufrv1NZUGRXd/I2rjMapMRVQmL+WumO/0LPmc7DUcpm1UJyUXvDjLXuZg2y8xy3A4TJBAGN6mdXiM0f6/8WWPHy0mJNMtw69DSKYQ5gBLo63sUwooqTlFa7sLl8tFR2cfQx4PHhnGSyJ5ZkghuZxY6DkhEELhJXWI+dVvkySxoQHJQ4FxPJfPolqrYgJSMZZyyPY57pEHiUJQm2ToTA2mfAWRX4S5yobq/A6Pfy5iuVgiMPRpZFLfxZuu/6SwnEg2iqcaZEIPGHF/ju1QKcYcIyWHjtLkvsVMVERqAcYHO1ArdkUcQypR26/hnZ4jMP4j7uaaiNNIHibr13R7pwhp0VNTCsgvqsDWdJmRmQW0qV7U8kJyjAewd9zCP/q/uL6I3q9gqDjJpb6bkXiI0eR/pdPxNqb8gvApM+5bzIQWmOo7RbkxH2N5Ax2x0EALzIxcjpzOlI+x5K1Y2oksEvB+S625kBxjOdbT1yIWyEUCY9dw1lZSlK8gFCOllk9xefTW2kUCY6vnOx3axHecOPY+VaX7qXG00N7egqOunubvf00UqtpjvBePYzbmYzTbON3zH/5vYJTrrs+xFOXF2+JSH8MTATRAC4zj6f4msa0u9TE88YApbzct1lci3uKvYG25imdiloVpL90ttsi7qmtDCDvgOd6iMCEAue4yvEHT0ANprZRIsowUkqkIMdlpoyASYlAdmnneGXoheU5nbUs2G7LNJVkhdJfepr9wefwRj+77mfDdxOPxhK+Bbi611LP/T2cYeSKlpESSTeQYn4bQXfrOOukYnpSrnutECkkJINtckgW0+/Qe/xP7UjpiRZmm+8Om5QHtJRLJhiLHeEm2kEJSAsg2l2w0US/2lcMpabMezn45EDmDXCKRZAs5xkuyhRSSEkC2uWSjiTtMKUXv8lX3Le48CkYEZYjA1BjD3zupq7vAyBr2hUokkqdDjvGSbCGFpASQbS7JBvP4B77GWlKQ5GSTi9H8Aac7vXGHLIlEklXkGC/JFlJISgDZ5pIsogV5dMfHsMeDZ9jHxHRAbmqXSJ4xcoyXZAspJCWAbHOJRCLZysgxXpItpJCUALLNJRKJZCsjx3hJtpBCUgLINpdIJJKtjBzjJdkirZCUl7zkJS95yUte8pKXvFa7UgpJyfZCtrlEIpFsXeQYL8kWUkhKANnmEolEspWRY7wkW0ghKQFkm0skEslWRo7xkmwhhaQEkG0ukUgkWxk5xkuyhRSSEkC2eRiNUGCK6T8285F9IQL3H/CHPBAmA2SdPT0aoYCfiQfzzzsjknUix3hJtpBCUgJs5zZfJDDxL7pdf8X+zj6MihmnL/i8M5WIFmDC043rqwbeKTWi7HXiW3remcouWnCc6+eaUO3vUGIopPSdRlzex6xZC65YZwF8FywUGl7jRN/9tT9z9UQJ+i5QXbiL0hP/ZGoLCFct8F8Gr5zHYS3HqBixdk+v7cbgbS5U/w+GUpW+KXmO0WZg+47xkmwjhaQE2M5trhEKPMD/2yWsBQpCbEIhSYjAtJ/f3O9TIARiqwvJoJfWyjJs3ZNoLBIY+ZpKg4Kyv43xNYuzFepM89NZU4gQeRQ7fmZuwzIeYqrTikEIlOImhue2gJIMBZj236C1sgAh1i4ktakr1BgUhLIPx/BsljMpWQvbd4yXZJsXREguMdv/MbsVgch/nWY5MG04m6/NnzFLt3Huzd2kQjLMks/J3uctJIM3+bKpl+y9gQv43e9iSGiHJYL3x7kTyNyylbrONEKPRvEMjuAPrrciA4x8+Vd6ZpPuD80w6vmJm/65DbR0roZGcOQ8TT1rtBZmzDTdVmNGQhIWeDT6LwZv+gk+i4rIer988dn2Y7wka7wgQjKE31UVCXyZT4Vr4nlnaMux+dr8GSOF5OpoDxlSy8izdmdxwo6Klo1ph+zU2SKzQ6cozjtKd7KQfA5os4OoxS9nIPIyZT1C8hnyTPrli8+2H+MlWUMKSQmwGdv8GSOF5CrMc899hAIhyM3mhL3B7bDxdaYRuuempkBB5G4CIRkax12zO8sibzMLyWfUL7cA236Ml2SNDRaS80wN/50m21uUGHMRQqAYS6iwfEhzhydpGWmJoN9DR/OHWCpfoyhfQTGWcsiq0trt41EovB6iTXVRZzKkPpZHKeTABR+b2cf2RSHzNl8kMPEzVxwHMQihm1QjzivtKhUGJWnyCRGYuMV19xmOmatx+v4gMPo9juq95ItcjObjXEzlUBF6iK/7HKrNRm1jPdaqt7DUNdPhuZu0bLZEcPo2vW2fUfP2eXzz9+g/XYMpf2ei84P2BxPXz2G3/Bmr/SOslg9x/tCBWpxCwIQmGe5owlZVgbloN0UV79PUcYOpUGIuteAk3t6LOGpstPoe4+//KxZTAfl6Z4M1Piv8vHGut57AUm3FbrdhqXXyg/sTijMSRYsERq/SXPcRjtZvaPuqjorS1zC/eS7x/lXzFWJqoBV7lQlFCJSiKupVFVU9S59/YZU8aIQe+ehuVbFZ62i0W6mqtFDX/Hc8+uVf7R59Z06iNh7BbFAQ4iXM1hOoqop6sgPvGvYbrr3ONEKBcYZcKm81XEsUH9oso9+foa7OQWv7Bb6qPUipuZw3W//NEqBNDXDWXkWRIhCKiar6RlT1JGf67oXLEpplfMiF+paqW/ZeZ7+PjpFNH2Cx1tNYa+Gg5QQtPb8R0ADtdwbO2qkqykOIPIqqjofr60wf/lWra43tAiQKybtMDXegWvZhVHIxllpQL91iZtn7MIVv4BKnjx3GnrzkntF7cBdPRxM2iw17Yy2WgzXYW64xFlhkLf1SC/zK98311DlaaG87Q23FnzCbLbRu0o/FbCKFpCRbbKCQ1JgbbqJYCQs885F6VPVjat/Zh1ERCFGE6glEfjuPv6sekyIiYrOcI/ZPsB8pj/w2D9Pxa+GJP+jD3dSApSQ/IiBzMFZ8EB4w1VOcHfj9Ge5F2rqse5CZ7caaK1JYZ5KtGBoLfi/XLzVQogiEeJVjjk94X/2W3qEf6b1wNNwfDFY69V6eoTt01ZWws6aD8eiHiPaYEechDGIXlc6bkUn1AZ5zp6iPTCqi+COcf/2UL776iHKDgth5goE5LXJvFbsrv2YksJj0PJEoJBd+pf3tKtT+SUIA2iOGm8woIg9Tw3VmNIBFZjwXUOsj4kKU8YmzmRNfNGMvfwkhirAPPFrjsyLZCdzEWflKvGyxv+0KvwNrEpIaoXEX1TvfxnUvKvYiS7Kv6O5fc76WmO0+Sm5Glh+NkP8qdaYiajp+i4n+WFkMh3COJAmodVok11pnWuA3epw2SvIVxLKyBBl3Wdh50MW92EfHQ4bU13jFeZtYlafq89osYz1OrCUF4fRi/7fOfs88/t5T7C+z0+WPhtx5xIC9CCF2UdnqJVw767EWZtou0TR28saRI1S/e4rW9nbamq2Rekx+H76h+Yvj4fcuOV9r7m8aIf811P2vU9d1J/xbNOYGTrBTKBgqz+ELaqzYL0NjuKqLOegaj5UlvA3gwKZddcgmUkhKssUGCsnoYJNLcfMt4naKJYL+IdqON9EzE564Nf8lDhvCA3lO+V+5FZ3QWeBh3wl2CxGfgAEI4FGL5NJ2Fsm+kIwQEwq7qXGPE586o5PkTqrddyMDf4CR5nIU5U3axp8kpqtN4D4cfv5h90RsoogtZeZX4vQGCPe/f3NzIoDGIjP9jZhyXsfpS/LVfXIDx54cnYCZw+c8wE5bFw/1Im/STXWOQChJz4iVayflzlsEAS3ox3tzgoCWwbO0afobSsgpc+Jb0EssjSfDTexZs5B8wnjbmyg5FtyTOoGi3cX90fnI/ZmUcR1CMniT5tIdKb2tY2OA4V3ceqvmeoRkxnW2wD3XYXKSy6KN0bY/n5xqN5MJ9fEdH51dRUhGf3vPxcGcFP+XUb+PfAQYXo54rsfLE/1Yj9fpOoRkxu0STSOPYnWQ2dg9iwSG/0KpIlJ4Z6fKVwb9LTSGq7qQgqTfMvczjuI8RGxMSN8vtfE29iv6egUIMel2cFYKSYlkw9hAIXkPd1X4azzHfJIfvHd4lNIjcp6x1gMRUbib2r6Hif+9eIvml3MQQrDDPkD4W1wKyWzz7IVkslAI4nOaESKXvVHrz2wvtQUKYk8Tw0+S7c6LPOw+RoEQKDoBseKeuAUvzrIdqf8vOV9z/dh3GtjfNpbGYpaf+H8rCaA1P0tjweekTNHVgf7nGe33CzHptpAj8jDVuhmNfawtErg/GbbaZVTGTIXkErN9H1IgctjjuMGT5P/WJum27UGIHZQ5vfEPz4yF5HrqLE1ZtLu4q3cilFepdf8Ss2yiBbh/PxCvoxWEZNr/y6TfE2DYURK3oicUdw6/9ya+6aiVMlMhuZ52WSmNqBBW2Gnv14VSSnFPBu9B+ANAb0yI5z/ov83PvumIGF9BSEYEqmI6jnt0Nm6VDExyP7D9NkRJISnJFhsoJONf+fF9jAaKyi26PS0QH2AEQvwJtXMQj8eju75DLckL/3+FCz8ghWT22XxCcnULXNjiIHTWiZXFVvT3KYVQUr7Caev2naW4PnX74hNx2nJpGTwrYkVMIwoydRzRAh6aSneEt48UvsVnV27H9h5nli/IXEhGxJBILfBiFlMhEi1jGQvJ9dRZurLoLGwil8LKz7jie8iyoEPZFpLR366pnTMVkutpl5XSiNdlooVz+daWtfe3aJ2spQ+s0C9jy+YCoeyh8rMr+B6ttqd36yKFpCRbbKyzTeguveqByF6zxEsxNdI/swhM4KrIX/b/y68CyppvRL5wpZDMNptPSMYniNQWSV3aumelF1vR/VVrEZJzkbQzWC5MW66lDJ4Vte5sjJAEjYOF2PAAACAASURBVNDMMBdqisOTqVAwlKt0js6iZZQvfTnWKiSj7Z/G8qVvX315MhaS66mzlcqywMzw36gpinzMipcoVzt1Fl2yLySjz8iKkFxPu6ycRuo6Tr4nk/6WSQioVfpl6AHDF45E9i8LhOEAauevcWvzNkIKSUm2yEL4nxCBiWF6Lp2jWT1GVczjOjpw6S2SVbj8awkyLIVkttl8QlJnkUze57dC2unFVjrLSap8RYVkusk2BasKybU8K1pn+cuX/1Ys2ypoc/gHz2GLOoIYqmj1zWZYxvVaJMWyPYcrPi9jIbmeOlu9LFrwLoPnjkacSfTOHWRfSM71Y9+ppO/3KcufqUUyk3ZZi5BcbWk7k/cg1b7RdKylXy4R9A9yzlZGvhAkOittH6SQlGSLDRSSIaZvDuFL3nuizeBRS3VL1fGlk5R7JGP3zRP4IzqISiGZbTafkCQ+oabcKxVdqlYw1FyJhfZZSWwtjbayTxGIgmN0P0zqpymXttP8FsKe3t9e5mZ0D9uqS9treVaQ0dY3UISy3MlglbIt5wkT37kY0LdJ6D796msokUl/JqMyZiok4xbglHv9YuNAITWd/rXtNU3J/DrqLN0eyf/y3fkfdWXTCE1dRy3dkdgHsy0ko3s1k/eP6srsv/YPBh8ukrmQXE+7rJRG9HlJ7Zh2aXst/W0hsr83cf+zPs2Q/zodg1M6y3qKPZITVzk/oJ9f5pnqPxXeupCy7FsbKSQl2WIDheTvdFp2YShXueKdjMf30wnJvNo+/gC0h13YCqJe22e4MaMfKsNe3hfrytkX28MTHWz1lk2N0KPbdH7Tv4aYaZLVWPcgExV7yZ7VUceWpxGSzDHadhiDUDAcvpTUzpE9uUo5zSOB+OPX4mwjdnGw7ZfECTo5X09u0rw3HJ/PVHclHnoIIstl9dR1jMX3z60kgNb8rKjjiEAYDtM2muhZnpmQDOJzWpYJ8LADQsR6lFEZ4xN2aktWChZ+oe3gLpI968MZGcd1sACl9Awj+mCg63a2yaTOVrCGmhuTBEbUaSmFkExlMdwQZ5v4md3C8BanB/TxUueZGjrLcefNpPA/a7HeRci4XVYSko/xqK8mWmzT3ZNBf4ud1S12UXm6XxeDWCM0NciZ4+ci+UvfL5d8TswJVlLiIl0KSYlkw9g4Ibn0C637dsT3RBpLeKPiDcxFkaXthPAQi8wM/SUSZ0wg8k1UWOtRG2uxlBdFlh/yeMV5OxJsXPcVLQRCMVJyoCQcc3IznC6xBVj3IBMJvaIIBUPFSS71DdDnPkNd3VdcaCwLLwtWNHLhey+PNFaYUNM4AYTu0N3wGvnKqwnx5EL+q9QV79P9LUxMOKTx9A6MfE2lQQl75l66xXRwKRxM3N1AaX5k4mr+nqGb/8F/K/JbEY112ojaeIwqU1FCvMJwwtFyleAYDqRPd7Vn6WJaKqbjXPJOEtSWCE7fwm1/LfxuGN6iuevHSEijdATxOcsxHGxjNGbRiYiugqjgyiBf6JybDG/TOjzGaP/f+LLHv0IeNEL+LhpKC1BM9bp4iOE4ssXF+r9FiIVhSlWP6ZLJtM5WWlbfnfSREQ5ZU6Cvx0iYICGMVLV6GB+9ztdf/jMsYjIWkun7fVfdq5GVm1yM5vewq/VYK16ltO4q/pjTVNSCqGCoamV4/Bf6v26hZ8Ul8UzbJbrUXEDluX/rRO0CM8NnOXzwM3qT2zGl+Mykv6WJNWytwFRqT8hfun656HOyN/njYsGLs+zl5R+S2wApJCXZYkMDkocC43gun0W1VsUEpGIs5ZDtc9wjD5K8HzVC0yN836pirToQPglHMVJSYaHui4t03/QnnlqiTTJ0pgZTvoLIL8JcZUN1fpfiFAbJeniaQUYL/Eqno4ZSYz7GkkPUOq8zEZzD12qjxtFG9/AEgZBGaNpLd4stMjnkYbJ+TZdnnMDCFN7ur7Gawg4OislGS7eX6ehkGTuJ5k0qLbU02o9itbfQMxYP6YE2y9h1F19Yoo4lu6hQO+gbnkjaWL9IYOwaLfYaKkoKMRSZqap10vPbT1z4c7jvdXl+ZTq4FPuts7aSonwFoRgptXyKK+FEnUUCY/+L64uayIb+qKC+yUTCNo+1PEtXlp4W7JYKSowvUWSuptZ5jd885/nzOx/yRdsPeH6ZWn5fAk+YuPwpx45VUWp+D0frt7S3OqirO8P3ulAomeXrMd6LxzEb8zGabZzu+c8qeYjcFj1xpvJNLLUnsNveT4rkAGgBJob7uKRWRpz1FAwVKu3dP+pC3ayUyBrrLDCOp/sb1IpIoHJDJeqlPoYnAmjaf7l8opZjVa9hrjlFa/u3tDo+oq75aqKzDYsEvN9Say4kx1iO9fQ1xv/vYyY83bTH8h/tf+PMTK2z34ceMOL+HNuhUow5RkoOHaXJneIUmcAtLtbux5hTiNn6JT3jf6xpTFxTu8R+exdPRzN1ljepqLRQ2/gxtdZampadWBYlnRUzg/7GAjMjlyMnpeVjLHkLW9NlRmaSJGCafqlNfMeJY+9TVbqfGkcL7e0tOOrqaf5eOttIJBvJC3LWtiTbyDaXSCQbx2Y+n3t7Isd4SbaQQlICyDaXSCQbSSYhfCTPAjnGS7KFFJISQLa5RCJ5SrRFQotJDjrpYsBKnjlyjJdkCykkJYBsc4lE8hRoE7gPG+POOwtenGX/Q7VrbPnJQJLnghzjJdlCCkkJINtcIpE8BdpjvBc/oqKogPySd6i3n8TZ89u2dGrZrMgxXpItpJCUALLNJRKJZCsjx3hJtpBCUgLINpdIJJKtjBzjJdlCCkkJINtcIpFItjJyjJdki7RCUl7ykpe85CUveclLXvJa7UopJCXbC9nmEolEsnWRY7wkW0ghKQFkm0skEslWRo7xkmwhhaQEkG0ukUgkWxk5xkuyhRSSEkC2uUQikWxl5BgvyRZZEZJa4Dd6WlWsh0oxKgKhGCk5ZMc9Js9c3azIQSbbhAjcucuD0NojNGvBh9x/9CSLeZJsC7QgD+4/lifMbHPkGL8BaLOM9fyNs333kLH242y8kAx6aa3cFfbkUfZQcewE9iPlGJUiVE/gqTIryR7rb/MAvgsWCg2vcaLvvny5ElgkMP4TV859htVciJJ7lO7ZpZV/P/Evul1OVGsFRfmFWLuns5S3JYIT1znnULFbSjEY9/FOQwfewGKW0tt8aMFxrp9rQrW/Q4mhkNJ3GnF5H2+JPqwFxvF0u3CqViqKDORau5l93pmSPFekkHw6tMBtXLYyTDVt22qcXAsbLCSXmO0+Sq4QCJFHqfN25Ct4kYD3Gtcn5p8qs5Lsse421/x01hQiRB7Fjp+Z29hsveBohAIP8N9qpTJHIFYVkpHf3xukuWwHQhizJCQ1gr5zVO6ppfvhImiPGXEewiDy2d82tiWE1KoEvbRWlmHrnkRjkcDI11QaFJT9bYxvhQoIBZj2jzPUvB9FCCkkJVJIrhuNkL+LhlITlacHmcpgVWm7sMFC8g+GHa9E4grlU+GaeNr8SZaxxGz/x+xWBCL/dZqHN2Z6WH+ba4QejeIZHMEfXEkkvcAEb/JlU+/6J+LZbqy5axGSUabpthqzJyS1CdyHjYi9TnzR7Ghz3P/PPQLbYpBcwO9+F4Mw4/RFt9ssEbw/zp3As1oAXuSRtwuXqwvvo2xZN+If9lJISqSQXA8aIf9V6orLsLluy7Pj07DBQjKARy2SQjKrhPC7qja8juUgkwbtIUNqGXlPMxFvNiEZzY9eSG4rovWrF5LPmiA+pznLeZBCUhJHjvGZEhGRpj1UtnqRHh7pkULyhUMKyWfHPPfcRyh42ol4kwnJJZ+TvWIbC8ml2zj35kohKdlWyDE+M7SZ6zSYCihWB5mVlsgV2TghufQf3O+9Qn7y0TlKIfu/Gg7vnZu7QXNZAUIUUOr4iQDz+Hv/QrXJgKhw4Y8+S5vD7+mgyVbNGyVGFJGLseQAVVaV1m4fj/TLb9odOt9/dXm6uksxVnMhYbBeIuj30NH8IZbK1yjKV1CMpRxK9XxmGWl9n8qKCirfPc/I3BKhqRu41HcpLzIgFCOl73xCm+f3RK/I0CTDHU3Yop7rIhdjSQWWumY6PHcJ6pPQ5vB7/k5znYVKcxH5IhdjyVtY1XN0+x7GnqtNdVFnMqQup1LIgQs+1rtI9lRL24FxhlwqbzVci09WoQB+n4euNgfW9y7yy9ICj3zdOGsPUWJ8iaKKj7jofYyGRmjqBh1N71NRVEB+USV1F2/plhA0QgE/Ps8PtDmO8l6bj6XQQ3zdTmoPlWI0mKio+zay+XmeqeG/02SroCi/QJcGhB1ZfuaK4yAGoRd1EQeXdpUKg6ITbyGmBlqxV5lQhEApqqJeVVHVs/T5FyJZm2Wsp4WGWjtftJ7nK/u7VNs+57IvyWFDLyQf+Bl2fYql1IiiGCm1nOLSyIMkj9oVhGS0X1VVYC7aTVHF+zR13FjTvh3N38cZVaXRag7XgcGMtVFFVVVOury6/a0LPPL10Kp+gLX2BHZrNZWWD2nu8CRtX1giOH2b3rbPqHn7PL75e/SfrsGUv5PSE/9kamGj+sAaCD3E130O1WajtrEea9Vby9817R59Z06iNh7BbFAQ4iXM1hOoqop6sgPv3AoJan8wcf0c9nf2Rd5nA0XlFuzOy3j8STuDQ7/jafuYKpMBIQyYqj7WjQ9RAZlqrMplr/M2a9L2sTHyXaz2E9RaqrDYW+gZm9X1vZWE5CKBsWu0NNRx/IsWLnzVgKX6KE2Xk5fvFgmMXqW57iMcrd/Q9lUdFaWvYX7zXMJHiBb4le+b66lztNDedobaij9hNltofW5CXZKMFJIZEBrDVV2I4WAbowtSRa7GxglJ7SHDFz/FUpIfGRRzMFZ8gKqe4uzA7+HBze+iIjpoVlxguLuOwti/I0IydIfuhtciwjAXo/k97OrH1Fbtjf2tsPocI1GvKe0BnnOfhScD3RWbLIVAKOW6r/55/F31mJSoyCzniP2TiGd52EnIdPwaU7G+M4GrIlqmQ/ytt5WDBS+HvdEtpXEBa3gXd1RgMMuwY19YgBjLOWJvRG2s5Z1SI4oQiHwVT1Q5hO7QVfdq+O+x8tZzxFwY/pvyKsd7I97QQR/upoYUdawm1vM6WE+ba4Hf6HHaKMlXEAmTVRC/d4j+Nlu4fU0f8FVzIyfb+xgZm2C0/wyVBgVRYKP92nnUpr8z4BvHP9qPs7oQIfZEnCCAhft4B3tpsxYhRC6m2i9obmiive8WY/d+pf/0WxiEQoG1jWvnHDRdGsA3cZfRn85SXaAgCo6FHUqipLUOphJvK0zEUQeVHAvuyUhjapN02/Yk9QV9mvs5cvQd3nW00O76G83WsnD/UUpo6J/WtV0aIbnwK+1vV/H/t3d+P1Hc+/9//wFzs5dekJiQTbggMWbDhcY0cIFpQ4LmNIRYG4JNDZi2AdsU2kbgRBebMqYV0hZt3SjEHrB1v5yv6JF6QI/YAC3bukSIZ9vi+YC6LSAoWfmALMzjc7Ezs7O/YBeXyrHvRzIXLLM7M++fz3m9X6/XW+2bCIkS7SHepiIUkUFe/Q1mkqz8lS2Sev/IfoeOscdhET58hlK7DXvpmVDf0x7gOfsJR3ShLfIP4/ryYz4/dZhiu4LIfo/2739ITxtYjeBdumsLyK7sYMwQumYA0VZKXUOR4ihli2SQia5qskQGuRVNtLvduN1tuNR3KHLsxNn/0DxTCwzhKt2KUPIoP97C+fMtHC/PQzH78RKB8Vt4PH24a/IRIp8adx8ejwePx8PQeGD1Zw7e47q6j121V/AbLxBzfTizFYS9jFafkR0jUfs16nML5Z33zDqe7jlElnBwoHNc/0wjOOamPPsN3PcXze/ODn5C/ouWthO8g7s8n33uMfPetdkB1Py9z9DiK4lGCslkCTDcXIxir8A9JttvMvyxS9tWIbnjJQqyNpNbfhz39Zv4/AGCphN86I2/+ITXMgEs4O86pAvPVSKEg3dwlzv0+9jKvvafMYZBzX+BA/aQ8LEVf8ktM4x/keneo2wTAiFyLZODVUhm4Sis46IxwWq/0VWppzqyTvqGcBB/oXnkcfi+tDn8nnbq1GvM6NdM+LzBCXqdO0O/nX2UftNasj7uA2uv80Xuuw9giye2jHKwldEe0SEf0u/MRQgb2+r7LMsGGk+8TWwXCtnOPkv9hidEW+k5xqzWN2MCFTup75+2TMIBvI0FUXVJ+oSkcV2rkDStTdmWCdpyTWUX6qDlHk0hKFDym/CadRzvXubwufaSXd3NtNUgP9FJuU0glFdw+ZKLmU8sJDXmh09SqMSL3jbaqoL9wAX8WtRvZZbiGgkQsvb/OyyI0tYGEqEP+sprtI9F5dw0gooixBGpC0ntDu17MhHbm/A+iSwVbX6aB4+NMUR/gYyp55CfrW3nCYbN7691aXueMXcF9ugXJPPl1Vp3idqvUfaR7dSoS1t5JxMawBPG2l9DiWjjgHaPzsNfmW1HG2tnjxLV5gky0dnIaSkkNwxSSCaDMQba2dPqY3H1L0h4lkJSZJBff42JiGVqfcAWAmF7h67pqIXapVs077DFEVeR9/BvVwk2IRBCIauyk/vmNRa407pXv/42anqnE/7+Zmc/oWRFViEZPcEmeN7JTsr05a8itYuRuzORS9nm8/5C666M0Pcz6uh9HHnS0kgzO4RAiB04+x8lV8ZrJB0pnxIKyRjRZkyisUt58UXOCtdIKAoSXCNdQpIF/IOdnLvis7zspHpNLELYKnjj3MtcH85se6zAM58/+dQ9iYXkA3prdiBEAY3e2Jyv2nQ31VlKhGhd1d8ybW0gAbPXqclS4oq8sJVNoOxy4TOWqNYqJGPEW9Rpk5eptNvIihL7YIgta7muUUg+uUnj9k1xRbY272fkx5+ZMse7RO1XI+gfpONcDz5LPrzYcg8y0VmBTWSQV9PJqHnuEoHfJsx2b7zMKHl1dI6Gl9a1wAS/yXx7GwYpJJNgcQTXrs1RL/aS1Xh2QjL6LRcslryo5V8Tq6iLNwAvMdPXEF62zv+EwVnrQGZM0CFrodo1YC4phY5LqAUZkUvtEdeMTqqe4Hm1Mdz7siL9nzJzKa44SsvVX8PCw/q8BSpdEffiwdOlUhBTllJIbgwhaUGbZ2rkX7Sf+Az1rRdSE5Lmda1CMPpeDCtdBrlldTFuHMbxcaePZPbBSSjUntykcbstsbgxX/TC9/pshaRRLonPDQk4gbBaLNe8tK27qqinONf1HcPjsxbfVo25/qNki80UqhcZjO7LfX+jKmerxWq3NiEZKptkfSmTab+Gn+uXNKtvhFZ8LGWpBTw0FW4Ojac5r3P88u0oH3IiLOtC2U7p8cv4HkpbzkZDCkkL2ix3rrqorb9kWeEyVl2ireuS1Xh2QjKeUFzt/yuKOo3g/U4qsxTdZ9HqKxTv+ysdWexqvqm/8a9BSKIR9F9DLd4S57ct/mwRFtoVjoh8kVJIbhghqc3h97g5VtekB8yswSIZ9zvR92LcR3qiuBMKNfPFJr5FMnxf4Xt9tkLSsgFCXIuk9Zks7WMtUdvaI3ydKqU5myx9cwvF6jXdT3GlIBrjsD7rWoSk8bzpEJKhgEP3MSfHLtxiJqglKHeN4IyXtsp83Y9bwV6s0jU6GznRBh/gbXuXXP0lXtj3onb9IvPubSCkkDQwXvoEEW4vi7dozt+E2HYMj7RGpsTGEpLTXVTYkhWSu2gesSzuzA/jKjYimrdR2TkWZ29Zq0WyDLc/meTDaxGSOsFZxr3XuNDajFqznzw9KMWcqK0WSWvU+opIIbkhhKQRzBGx3Pk0QnKlpW3jPmxsb7yZlNVxJVa3SCZ6I48tow1jkYy3wpHo+k+T/keb5+FdH57u9lAQjdjMLtcIi+YycCIRHs1ahGR4Agz7Ma7EasE2OyICmlYsd20O/8BZqguyVnhRX2beP8DZaj2ITGyV+fc2EFJIWpgfolm3tIdcVhaZ7f0rWWIT+c23pG9kimwsIWkO8AIh9tJ6J2pLxce91GTo37dODLozuxJt8YtBdx7XxWaMj6SBtkDgsXFza1nanmTou59j3sa1wPeoeZvC51p9QuP4SJrfm3/MY9P8LoXksxeSGk+GT7BTif58LUJSD3ywV9E1abS5REvbIrGfnvaI4W8uMpTEm3RiwWAEYSQIdDHaq+Ven7mPZFwfU8stj7WzR1GwV14OZ2JIVx7JuR9Qt9n0bRU1Fn0udinJbhW6tqXtVYOrgne51jGo+2gmaL9PhmjemRFTJ7Hl/oTxS276rfUW/I0+9WUUSxvRxq/wVb91LF1gsu8TChWxgi+75I9GCkkrGnPeJvIVgRA2cmpcnCrLfvox4U/KxhKSzOFzvRIWhLWXw+k8gg/wuvbrEc6KxaE9nDQ6FIl9mn/HjWwJYQYMCIGt+CQ3Z6zvHsvM+wc5V1vMbnNyW4OQnO6iwraFYvUSI1PzFudzQ0gaIjYcDCCEneJmDzMRwUdz+D3nqC3cZ2nc1iU0w0KlEXx4m66v+8xo2lT50whJQ3hER/nqTtaJhGSkBSj8QmLb5+a++bkRKZ5KsM0PqNtyoiw3cUStMflH9wvQlxWPUNtxJ44VPpaVorYXR9vZZ1diUxgB2n03+2ybKWweMu/1mQtJ5hhtPxATTR5CzyigFNM8bOm3KQvJAD97bkf2TTCFpNk2tPt0HdyGsO/HNRyVS5Rl5v03Gfg5OtjGasFc4nFgbmXfLHNvewV76Un6/Zbzg78zeLIB1/DK6X9Mv1HbAUtan3j+pvP4XBUxAj0kZsNCctnnoij6xUO7R2d5thSSGwgpJKPQpumv3xnpfrKjmREZH5YyG0xIAsH/cKHyBV1MCkRmLkUlr1DgMCyVCpm7PsWjB9FY0/mIhMEIJ+m6YwxzS8wMfhbKdScEIjOPkqojqA01VBTn6ksyGbzouq0n905dSC6PtrLb8BUSm3AUvEJJSSjxeSgIyBIRpj1gsOlVUyBn5pZQ5WygoeatUMJzIaLyYFr9OwRCcVCwtyCUAzPpnVNieTZCMnapdu1CMnpJMYFQ0aboqy9AEQr2kmNc6O2nt/MktbWnaGvYFZqgSxpo+3aEh5pl0rW/Qav3DqN9f+OLq/dZ0C2SQsmn4tQ/6Ou9gKv+Q44f2YtNKGRVtPDd9X8ycHfBkipoP2d/fWyZ+B/gdb3NPtPPziCeddRYjtTbkJmf9BBlebmxuRITYhEMcf0KF/D3fERh5mbyrHkKg3fprt1NvvUza32t5qP41G1gBYzcs8pL1Hbf1cW0sUfubstnOuYSfpLL0HobU3JKqPm8lfNuN+7zZ2go3Y4SlQM0nEdyOyVHvgjlnGw/hfpuMQ7FajU1+rHe3txtnDpSyvYDqy1ZG9u26UGBSg5F7x5Bdb5LSV5R1LOuYpEUGeRWfEFX37/odDVQf7yOV20CkVVJ63fXuDTwC7ddxVFJmXXLa9YB2kctkfv28N+A/mK2IyL1muTZIoVkLFbDkhAKW9RBFlb/miSKNAvJBcbcb4ZEkVKIOjgT+e/AAGpeRmiyfu08Y4kmieA0vuvnaDR3tlHIzH2Z0op6XBd/jNhdQ/Nf4mCEA3zsoThK+dxjvReN4NQw37aqVJXtDYlUxUFBSQW1n5+jZ8hvSdczTZ9zZ0jY2t/EPWZtZomeN0hgzMNFl0pVWZEuIEO71VQ3XWR4JnpoXWBq5AqtajVlr4ZEoeIooKTir3ze3sOQP8pKoU0weLIy5HOZmUtRWTWq6xKe6PNSYC11rgXG8PR8jVqi59K0l6Je6MU7/oBJ30B4FxmRQ1nzZQZ8UywGxvB0n6FKnwiVvGpaun9iPDDH1EgPLVUv6tGfL1Lddg3v6B18AxdpNK6RVU7zP7/HNzVHYPwnuluq9Sj9DPKqztDtGSOwOMlIj/Ua79N2fYhxPRWJFviFrsZKCh2ZOAr2U+O6wfj8HL7Waiob2+nxjhMwxJL2iJFzdRQ5MnEUVXPi6n9CbUObZbSrifK8LDJzS8x61SavoxbnYHPsxdlh7BJi3UmphNKKGhoaaql6rylql6PQLjvdbU79RccQu8a9h3YjcdWUhtqU4qCw4mPc0TslJayvcby9HeH6ElspUb+mZ8BnSRmj3+/4DVqdlZSWVlDTcITqqg8jMw5os9y54ebzCiMIYyslage93nH9nAWm0tUGkknSbew8U/FaqHyd71MVvdOLFmDc28sFtdR8cbOXqJzv+R7f1ErTxzLz/h+56KoPv2wqDgrfOkpr//1YK7Cx+1BJHplCoDh285azhW9HJqN2vzI2X1DIzN1DhdPFRe9EElZljeDMLTqb3md/gQObo5D91Z/SadkhSQuM4en+CqcR8Gf2zUAoKfrotzSW7yQzM48S47vab/Sqr+Kw5VDs/Du+wP8yfvFjDh0qo7DobRpbv+F8ayO1tSf51prmZ/wSRw+9R1nhHiobQ0nYG2uP0PytDLbZSEghGY9wRoa4mkWSFGkWkpL/VmSdSyQSyfOLHOMTELxH72kXHUm9xEniIYWkBJB1LpFIJM8zcoyXrBdSSEoAWecSiUTyPCPHeMl6IYWkBJB1LpFIJM8zcoyXrBdSSEoAWecSiUTyPCPHeMl6IYWkBJB1LpFIJM8zcoyXrBdSSEoAWecSiUTyPCPHeMl6IYWkBJB1LpFIJM8zcoyXrBcJhaQ85CEPechDHvKQhzzksdoRV0hK/lzIOpdIJJLnFznGS9YLKSQlgKxziUQieZ6RY7xkvZBCUgLIOpdIJJLnGTnGS9YLKSQlgKxziUQieZ6RY7xkvfjjhKQ2TucBB0IIlPwmvHNa6PPlX2l/JTvksKnsptE7m/5rS1ZFDjISyUZmmfmZ/+C99iPji9qzvhlAIxiYZOrx0rO+EUmSyDFesl78cUIy6EHNNCJ8ynD7g3E+z6TEPZ7+a0tWRQ4yABrzvjbKc7ZSePRfTKZ9vg7gl4VE7QAAFsFJREFUa6sgx/4yR3t/YyPIgeeK+du0lb+AvVCldzL4rO8mDksERq9w4vQAM8lWvjbNcEcT1SV5ZAqB2PQ+PbPL63qXiVkiMP4TPe4vcb61G4dShMs3b9yo7DtpRJvp5+Sx/48vkD6hLsd4yXrx7IXk0i2ad9j0z7dQ0fV7+q8tWRU5yAAEmeyqwh5tNU8Xmp+uyhyEyCC/8Ufm0vvrf3q0yctU2pUNurKxgL/nU+rODhFYQ7PS7rvZZ3vWQlIjGHiA/9cLVGUpCGEVkrLvpBstMMTp9z6h27+Qlt+TY7xkvXj2QpJl5qd+xevx4Bm5SyC41gFomdm+D9mmCETmKzRvuIlkYyMHGZ3gDKOeHxjyzz2F1UNjfvgrmq5OxXwefDiKZ2AY//yzEgPPM4s8HP2JgSE/8xvKZLWAv9vJ7rcvM7HW+5rtoWrTsxaSOsu3ce3cFCUkSVPfAeaH+KLpOpEj+J+z72gTl3n7L8cZnH16y6Qc4yXrxQYQkmm7AH53mVwiXyNykEkf2uwAav4OqnqihaTkz4fG4mg7+7a9Tad/ce0/898gJNOBNs2guouMqh6kKQBgEX/n22w/+BQvITpyjJesF1JISgA5yKSN4BidldsQwiGFpEQPMswh29n3dMuxfwohucD9znfJEoJNUkiaaNPdVGdt52DX/aey9MoxXrJerIOQXGDS+3eaql+nwLEJxVHI/hoXV33/pCGukJxhUC1EEQJhfxP3WJQ/SHAC70UXzooSChybEEIhM7eIsupG2q/7eBjU0Ca7qc2zx9+6R8lhb5sPGVu4Mk9T51rgV662NFBT9ymtbV/grHiT6qZLsY7i2iyj356ktraR1vNtnKrZR2FRMa+1/pvlVM4BYJGHvqu0qh9QVXMUZ1U5pRV/pbnDk2Dpa5l5v4eOpg+oqDpCQ00F+yqO0nL110ifteAsY4Nu1NdVrkZN2Ks+p/Y7/aedlOVmIEQGuWV1qKqKerIXvwYhH7MxBt0qr9dfY5YlAne6aSzZqrfVF6lu62fM+L3gBN7OY5TYbWSVfckPVl+p4ATejiaqy0ooyt1Gbsl7NHXcZDIZ15BgAL/PQ3d7I1Vvn+Pn5UUe+npw1eynwLGF3JLDnBt5hIZGcPImHU3vUZKbRWZuKbXnbsX6+Gmz3LnaQn2Nk89bv+KU8yDl1Z9y0ffIMvFpBAPjeHv+hlr+Nq2+R/j7W6gpySNTbMJRdAjXjTHLknSQwLiXnjaV8tfP4lu4T7+rlpJcO0LJoejQGW6MP46aWIMExn7Arb5NvelWECQwfosbnSc5VFSOy/eYwOi3NJbv1K9bpz9rNMaz11BVU0dV2etU1H5mjjnJoTHnbSJf2UFN74MEpyRTdsQRkhrBKR/97dXkiE3klH5EW3c/Q+MB/XtLBO5co6W+lrrPW2g7VU9F+fs0Xbydmo+m9pjxG2dxVrxJlfMwVRV/xfXPDtT8eEvbifvO6v0vyGR/K86yPBQhUHLLOKKqqOppev2LxPadKILT+HrOolZXU9NwRK+vZjo89yLdHIKzjA/foPPEBxS9cgbf0iyjXU2U5+ntquYbRuKMW8nUkTY/wcj1czRWVofad9+XVORlkVlYR9s/2qgv3hLq5/ZS1Avfh/u5FmCsz0V5Vpx+DqDdo7M8G2WXC99TROxLISlZL9IsJOcZc1dgN0Rc5k7Kaj6koWY/eVvtZCrxhOQ47pJM/fNcVE/A/DUtMISrdKspChVHAa+WvEpRriEat1HTOw3zPjqb6qkoMH7HhqPkg9Akrn7C6f7fn/sov6dlrXVu1JGtvNNcegm9QduwH7igCygw2kb2Pjf3jc+0aQbVl3nRdVsXicmcAyGfsyPkZb9Dx9jj8MQ5fIZSuw176RmGIyaDBfzXP2HPLqfFcf0h/c5chNhKaesI89osd666qCrICrWhKMtP8s85RU+VI8YiqQV+5aqrmoJMBRFhcdEIjrkptysI5TXax55EFvCTmzTueBWXz2LPWvyF82+UofZNEATQHuJtKkIRGeTV31glInge/8ggfe3V5AiByPuAU80NHDvfy/CdcUb7TlJqVxBZ1Zy/9hVq09/p943hH+3DVZ6DENup7pkI9yftEcOu/dhtFXRO6H1am6CnejvCflBfztVYHB+g41QVeYpAiHzeqq7kQMMZzru/ofX4G+QqAqG8RG33XYLMM97/d05VvRh6wcwtp7ryIA0t3+A+38Lxcl1s5B3R6zMkmlxVu0KRzWbZayz6R7hxoZ4CRSDESxxq/Ij31G+4Pvg919veD92PvYquqChvbfIadXl55rNqAQ9NhZstL6mb2BnRJuOhtzFr2URcJJmy04lnkdQe4m06yME2LzMR4tboC1so77xn9o/pnkNkCQcHOseTGw+1Rwy7ythm7U/GPQsRFpKr9J2k+h8Ay8z2vM+mKItk4r6jE7xLd20B2ZUdjBkvkeZ9bqXUZQQ4zeMf+Y4Lzt2hdpVXTaNah3r+GoOD12irfhFFKNgrL4ejzpOqoyVmPG2oR8pC7Vjs4iNXM0c/b8ZZvAUhcnH2z+gvFQKRfZT+6GCkeP08/E/G2l9DEQU0egNx/p8cUkhK1ou0CkkzalIIRNa7dN43Bg2N4INvqcpIRUg+wqO+qH++lVLXj5bBcpGHvn+gFr9ueSMO4FFz5dL2GllbnWvM9R8lW4gIgWUufUUMvndo35MZeR6gTVzi8Gl9Qk7mHDTmh09SqGSyp/1O1IS4iL/zIHahWMSdIdR2RAog01okUPa0M2aIw7jRsSk8ZwIhadzfffcBbNGToTExic3sco0Qlg8ai77T7H2vm2nzxufwufaSXW39DLSJTsptAqG8kmAyisIQJrYy2sesy5PGBG9jW30fs2HFyBNvE9uFErlMO9eHM1uJKoN5fK4ihMi2CBlLeYntVHXdIyytLC+gWYfomQ6JlmWfi51CILLep2vCIrCDd3CXOxBCIctaDtoY7n1ZsWVvXncblZ1jlusazxp1n0Z9RLQBQ4iJmPaZkCc3adxuS7wcnUrZRQvJ4F26j9bS1HefWIka/7mM8kzu/peY6WsgzxanPRnPFWWRTNR3ku9/8YVkiAR9hwDDzcUo8V7CzNzFkeI53K6scxTx62NN7TubYtct5gFt3s/I0HhIyC6O4Nq1OfZlTO9bOyzjUCRGudjY3niTJ/FOSQIpJCXrRRqF5BLTXe9gEwIhFLaog0QY6BP6SCYQksbAKQQio47ex3F62FKQ8Iu4FJJPw5oHmaCfwY7zXLEu88TzodKXZ4TyEjWdP4eX17QAv/2mL8clcw4P6K3ZgUjwdh6yEioWQRXA21gQ3wqgzeEfGcI3ZWmpiXzRkn3OFYVkoolSY9HnYpciUHaeYPiJcYVHeNTXcfY/DP/EXB/ObHusiDbvJZ7AjkNCnztjkoy1uJkT8E4XPvMfC/gHOzl3xWdZMk3wGyv51pniJNd83vjXC5VXSNRGW3cSlH3C6ya4T0M8RJeNUWbx2lI8jPNj7n8NZWepr+7ff6Xr6GFOD8dbkg+VT9A/SMe5ngj3ksTlGQdD9MQ7N1F5xm1TqfS/lYRkgv/NXqcmS0Fsb8L7JLo0wuLfuiycsBziPlea2vcK9xMqo1ciRWkUxj0/je+oFJKS9SKNQvIx3sYXEwu5FIWk2dmFQJS48a96fSkkn4Z0DDIhH6GvOdH8EW/lRFsslgh4P6NQCS0L5pQe57JvOsqaksQ5Cawh4ZsIWTVNQWUM7slMnpBUUMPKz7kWIYnFmhYWUsz1UV900iIsDQFl8b+Mc3zc6VvdapE2IRlRMEyN/Iv2E5+hvvVCihOtYUULf2fF6xliL5myT1VImtbaqCVp43eSFJIpCbfVys68p3yK/rIFJf+TJFPCLDM/dZvr7V/SrL4RcmdI4n60sXb2KAmESypCMqX+l6qQtLxQJPh94zmsbiOpCUnrjz1N+9Z5MkTzzozIXKdPbtL4Uq1piY9bMqm0pQRIISlZL9IoJFcRcikKyaBH1f2dpJD8I3iqYJv5e3jcjdQdu8DwzOIKA+oiM96/UZmbgZGAvljtYjTCn3GVc0xLdSJ/IUNM6IP8qlahKFYQksk95xqFpGmtMJZrl5juOczeiKVu4/tpiAhPp5DU5vB73Byra+LC8AOCa7LYxH5nxckzlbJPVUiaya+j/Amf3KRx+6bI5fQVSGryT7bszPp6jYaPX8MuMsiru7bCDjKh4Bb3MSfHLtxiJqilIEbCrhxPLSRT6n+pCsnwZ/EtkpbrW+41ZSGZlvZtYHG/qbzMpKYx1/8hL6zSpqSQlGxkpEVSAjxtsI0jcnJdZUDV5u8xcPZ93XlewV56Fl9UBumE55gWyewES0FRYiKuj9MKJBBYyT/nWoUkMPcD6jYbwnYA971fcL/+Du77i3G+/3S+Uis9Z8pC0ghGsPg2rm3pz/hOMkvblt9aj6VtNIL+a6jFW1DyDnN59BFB7TFjXUfYtS86kCsxpt9qQgtuCmVnra8H/6Z931aEcFDuvhPHR9IIton0S0xejITbqRLPby8VIZlS/3sKi2Si349zTykJybS1bwtGmSiv4PLdo99ZEum+Eq9kUvJvjY8UkpL1Yp18JG3saL4VmXJnPXwkg9P4Hxj+NVJIPg1rfXkYbn4ZJZmJW/sfLn31fYRfYHDyBmrh5rB4SOYccwlUiZ+bz1jaNiJxDb/LmEAWgwX81/7BwHSUxTNCYKXwnE8jJJnD53oFRWym8N2DlL4XbaWwTJwRE5v1lEcMf3ORodWWXtMiJDWeDJ9gZ8wS6FomWt331RJBvaLwmb1OTZYtMsI2bUIy9GyLvq95r+YzWl1NqI0u3L2ppP4hLBjiLoWnWHYR9bVE8H4nlVkKQimiyfsw8oXKWD6NqttUrFrLo63sVhK0s1SEZEr9bw0+kqaLQ25cMRZa2o6MxE5eSKazfVsx+rlC9tsf8HZBAmtq+Cl0C3GCMS9JpJCUrBfpjdo23sCFQGw7TM+EIfIWmRls0n3fkhSSEVHbdoqbPeGobW2eqZErNJa8YBGMRucOCdmQxUYj+PA2XV/3WdKzSOKxpjo3/RGz2Ocei1oCjPIdXL6Nq6ghakINMtFZgc2YBJI5x9gpxK7EpkjBiBzdTGHzkJ5SJLwHsLC/zol+a165BSYHT1PnMs4lwWSYwnOaYiaexXQ1IWmJfE0wMZoiQWSQV3s5nO4EIPgAb9sRajviWamiSIuQNNKSCGzWlE1GgEUKE20oSGpzxD7KiYWP7gYQs6d2uoTkEgFfB389di2pJeyEGNHfca1lKZZdTH0FmbxeT54iUPIa6JsJiz3TL9B2wGLRXt2fMAIzwngr+9p/jhSAKQXbpNL/wv0j1vKWqO/MMdp+ICpTg/kQoUhvpZjm4bAbTPJCMn3tOxozKDChwI737IlWYZJDCknJepHmPJJGp9YFY2YhFc6PcL5bTM4LubxgS0VIgjb7Iyf26ElchUJm7suUlLxMrp5PLDJCNezXE0ru7KBgbwEOZeXACUmIp7NICpTcg5zqukFvp4v6epUjr2YhxDYqWq9y/dL33A3exrVzW9SkFEplk7WvndFFTR+IVzkHgAX8PR9RmLmZvNor+I0XjOBdumt3k2/9zPz8pVDuOLEJR9HbONUjVJW8RGH0uStaJJN4zmVj8lGwl7XiHfuZvjMtXJ0IkoyQNMRHZPS2FWPZMtQHFEcx7zobUBsOUZaXa8mZtwqrCsnY5fOVLJJCyafi1D/o672Aq/5Djh/Zi00oZFW08N31fzJwd8Ey0Ual8wpOMdhYTFZU/k/zevb9uLwPdHGsEZwZoLFwR5xnTVVIJhIEP9O6O5PMgoM0tn6D2+3G3dFF76AHj2cInz+wulAHwn6v8fx5Uyy7uC84oe0EFaFE5k61vGzkVnxBV9+/6HQ1UH+8jldtApFVSet317g0cG+FPJiWdqa8RM2FW0zNL4cCzTrrKcxUQvXY/C2DRnqbhBkPku9/pgi2v0Gr9w6jfX/ji6t+tJX6TvAuPfUvk2nmIQ2Vb9B/hdr83ZbP9OpNJCRjXgzX2r6TyfeoWyWTStelt9M4+U5TQQpJyXqR/p1tzJ0Q9pCbaSMzt4TqposMT4/Tf/oTfaePq4yZomDlnW20+Xt4OpqpNXe22YSj4HWq1NNcjN61QJtg8GQleZkKIjOXorJqVNclPP45mZB8FdbuI/kLXY1vkJeZFdpdpfMWM8FFJns/odiRiaO4ng7fIzTtf7h4tIZDZS9TVPkJree/obXxMLXNV8KBNMmcY7LM/PgNWp2VlJZWUNNwhOqqD2N3qjEIPmC481Oq9xfisDko2P++fq+GlTvAuKeH82qp/iK0lRK1g15vaJJM+jkBLXCLczV7cNhyKKr6gqtjj9ECY3h6vkY1drGxl6Je6MVr7kRilihzXhcfnPt5BSuFnoC7pjT0UqU4KKz4GHd0f4jLAlO+AS437tOfM4ey5ssM+KZYDIzh6T5DVV4o0EnJq6al+yfGA3NMjfTQYiQIV16kuu1a6N41Y2eQrHBfn1lEm7yOWpyDzbEXZ4e+m4ploj3Seora8srQLiTl76C6Y3flMSf8vDpaWw5TXlFDg7OK8sqPcXsnLOJgicD4T/ScVymx6z61Jce40Otl9M4QPS3VeiL0DPKqztDtGSOwOMlIT9Sz9owwFdQIJdBuojRnE3F3yxJbKG4aWCXxu87cjzTmZ8b3aU2m7P7fALf6/0Gr85Vwu2y8SP/QOIHAOF53Ddv0F2178RFau4aYDC6Fd+/JzKOk+lM6hx8Q1H6jV30Vhy2HYuffY3eeStDOWpyVlBTkYM8toqzGxdVff6DtzQpqPz9Ht+cXpuYfrdh3gNX7n1kmjxg5V0eRIxNHUTUnrv6H/02m75jzzmuUVtTQ4HyfKmcLV+/MWvrXQkw7rmq5gmd8lsWpkZh20jMySTCp9n0L/+h3uD+v1BOSG+1viPGEZRwSxhkJc0daWBzBtSsz6SCvREghKVkv/ri9tiUbGlnnknUnxT2a0xGpujYW8F//kmMXfyXwcAL/uA+vx4PH48Hj6aPnwhmce16jefhxEr+1xOzgJ+RvP4YnmdyTkj8JAbyNpVR2+VcxcuiJ27Or6UomWHAF5BgvWS+kkJQAss4lfwD/FUIy5HuYv3ulfY2Xme35mA9XibQ10R4x7HqXg3EjrCV/RrTpbqp31KyYOxKAxZ9pf20vddd/e+pVNTnGS9YLKSQlgKxzyR/Af4OQ1P3kVkyzok3jOX2W/pnk0gAB+paGK+1GI3mu0QKMeQYYGLlLYH6SwcZX2b1akI32iOHThzka5eO5VuQYL1kvpJCUALLOJX8AKQUjWIRkomTT64G580g+Fae6Gbk7o/udagQDE9zxfourtp6zaxGEwd8ZPN3E6cHfpWXyT4UlYl4/lMLP8K7koxr8ncGzJ/nKk762Isd4yXohhaQEkHUuWU9CwTDdZjBDKDCk5fxF+sfjbUUXYNxzJRwUIbZQ7DzN+Y4BxhMuN6cLjaC/D1fVrvDOWsbk79jDoRNdjMysnKxlZRZ5NPVICsk/GVrgFueqd5Epsiio/grP5MLKXwjO8vBxChbvJJBjvGS9kEJSAsg6l6wnGsHAA/x+f9QxwcP5eGvWQQJTv8We/9vDJKLS08Uy8w/v4fPqKX/GpwikkoxcItlgyDFesl5IISkBZJ1LJBLJ84wc4yXrhRSSEkDWuUQikTzPyDFesl5IISkBZJ1LJBLJ84wc4yXrRUIhKQ95yEMe8pCHPOQhD3msdsQISYlEIpFIJBKJJBX+Dw92Yh11+7TdAAAAAElFTkSuQmCC" } }, "cell_type": "markdown", "metadata": {}, "source": [ "![mutable.png](attachment:907113d4-c125-46ec-8326-790717f0208c.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## ```if __name__ == \"__main__\":```\n", "\n", "* 當一個 script 被當作 entry point 執行時 ```__name__``` 會被設成 ```\"__main__\"```\n", "* 如果是被當作 module include,```__name__``` 會被設成該 script 的檔名" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Context Manager (the ```with``` Statement)\n", "\n", "* [SO discussion](https://stackoverflow.com/questions/1984325/explaining-pythons-enter-and-exit)\n", "* The following are equivalent: " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# try block\n", "\n", "SET_THINGS_UP\n", "try: \n", " DO_SOMETHING\n", "finally:\n", " TEAR_THINGS_DOWN\n", "\n", "# with statement\n", "\n", "class controlled_execution:\n", " def __enter__(self):\n", " SET_THINGS_UP\n", " return THING\n", " def __exit__(self, exc_type, exc_value, traceback):\n", " TEAR_THINGS_DOWN\n", " \n", "with controlled_execution as THING:\n", " SOME_CODE" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "* controlled_execution is a context manager class which implements ```__enter__()``` and ```__exit__()```. The return value of ```__enter__()```, if provided, is assigned to the variable followed by ```as```" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.5488135039273248\n", "0.9087389795050141\n" ] } ], "source": [ "import numpy as np\n", "\n", "class fix_seed:\n", " def __init__(self, seed=0):\n", " self.seed = seed\n", " \n", " def __enter__(self):\n", " np.random.seed(self.seed)\n", " \n", " def __exit__(self, exc_type=None, exc_value=None, traceback=None):\n", " np.random.seed()\n", " \n", "with fix_seed(seed=0):\n", " print(np.random.uniform())\n", "print(np.random.uniform())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## itertools.groupby\n", "\n", "* 在把 data 丟進 groupby 裡之前必需是已經 sorted by key\n", "* key 的用法和 sorted 一模一樣\n", "* 迴圈裡的每一個 g 都是 iterator" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "keys: [' ', 'b', 'e', 'f', 'i', 'n', 'r', 's', 't', 'u', 'v', 'y']\n", "groups: \n", "[[' ', ' ', ' ', ' '],\n", " ['b', 'b'],\n", " ['e', 'e'],\n", " ['f'],\n", " ['i', 'i'],\n", " ['n'],\n", " ['r', 'r'],\n", " ['s', 's'],\n", " ['t', 't', 't', 't', 't', 't'],\n", " ['u', 'u', 'u'],\n", " ['v'],\n", " ['y', 'y']]\n" ] } ], "source": [ "import itertools\n", "\n", "data = 'trust but verify by unittest'\n", "keyfunc = None\n", "\n", "groups = []\n", "uniquekeys = []\n", "data = sorted(data, key=keyfunc)\n", "for k, g in itertools.groupby(data, keyfunc):\n", " groups.append(list(g)) # store group iterator as a list\n", " uniquekeys.append(k)\n", "\n", "from pprint import pprint\n", "print('keys: ', uniquekeys)\n", "print('groups: ')\n", "pprint(groups)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## collections" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### deque\n", "\n", "* 用 doubly linked list 寫成,左右兩端插入刪除都是 O(1)\n", "* 也有 [circular array implementation](https://youtu.be/IITnvmnfi_Y?t=236)\n", " * 用兩個 index 指向兩端,因為是 circular array 所以沒有 index out of range 的問題\n", " * 如果 push 到空間不夠就 resize,向系統要更多空間重抄一次(takes $O(n)$ operations)重抄的時候用 0 當 start index\n", " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Counter\n", "\n", "* Dictionary of element frequencies of a list" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Counter({'a': 3, 'b': 2, 'c': 4, 'd': 2, 'e': 1, 'f': 4, 'g': 1})" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import collections\n", "\n", "collections.Counter('aaabbccccddeffffg')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### OrderedDict\n", "\n", "* [python doc](https://docs.python.org/3/library/collections.html#collections.OrderedDict)\n", "* 記得輸入順序的 dict,比 dict 多了兩個 method:\n", " * ```popitem(last=True)```\n", " * ```move_to_end(key, last=True)```\n", "* ```last=True``` 代表要 pop 最後一個 item,move_to_end 也是,如果用 ```last=False``` 變成 pop 第一個 item 和 move 到 beginning\n", "* Implementation 是用 doubly linked list 來維持順序,再用 hash table 記下指標指向對應的 node\n", "* 可以用來寫 LRU Cache" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "OrderedDict([('e', 5), ('a', 1), ('b', 2)])\n", "OrderedDict([('e', 5), ('b', 2), ('a', 1)])\n" ] } ], "source": [ "import collections\n", "\n", "d = collections.OrderedDict()\n", "d['e'] = 5\n", "d['a'] = 1\n", "d['b'] = 2\n", "print(d)\n", "\n", "d.move_to_end('a')\n", "print(d)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### OrderedDict and dict\n", "\n", "* 從 3.7 開始 Python 會記住 `dict` 元素插入的順序,用的是 doubly linked list + hash table\n", "* 現在 `OrderedDict` 和 `dict` 一樣,而且增刪查改都一樣是 O(1)\n", "* 但 `OrderedDict` 還在,因為\n", " * Backward compatibility, legacy code\n", " * 用 `OrderedDict` 讀起來比較 explicit\n", " * `OrderedDict` 有 `popitem(last=True)` 和 `move_to_end(key, last=True)`,`dict` 沒有。有時候會需要這些操作\n", " * `dict.popitem()` 只 pop 第一個,不能指定 pop 最後一個\n", " * `d1==d2` 用 `dict` 只比 key,在 `OrderedDict` key 和 value 都比" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### defaultdict\n", "\n", "* [SO explanation](https://stackoverflow.com/questions/5900578/how-does-collections-defaultdict-work), [python doc](https://docs.python.org/3/library/collections.html#collections.defaultdict)\n", "* access 沒加過的 key 也不會有 key error 而是回傳 default value\n", "* 要輸入一個 callable 當作 default_factory 例如 ```collections.defaultdict(int)```,default value 是這個 callable 的傳回值" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Functional Programming\n", "\n", "* Pure function on immutable data\n", " * 如果用 mutable 到 multithread 的時候就要擔心同步問題\n", " * 不要用 list of dictionaries,用 tuple of collections.namedtuple,完全 immutable\n", "* Pure function:每次執行結果都一樣,no access to global states,也不能改變 input(即使是 mutable)\n", "* Higher Order Functions\n", " * ```filter(function, iterable)```\n", " * ```map(function, iterable, ...)```\n", " * ```functools.partial(func, /, *args, **keywords)```\n", " * ```functools.reduce(function, iterable[, initializer])```\n", "* helper functions\n", " * ```zip(*iterables)```\n", " * any, all\n", " * enumerate\n", " * sort\n", " * [itertools](https://docs.python.org/3/library/itertools.html):一些常用的 iterator\n", "* 其實用 list comprehension 就可以取代 filter 和 map 了" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exception\n", "\n", "* [Corey Schafer tutorial](https://www.youtube.com/watch?v=NIWwJbo-9_8)\n", "* [Built-In Exception Hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy)\n", "* Built-in exception class 在執行環境中一啟動就已經載入了,無需另外 import" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "try: \n", " pass\n", "except ValueError as error: # 如果抓到 ValueError 就跑這裡\n", " pass\n", "except TypeError as error: # 如果抓到 TypeError 就跑這裡\n", " pass\n", "except Exception as error: # 任何其它 Exception 跑這裡。越 general 的要放越下面\n", " pass\n", "else: # 完全沒抓到 Exception 就跑這裡\n", " pass\n", "finally: # 不管有沒有 Exception 都會跑到這裡\n", " pass" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[Errno 2] No such file or directory: 'circles_.py'\n", "Done!\n" ] } ], "source": [ "try:\n", " f = open('circles_.py')\n", "except FileNotFoundError as e:\n", " print(e)\n", "else:\n", " print(f.readline())\n", " f.close()\n", "finally:\n", " print('Done!')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## unittest\n", "* [Socratica video](https://www.youtube.com/watch?v=1Lfv5tUGsn8) 8 分鐘極簡版\n", "* [Corey Schafer 40 min](https://www.youtube.com/watch?v=6tNS--WetLI)\n", "* unittest test case methods 名稱一定要以 test 開頭,但 module 名稱不限\n", "* 跑 unittest:可以指定 module 也可以不指定(```m``` 是當作 module 來跑)\n", " * ```python -m unittest test_circles.py```\n", " * ```python -m unittest test_circles```\n", " * ```python -m unittest```\n", "* 不指定時 python 用 test discovery 抓所有名稱以 test 開頭的 test case method 來跑\n", "* 如果在 test_circles.py 裡加這個就可以直接 ```python test_circles.py```\n", "```python\n", " if __name__ == '__main__':\n", " unittest.main()\n", "```\n", "* ```misc/pycircle``` 裡有 minimum python module with unittests,可以在 misc/ 下跑 ```python -m unittest```\n", "* 每次 library 在使用中出錯時,修好後應該去對應的地方加一個相關的 test 保證以後不再出現同樣的錯\n", "* ```setUp``` 和 ```tearDown```\n", " * ```setUp``` 在每一次 test case method 開始前先執行\n", " * ```tearDown``` 在每一次 test case method 結束後執行\n", " * ```setUpClass``` 在所有 test case method 開始前先執行一次\n", " * ```tearDownClass``` 在所有 test case method 結束前執行一次\n", "* 所有 test case method 不一定會照順序執行,所以他們之間一定要獨立\n", "* [unittest.mock.patch](https://youtu.be/6tNS--WetLI?t=1723) 沒看\n", "* 一個 test script 的 `if __name__=='__main__': ` 裡面是寫 `unittest.main()`。看 [unittest doc](https://docs.python.org/3/library/unittest.html#basic-example)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "# circles.py\n", "\n", "from math import pi\n", "\n", "def circle_area(r):\n", " if type(r) not in [int, float]:\n", " raise TypeError(\"The radius must be a non-negative real number.\")\n", " \n", " if r < 0:\n", " raise ValueError(\"The radius cannot be negative\")\n", " \n", " return pi*(r**2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# test_circles.py\n", "\n", "import unittest\n", "from circles import circle_area\n", "from math import pi\n", "\n", "class TestCircleArea(unittest.TestCase):\n", " @classmethod\n", " def setUpClass(cls):\n", " pass\n", " \n", " @classmethod\n", " def tearDownClass(cls):\n", " pass\n", " \n", " def setUp(self):\n", " pass\n", " \n", " def tearDown(self):\n", " pass\n", " \n", " def test_area(self):\n", " # Test areas when radius >= 0\n", " self.assertAlmostEqual(circle_area(1), pi)\n", " self.assertAlmostEqual(circle_area(0), 0)\n", " self.assertAlmostEqual(circle_area(2.1), pi*(2.1**2))\n", " \n", " def test_values(self):\n", " # Make sure value erros are raised when necessary\n", " self.assertRaises(ValueError, circle_area, -2) # 寫法一\n", " with self.assertRaises(ValueError): # 寫法二,可以正常呼叫函數\n", " circle_area(-2)\n", " \n", " def test_types(self):\n", " # Make sure type errors are raised when necessary\n", " self.assertRaises(TypeError, circle_area, 3+5j)\n", " self.assertRaises(TypeError, circle_area, True)\n", " self.assertRaises(TypeError, circle_area, \"radius\")\n", " \n", "if __name__ == '__main__':\n", " unittest.main()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "...\n", "----------------------------------------------------------------------\n", "Ran 3 tests in 0.000s\n", "\n", "OK\n", "...\n", "----------------------------------------------------------------------\n", "Ran 3 tests in 0.000s\n", "\n", "OK\n" ] } ], "source": [ "# this works because the scripts are here\n", "\n", "!python -m unittest test_circles\n", "!python -m unittest" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## PEP8 Naming Styles\n", "\n", "* [RealPython tutorial](https://realpython.com/python-pep8/)\n", "\n", "| Type | Style |\n", "|---|---|\n", "| MyClass | PascalCase |\n", "| MY_CONST | CAPITAL_SNAKE_CASE | \n", "| mypackage | likethis |\n", "| everything_else | snake_case |" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## OOP\n", "\n", "* [Corey Schafer Videos on OOP](https://www.youtube.com/playlist?list=PL-osiE80TeTsqhIuOqKhwlXsIBIdSeYtc)\n", "\n", "### Sample Program\n", "\n", "* From [Corey Schafer Video on Preparing for Python Interview](https://youtu.be/DEwgZNC-KyE?t=899)\n", "* 重覆默寫這段 code 直到覺得自然為止" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "My name is Corey.\n", "My name is Wade Wilson.\n", "And I'm Deadpool.\n" ] } ], "source": [ "class Person:\n", " def __init__(self, name):\n", " self.name = name\n", " \n", " def reveal_identity(self):\n", " print(f\"My name is {self.name}.\")\n", " \n", "class SuperHero(Person):\n", " def __init__(self, name, hero_name):\n", " super().__init__(name)\n", " self.hero_name = hero_name\n", " \n", " def reveal_identity(self):\n", " super().reveal_identity()\n", " print(f\"And I'm {self.hero_name}.\")\n", " \n", "corey = Person('Corey')\n", "corey.reveal_identity()\n", "\n", "wade = SuperHero('Wade Wilson', 'Deadpool')\n", "wade.reveal_identity()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### classmethod and staticmethod\n", "\n", "* staticmethods don't have access to anything. A good use is to [group util functions](https://stackoverflow.com/questions/2438473/what-is-the-advantage-of-using-static-methods-in-python)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.05\n", "1.05\n", "1.05\n", "John.Doe@email.com\n", "70000\n", "True\n" ] } ], "source": [ "class Employee:\n", "\n", " num_of_emps = 0\n", " raise_amt = 1.04\n", "\n", " def __init__(self, first, last, pay):\n", " self.first = first\n", " self.last = last\n", " self.email = first + '.' + last + '@email.com'\n", " self.pay = pay\n", "\n", " Employee.num_of_emps += 1\n", "\n", " def fullname(self):\n", " return '{} {}'.format(self.first, self.last)\n", "\n", " def apply_raise(self):\n", " self.pay = int(self.pay * self.raise_amt)\n", "\n", " @classmethod\n", " def set_raise_amt(cls, amount):\n", " cls.raise_amt = amount\n", "\n", " @classmethod\n", " def from_string(cls, emp_str):\n", " first, last, pay = emp_str.split('-')\n", " return cls(first, last, pay)\n", "\n", " @staticmethod\n", " def is_workday(day):\n", " if day.weekday() == 5 or day.weekday() == 6:\n", " return False\n", " return True\n", "\n", "\n", "emp_1 = Employee('Corey', 'Schafer', 50000)\n", "emp_2 = Employee('Test', 'Employee', 60000)\n", "\n", "Employee.set_raise_amt(1.05)\n", "\n", "print(Employee.raise_amt)\n", "print(emp_1.raise_amt)\n", "print(emp_2.raise_amt)\n", "\n", "emp_str_1 = 'John-Doe-70000'\n", "emp_str_2 = 'Steve-Smith-30000'\n", "emp_str_3 = 'Jane-Doe-90000'\n", "\n", "first, last, pay = emp_str_1.split('-')\n", "\n", "#new_emp_1 = Employee(first, last, pay)\n", "new_emp_1 = Employee.from_string(emp_str_1)\n", "\n", "print(new_emp_1.email)\n", "print(new_emp_1.pay)\n", "\n", "import datetime\n", "my_date = datetime.date(2016, 7, 11)\n", "\n", "print(Employee.is_workday(my_date))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Inheritance" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Sue.Smith@email.com\n", "--> Corey Schafer\n" ] } ], "source": [ "class Employee:\n", "\n", " raise_amt = 1.04\n", "\n", " def __init__(self, first, last, pay):\n", " self.first = first\n", " self.last = last\n", " self.email = first + '.' + last + '@email.com'\n", " self.pay = pay\n", "\n", " def fullname(self):\n", " return '{} {}'.format(self.first, self.last)\n", "\n", " def apply_raise(self):\n", " self.pay = int(self.pay * self.raise_amt)\n", "\n", "\n", "class Developer(Employee):\n", " raise_amt = 1.10\n", "\n", " def __init__(self, first, last, pay, prog_lang):\n", " super().__init__(first, last, pay)\n", " self.prog_lang = prog_lang\n", "\n", "\n", "class Manager(Employee):\n", "\n", " def __init__(self, first, last, pay, employees=None):\n", " super().__init__(first, last, pay)\n", " if employees is None:\n", " self.employees = []\n", " else:\n", " self.employees = employees\n", "\n", " def add_emp(self, emp):\n", " if emp not in self.employees:\n", " self.employees.append(emp)\n", "\n", " def remove_emp(self, emp):\n", " if emp in self.employees:\n", " self.employees.remove(emp)\n", "\n", " def print_emps(self):\n", " for emp in self.employees:\n", " print('-->', emp.fullname())\n", "\n", "\n", "dev_1 = Developer('Corey', 'Schafer', 50000, 'Python')\n", "dev_2 = Developer('Test', 'Employee', 60000, 'Java')\n", "\n", "mgr_1 = Manager('Sue', 'Smith', 90000, [dev_1])\n", "\n", "print(mgr_1.email)\n", "\n", "mgr_1.add_emp(dev_2)\n", "mgr_1.remove_emp(dev_2)\n", "\n", "mgr_1.print_emps()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Special Methods" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "13\n" ] } ], "source": [ "class Employee:\n", "\n", " raise_amt = 1.04\n", "\n", " def __init__(self, first, last, pay):\n", " self.first = first\n", " self.last = last\n", " self.email = first + '.' + last + '@email.com'\n", " self.pay = pay\n", "\n", " def fullname(self):\n", " return '{} {}'.format(self.first, self.last)\n", "\n", " def apply_raise(self):\n", " self.pay = int(self.pay * self.raise_amt)\n", "\n", " def __repr__(self):\n", " return \"Employee('{}', '{}', {})\".format(self.first, self.last, self.pay)\n", "\n", " def __str__(self):\n", " return '{} - {}'.format(self.fullname(), self.email)\n", "\n", " def __add__(self, other):\n", " return self.pay + other.pay\n", "\n", " def __len__(self):\n", " return len(self.fullname())\n", "\n", "\n", "emp_1 = Employee('Corey', 'Schafer', 50000)\n", "emp_2 = Employee('Test', 'Employee', 60000)\n", "\n", "# print(emp_1 + emp_2)\n", "\n", "print(len(emp_1))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Property Decorators - Getters, Setters, and Deleters" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Corey\n", "Corey.Schafer@email.com\n", "Corey Schafer\n", "Delete Name!\n" ] } ], "source": [ "class Employee:\n", "\n", " def __init__(self, first, last):\n", " self.first = first\n", " self.last = last\n", "\n", " @property\n", " def email(self):\n", " return '{}.{}@email.com'.format(self.first, self.last)\n", "\n", " @property\n", " def fullname(self):\n", " return '{} {}'.format(self.first, self.last)\n", " \n", " @fullname.setter\n", " def fullname(self, name):\n", " first, last = name.split(' ')\n", " self.first = first\n", " self.last = last\n", " \n", " @fullname.deleter\n", " def fullname(self):\n", " print('Delete Name!')\n", " self.first = None\n", " self.last = None\n", "\n", "\n", "emp_1 = Employee('John', 'Smith')\n", "emp_1.fullname = \"Corey Schafer\"\n", "\n", "print(emp_1.first)\n", "print(emp_1.email)\n", "print(emp_1.fullname)\n", "\n", "del emp_1.fullname" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Generator\n", "\n", "* [Difference between iterators and generators](https://stackoverflow.com/questions/2776829/difference-between-pythons-generators-and-iterators)\n", " * iterator is any object of a class that has ```__next__``` and ```__iter__``` methods (```___iter___``` returns self)\n", " * generator is a function that has ```yield```\n", " * iterator 是比較廣的概念(any generator is an iterator but not vice versa)generator 寫起來比較快,但 iterator 有 class 可以客製很多不同的行為\n", "* ```x**2 for x in range(100) if x%2 == 1``` 是一個 generator expression" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[2, 4, 8, 16, 32, 64, 128, 256, 512]\n", "2\n", "4\n", "8\n" ] } ], "source": [ "def pow2():\n", " n = 2\n", " while n < 1000:\n", " yield n\n", " n *= 2\n", "\n", "print([i for i in pow2()])\n", "\n", "a = pow2()\n", "\n", "print(next(a))\n", "print(next(a))\n", "print(next(a))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [Coroutine](https://www.youtube.com/watch?v=7AoANOGIDuM)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Received: 100\n", "1\n" ] } ], "source": [ "# 呼叫 next() 時會跑到 coro 裡的下一個 yield\n", "# 然後可以用 send 把值傳進正在跑的函數裡,同時 send 也會 return yield 的結果\n", "\n", "def coro():\n", " step = 0\n", " while True:\n", " received = yield step\n", " step += 1\n", " print(f'Received: {received}')\n", "\n", "c = coro()\n", "next(c) # important! get to the first yield\n", "print(c.send(100))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Decorator \n", "\n", "* 寫的很好的 [RealPython tutorial](https://realpython.com/primer-on-python-decorators/#stateful-decorators),整篇看完了但沒時間作筆記\n", "* 被 decorate 過的函數呼叫 ```.__name__``` 或 ```.__doc__```(```help()```)的時候會叫到 wrapper 的,所以才需要用 ```@functools.wraps(func)``` 把 func 的 name 和 docstring 抄給 wrapper\n", "* ```@debug``` 印下函數的 input/output,可以用寫 recursive 的時候 debug\n", "* [classes as decorators](https://realpython.com/primer-on-python-decorators/#classes-as-decorators),implement ```__init__``` 和 ```__call__```,可以存狀態,例如 lru_cache\n", "\n", "### General Pattern (No Argument)" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "import functools\n", "\n", "def decorator(func):\n", " @functools.wraps(func)\n", " def wrapper_decorator(*args, **kwargs):\n", " # Do something before\n", " value = func(*args, **kwargs)\n", " # Do something after\n", " return value\n", " return wrapper_decorator" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Decorator fix_seed" ] }, { "cell_type": "code", "execution_count": 367, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.5488135039273248\n", "0.6161167995056092\n" ] } ], "source": [ "# fix_seed:固定 seed = 0 版本。離開函數 seed 會還原成 None\n", "\n", "import numpy as np\n", "import functools\n", "\n", "def fix_seed(fnc):\n", " @functools.wraps(fnc)\n", " def wrapper_fix_seed(*args, **kargs):\n", " np.random.seed(0)\n", " res = fnc(*args, **kargs)\n", " np.random.seed()\n", " return res\n", " return wrapper_fix_seed\n", "\n", "@fix_seed\n", "def printRand():\n", " print(np.random.uniform())\n", " \n", "printRand()\n", "print(np.random.uniform())" ] }, { "cell_type": "code", "execution_count": 377, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.5434049417909654\n", "0.3289099673526439\n" ] } ], "source": [ "# 接受 argument 版本,但變成一定要指定 seed\n", "\n", "import numpy as np\n", "import functools\n", "\n", "def fix_seed(seed=0):\n", " def decorator_fix_seed(fnc):\n", " @functools.wraps(fnc)\n", " def wrapper_fix_seed(*args, **kargs):\n", " np.random.seed(seed)\n", " res = fnc(*args, **kargs)\n", " np.random.seed()\n", " return res\n", " return wrapper_fix_seed\n", " return decorator_fix_seed\n", "\n", "@fix_seed(100)\n", "def printRand():\n", " print(np.random.uniform())\n", " \n", "printRand()\n", "print(np.random.uniform())" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.5488135039273248\n", "0.13056825103667768\n" ] } ], "source": [ "# 可以指定也可以不指定。若不指定 seed 預設為 0。若要指定一定要寫 seed=\n", "\n", "# 有指定 seed 的時候相當於 printRand = fix_seed(seed=0)(printRand),所以 _func 是 None\n", "# 不指定 seed 的時候則變成 printRand = fix_seed(printRand) 把 function 傳進去\n", "\n", "import numpy as np\n", "import functools\n", "\n", "def fix_seed(_func=None, *, seed=0):\n", " def decorator_fix_seed(func):\n", " @functools.wraps(func)\n", " def wrapper_fix_seed(*args, **kwargs):\n", " np.random.seed(seed)\n", " res = func(*args, **kwargs)\n", " np.random.seed()\n", " return res\n", " return wrapper_fix_seed\n", "\n", " if _func:\n", " return decorator_fix_seed(_func)\n", " else:\n", " return decorator_fix_seed\n", "\n", " \n", "# @fix_seed(0) # TypeError: 'int' object is not callable\n", "# @fix_seed(seed=0)\n", "@fix_seed\n", "def printRand():\n", " print(np.random.uniform())\n", " \n", "printRand()\n", "print(np.random.uniform()) " ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" }, "toc-autonumbering": false, "toc-showcode": false, "toc-showmarkdowntxt": false }, "nbformat": 4, "nbformat_minor": 4 }