From e8d5b05d6d197da14b078a6809c58d5d0277515a Mon Sep 17 00:00:00 2001 From: Edoardo Baldi Date: Fri, 10 Apr 2026 16:53:57 +0200 Subject: [PATCH] Add notebook with decorators example --- 12_functions_advanced.ipynb | 493 +++++++++--------- extra/12a_decorators_retry_validate.ipynb | 591 ++++++++++++++++++++++ 2 files changed, 853 insertions(+), 231 deletions(-) create mode 100644 extra/12a_decorators_retry_validate.ipynb diff --git a/12_functions_advanced.ipynb b/12_functions_advanced.ipynb index 3950c49b..42d4a39d 100644 --- a/12_functions_advanced.ipynb +++ b/12_functions_advanced.ipynb @@ -56,9 +56,7 @@ { "cell_type": "markdown", "id": "2", - "metadata": { - "jp-MarkdownHeadingCollapsed": true - }, + "metadata": {}, "source": [ "## Recap" ] @@ -295,7 +293,9 @@ { "cell_type": "markdown", "id": "19", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "## Mutable objects as default values of function's parameters" ] @@ -745,7 +745,9 @@ { "cell_type": "markdown", "id": "57", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "## Lambdas" ] @@ -2005,7 +2007,7 @@ "metadata": {}, "outputs": [], "source": [ - "f = incrementer(2)\n", + "f = incrementer(n=2)\n", "f.__code__.co_freevars" ] }, @@ -2043,14 +2045,16 @@ "metadata": {}, "outputs": [], "source": [ - "inc_10 = incrementer(10)(100)\n", + "inc_10 = incrementer(n=10)(start=100)\n", "inc_10()" ] }, { "cell_type": "markdown", "id": "177", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "### Quiz: Closures" ] @@ -2072,7 +2076,9 @@ { "cell_type": "markdown", "id": "179", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "### Closures: examples" ] @@ -2511,7 +2517,7 @@ }, "outputs": [], "source": [ - "def fcounter(function):\n", + "def function_counter(function):\n", " count = 0\n", "\n", " def inner(*args, **kwargs):\n", @@ -2556,7 +2562,7 @@ "metadata": {}, "outputs": [], "source": [ - "counter_fact = fcounter(factorial)" + "counter_fact = function_counter(factorial)" ] }, { @@ -2627,7 +2633,7 @@ "metadata": {}, "outputs": [], "source": [ - "@fcounter\n", + "@function_counter\n", "def mult(a: float, b: float) -> float:\n", " \"\"\"Multiplies two floats\"\"\"\n", " return a * b" @@ -2895,13 +2901,21 @@ "id": "250", "metadata": {}, "source": [ - "#### Example 1: timer" + "Check out also the notebook in the `extra/` folder: [Decorators example: retry & validate](./extra/12a_decorators_retry_validate.ipynb) for two examples of usage patterns built from scratch." ] }, { "cell_type": "markdown", "id": "251", "metadata": {}, + "source": [ + "#### Example 1: timer" + ] + }, + { + "cell_type": "markdown", + "id": "252", + "metadata": {}, "source": [ "This is classic example of using decorators: creating a timer for a generic function." ] @@ -2909,7 +2923,7 @@ { "cell_type": "code", "execution_count": null, - "id": "252", + "id": "253", "metadata": { "lines_to_next_cell": 2 }, @@ -2943,7 +2957,7 @@ }, { "cell_type": "markdown", - "id": "253", + "id": "254", "metadata": {}, "source": [ "Let's test it with a function to calculate the n-th Fibonacci number: `1, 1, 2, 3, 5, 8, 11, ...`\n", @@ -2959,7 +2973,7 @@ }, { "cell_type": "markdown", - "id": "254", + "id": "255", "metadata": { "lines_to_next_cell": 2 }, @@ -2970,7 +2984,7 @@ { "cell_type": "code", "execution_count": null, - "id": "255", + "id": "256", "metadata": {}, "outputs": [], "source": [ @@ -2983,7 +2997,7 @@ { "cell_type": "code", "execution_count": null, - "id": "256", + "id": "257", "metadata": {}, "outputs": [], "source": [ @@ -2993,7 +3007,7 @@ { "cell_type": "code", "execution_count": null, - "id": "257", + "id": "258", "metadata": {}, "outputs": [], "source": [ @@ -3003,7 +3017,7 @@ { "cell_type": "code", "execution_count": null, - "id": "258", + "id": "259", "metadata": {}, "outputs": [], "source": [ @@ -3015,7 +3029,7 @@ { "cell_type": "code", "execution_count": null, - "id": "259", + "id": "260", "metadata": {}, "outputs": [], "source": [ @@ -3025,7 +3039,7 @@ { "cell_type": "code", "execution_count": null, - "id": "260", + "id": "261", "metadata": {}, "outputs": [], "source": [ @@ -3035,7 +3049,7 @@ { "cell_type": "code", "execution_count": null, - "id": "261", + "id": "262", "metadata": { "lines_to_next_cell": 2 }, @@ -3046,7 +3060,7 @@ }, { "cell_type": "markdown", - "id": "262", + "id": "263", "metadata": {}, "source": [ "Sounds a bit long, doesn't it?\n", @@ -3056,7 +3070,7 @@ }, { "cell_type": "markdown", - "id": "263", + "id": "264", "metadata": { "lines_to_next_cell": 2 }, @@ -3067,7 +3081,7 @@ { "cell_type": "code", "execution_count": null, - "id": "264", + "id": "265", "metadata": {}, "outputs": [], "source": [ @@ -3085,7 +3099,7 @@ { "cell_type": "code", "execution_count": null, - "id": "265", + "id": "266", "metadata": {}, "outputs": [], "source": [ @@ -3095,7 +3109,7 @@ }, { "cell_type": "markdown", - "id": "266", + "id": "267", "metadata": {}, "source": [ "Incredibly more efficient!\n", @@ -3104,7 +3118,7 @@ }, { "cell_type": "markdown", - "id": "267", + "id": "268", "metadata": {}, "source": [ "##### Fibonacci using `reduce`" @@ -3112,7 +3126,7 @@ }, { "cell_type": "markdown", - "id": "268", + "id": "269", "metadata": {}, "source": [ "First, a quick refresher:" @@ -3121,7 +3135,7 @@ { "cell_type": "code", "execution_count": null, - "id": "269", + "id": "270", "metadata": {}, "outputs": [], "source": [ @@ -3131,7 +3145,7 @@ { "cell_type": "code", "execution_count": null, - "id": "270", + "id": "271", "metadata": { "lines_to_next_cell": 2 }, @@ -3142,7 +3156,7 @@ }, { "cell_type": "markdown", - "id": "271", + "id": "272", "metadata": {}, "source": [ "It's just the progressive sum of pairs of numbers.\n", @@ -3155,7 +3169,7 @@ }, { "cell_type": "markdown", - "id": "272", + "id": "273", "metadata": { "lines_to_next_cell": 2 }, @@ -3190,7 +3204,7 @@ { "cell_type": "code", "execution_count": null, - "id": "273", + "id": "274", "metadata": {}, "outputs": [], "source": [ @@ -3204,7 +3218,7 @@ { "cell_type": "code", "execution_count": null, - "id": "274", + "id": "275", "metadata": {}, "outputs": [], "source": [ @@ -3214,7 +3228,7 @@ }, { "cell_type": "markdown", - "id": "275", + "id": "276", "metadata": {}, "source": [ "If we compare the three methods:" @@ -3223,7 +3237,7 @@ { "cell_type": "code", "execution_count": null, - "id": "276", + "id": "277", "metadata": { "lines_to_next_cell": 2 }, @@ -3236,7 +3250,7 @@ }, { "cell_type": "markdown", - "id": "277", + "id": "278", "metadata": {}, "source": [ "Although the recursive method is the __easiest__ to understand, it's also the slowest because it's written inefficiently.\n", @@ -3245,7 +3259,7 @@ }, { "cell_type": "markdown", - "id": "278", + "id": "279", "metadata": {}, "source": [ "#### Example 2: memoization" @@ -3253,7 +3267,7 @@ }, { "cell_type": "markdown", - "id": "279", + "id": "280", "metadata": {}, "source": [ "The previous example showed one task that a decorator can accomplish pretty well: adding some feature to a predefined function.\n", @@ -3262,7 +3276,7 @@ }, { "cell_type": "markdown", - "id": "280", + "id": "281", "metadata": { "lines_to_next_cell": 2 }, @@ -3274,7 +3288,7 @@ { "cell_type": "code", "execution_count": null, - "id": "281", + "id": "282", "metadata": {}, "outputs": [], "source": [ @@ -3286,7 +3300,7 @@ { "cell_type": "code", "execution_count": null, - "id": "282", + "id": "283", "metadata": { "lines_to_next_cell": 2 }, @@ -3297,7 +3311,7 @@ }, { "cell_type": "markdown", - "id": "283", + "id": "284", "metadata": {}, "source": [ "You can see that `fib(2)` is calculated **three times**.\n", @@ -3307,7 +3321,7 @@ }, { "cell_type": "markdown", - "id": "284", + "id": "285", "metadata": {}, "source": [ "We'll see how we can improve this approach using a decorator and a caching mechanism for previously calculated numbers.\n", @@ -3316,7 +3330,7 @@ }, { "cell_type": "markdown", - "id": "285", + "id": "286", "metadata": { "lines_to_next_cell": 2 }, @@ -3327,7 +3341,7 @@ { "cell_type": "code", "execution_count": null, - "id": "286", + "id": "287", "metadata": {}, "outputs": [], "source": [ @@ -3345,7 +3359,7 @@ { "cell_type": "code", "execution_count": null, - "id": "287", + "id": "288", "metadata": {}, "outputs": [], "source": [ @@ -3356,7 +3370,7 @@ { "cell_type": "code", "execution_count": null, - "id": "288", + "id": "289", "metadata": { "lines_to_next_cell": 2 }, @@ -3367,7 +3381,7 @@ }, { "cell_type": "markdown", - "id": "289", + "id": "290", "metadata": { "lines_to_next_cell": 2 }, @@ -3380,7 +3394,7 @@ { "cell_type": "code", "execution_count": null, - "id": "290", + "id": "291", "metadata": {}, "outputs": [], "source": [ @@ -3400,7 +3414,7 @@ { "cell_type": "code", "execution_count": null, - "id": "291", + "id": "292", "metadata": {}, "outputs": [], "source": [ @@ -3411,7 +3425,7 @@ { "cell_type": "code", "execution_count": null, - "id": "292", + "id": "293", "metadata": {}, "outputs": [], "source": [ @@ -3420,7 +3434,7 @@ }, { "cell_type": "markdown", - "id": "293", + "id": "294", "metadata": {}, "source": [ "Once again, cached valued are just returned and not recalculated." @@ -3428,7 +3442,7 @@ }, { "cell_type": "markdown", - "id": "294", + "id": "295", "metadata": {}, "source": [ "How can we implement this as a decorator?" @@ -3437,7 +3451,7 @@ { "cell_type": "code", "execution_count": null, - "id": "295", + "id": "296", "metadata": {}, "outputs": [], "source": [ @@ -3459,7 +3473,7 @@ { "cell_type": "code", "execution_count": null, - "id": "296", + "id": "297", "metadata": {}, "outputs": [], "source": [ @@ -3472,7 +3486,7 @@ { "cell_type": "code", "execution_count": null, - "id": "297", + "id": "298", "metadata": {}, "outputs": [], "source": [ @@ -3482,7 +3496,7 @@ { "cell_type": "code", "execution_count": null, - "id": "298", + "id": "299", "metadata": {}, "outputs": [], "source": [ @@ -3492,7 +3506,7 @@ { "cell_type": "code", "execution_count": null, - "id": "299", + "id": "300", "metadata": { "lines_to_next_cell": 2 }, @@ -3503,7 +3517,7 @@ }, { "cell_type": "markdown", - "id": "300", + "id": "301", "metadata": {}, "source": [ "`fib(6)` was literally instantaneous because we already had it in the cache." @@ -3511,7 +3525,7 @@ }, { "cell_type": "markdown", - "id": "301", + "id": "302", "metadata": { "lines_to_next_cell": 2 }, @@ -3523,7 +3537,7 @@ { "cell_type": "code", "execution_count": null, - "id": "302", + "id": "303", "metadata": { "lines_to_next_cell": 2 }, @@ -3543,7 +3557,7 @@ }, { "cell_type": "markdown", - "id": "303", + "id": "304", "metadata": { "lines_to_next_cell": 2 }, @@ -3554,7 +3568,7 @@ { "cell_type": "code", "execution_count": null, - "id": "304", + "id": "305", "metadata": {}, "outputs": [], "source": [ @@ -3567,7 +3581,7 @@ { "cell_type": "code", "execution_count": null, - "id": "305", + "id": "306", "metadata": {}, "outputs": [], "source": [ @@ -3577,7 +3591,7 @@ { "cell_type": "code", "execution_count": null, - "id": "306", + "id": "307", "metadata": {}, "outputs": [], "source": [ @@ -3587,7 +3601,7 @@ { "cell_type": "code", "execution_count": null, - "id": "307", + "id": "308", "metadata": {}, "outputs": [], "source": [ @@ -3596,7 +3610,7 @@ }, { "cell_type": "markdown", - "id": "308", + "id": "309", "metadata": {}, "source": [ "Caching and decorators play a crucial role in optimizing function performance.\n", @@ -3609,7 +3623,7 @@ }, { "cell_type": "markdown", - "id": "309", + "id": "310", "metadata": {}, "source": [ "Additionally, our current implementation does not handle keyword arguments (`**kwargs`), which can be a significant limitation in more complex scenarios.\n", @@ -3623,7 +3637,7 @@ { "cell_type": "code", "execution_count": null, - "id": "310", + "id": "311", "metadata": {}, "outputs": [], "source": [ @@ -3633,7 +3647,7 @@ { "cell_type": "code", "execution_count": null, - "id": "311", + "id": "312", "metadata": {}, "outputs": [], "source": [ @@ -3646,7 +3660,7 @@ { "cell_type": "code", "execution_count": null, - "id": "312", + "id": "313", "metadata": {}, "outputs": [], "source": [ @@ -3656,7 +3670,7 @@ }, { "cell_type": "markdown", - "id": "313", + "id": "314", "metadata": {}, "source": [ "Once again, the last value `fact(8)` was simply fetched from the cache." @@ -3664,7 +3678,7 @@ }, { "cell_type": "markdown", - "id": "314", + "id": "315", "metadata": {}, "source": [ "Now let's see if we have improved on our recursive approach of calculating Fibonacci numbers.\n", @@ -3674,7 +3688,7 @@ { "cell_type": "code", "execution_count": null, - "id": "315", + "id": "316", "metadata": {}, "outputs": [], "source": [ @@ -3684,7 +3698,7 @@ { "cell_type": "code", "execution_count": null, - "id": "316", + "id": "317", "metadata": {}, "outputs": [], "source": [ @@ -3695,7 +3709,7 @@ { "cell_type": "code", "execution_count": null, - "id": "317", + "id": "318", "metadata": {}, "outputs": [], "source": [ @@ -3709,7 +3723,7 @@ { "cell_type": "code", "execution_count": null, - "id": "318", + "id": "319", "metadata": {}, "outputs": [], "source": [ @@ -3721,7 +3735,7 @@ { "cell_type": "code", "execution_count": null, - "id": "319", + "id": "320", "metadata": {}, "outputs": [], "source": [ @@ -3734,7 +3748,7 @@ }, { "cell_type": "markdown", - "id": "320", + "id": "321", "metadata": {}, "source": [ "It's about **4 orders of magnitude** faster than the naive approach! 🔥\n", @@ -3744,7 +3758,7 @@ { "cell_type": "code", "execution_count": null, - "id": "321", + "id": "322", "metadata": {}, "outputs": [], "source": [ @@ -3758,7 +3772,7 @@ { "cell_type": "code", "execution_count": null, - "id": "322", + "id": "323", "metadata": { "lines_to_next_cell": 2 }, @@ -3773,7 +3787,7 @@ }, { "cell_type": "markdown", - "id": "323", + "id": "324", "metadata": {}, "source": [ "Not the same time, but about the same order of magnitude.\n", @@ -3782,7 +3796,7 @@ }, { "cell_type": "markdown", - "id": "324", + "id": "325", "metadata": { "lines_to_next_cell": 2 }, @@ -3795,7 +3809,7 @@ { "cell_type": "code", "execution_count": null, - "id": "325", + "id": "326", "metadata": {}, "outputs": [], "source": [ @@ -3808,7 +3822,7 @@ { "cell_type": "code", "execution_count": null, - "id": "326", + "id": "327", "metadata": {}, "outputs": [], "source": [ @@ -3818,7 +3832,7 @@ { "cell_type": "code", "execution_count": null, - "id": "327", + "id": "328", "metadata": {}, "outputs": [], "source": [ @@ -3828,7 +3842,7 @@ { "cell_type": "code", "execution_count": null, - "id": "328", + "id": "329", "metadata": {}, "outputs": [], "source": [ @@ -3837,7 +3851,7 @@ }, { "cell_type": "markdown", - "id": "329", + "id": "330", "metadata": {}, "source": [ "We had to recalculate `fib(1)` because when we called `fib(9)` the least recent item in the cache (the result of `fib(1)`) was evicted from the cache." @@ -3845,7 +3859,7 @@ }, { "cell_type": "markdown", - "id": "330", + "id": "331", "metadata": {}, "source": [ "### Parametrized decorators" @@ -3853,7 +3867,7 @@ }, { "cell_type": "markdown", - "id": "331", + "id": "332", "metadata": {}, "source": [ "Here comes a natural question: what if I need to pass some argument to my decorator?\n", @@ -3862,7 +3876,7 @@ }, { "cell_type": "markdown", - "id": "332", + "id": "333", "metadata": {}, "source": [ "Let's bring back our `timed` decorator and make a small change.\n", @@ -3872,7 +3886,7 @@ { "cell_type": "code", "execution_count": null, - "id": "333", + "id": "334", "metadata": {}, "outputs": [], "source": [ @@ -3901,7 +3915,7 @@ { "cell_type": "code", "execution_count": null, - "id": "334", + "id": "335", "metadata": {}, "outputs": [], "source": [ @@ -3917,7 +3931,7 @@ { "cell_type": "code", "execution_count": null, - "id": "335", + "id": "336", "metadata": { "lines_to_next_cell": 2 }, @@ -3928,7 +3942,7 @@ }, { "cell_type": "markdown", - "id": "336", + "id": "337", "metadata": { "lines_to_next_cell": 2 }, @@ -3943,7 +3957,7 @@ { "cell_type": "code", "execution_count": null, - "id": "337", + "id": "338", "metadata": {}, "outputs": [], "source": [ @@ -3968,7 +3982,7 @@ { "cell_type": "code", "execution_count": null, - "id": "338", + "id": "339", "metadata": {}, "outputs": [], "source": [ @@ -3982,7 +3996,7 @@ { "cell_type": "code", "execution_count": null, - "id": "339", + "id": "340", "metadata": {}, "outputs": [], "source": [ @@ -3991,7 +4005,7 @@ }, { "cell_type": "markdown", - "id": "340", + "id": "341", "metadata": {}, "source": [ "But wait: why did we use the fancy `@-` syntax?\n", @@ -4000,7 +4014,7 @@ }, { "cell_type": "markdown", - "id": "341", + "id": "342", "metadata": {}, "source": [ "To fix this behavior we need to rethink of what `@` is doing.\n", @@ -4024,7 +4038,7 @@ { "cell_type": "code", "execution_count": null, - "id": "342", + "id": "343", "metadata": {}, "outputs": [], "source": [ @@ -4034,7 +4048,7 @@ { "cell_type": "code", "execution_count": null, - "id": "343", + "id": "344", "metadata": {}, "outputs": [], "source": [ @@ -4043,7 +4057,7 @@ }, { "cell_type": "markdown", - "id": "344", + "id": "345", "metadata": {}, "source": [ "So, for the syntax `@timed(10)` to work, where `10` is the number of repetition, `timed` should return **a decorator itself**, and not our closure.\n", @@ -4053,7 +4067,7 @@ { "cell_type": "code", "execution_count": null, - "id": "345", + "id": "346", "metadata": {}, "outputs": [], "source": [ @@ -4086,7 +4100,7 @@ { "cell_type": "code", "execution_count": null, - "id": "346", + "id": "347", "metadata": {}, "outputs": [], "source": [ @@ -4102,7 +4116,7 @@ { "cell_type": "code", "execution_count": null, - "id": "347", + "id": "348", "metadata": {}, "outputs": [], "source": [ @@ -4112,7 +4126,7 @@ { "cell_type": "code", "execution_count": null, - "id": "348", + "id": "349", "metadata": {}, "outputs": [], "source": [ @@ -4132,7 +4146,7 @@ { "cell_type": "code", "execution_count": null, - "id": "349", + "id": "350", "metadata": {}, "outputs": [], "source": [ @@ -4141,7 +4155,7 @@ }, { "cell_type": "markdown", - "id": "350", + "id": "351", "metadata": {}, "source": [ "And yes, you can **stack multiple decorators**! 😎" @@ -4149,7 +4163,7 @@ }, { "cell_type": "markdown", - "id": "351", + "id": "352", "metadata": {}, "source": [ "### Quiz: Decorators" @@ -4158,7 +4172,7 @@ { "cell_type": "code", "execution_count": null, - "id": "352", + "id": "353", "metadata": { "lines_to_next_cell": 2 }, @@ -4171,15 +4185,17 @@ }, { "cell_type": "markdown", - "id": "353", - "metadata": {}, + "id": "354", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "## Generators" ] }, { "cell_type": "markdown", - "id": "354", + "id": "355", "metadata": {}, "source": [ "The concept of generators is very much tied to that of \"looping over some kind of container\".\n", @@ -4196,7 +4212,7 @@ }, { "cell_type": "markdown", - "id": "355", + "id": "356", "metadata": {}, "source": [ "To understand generators, we first need to review what it means to be **iterable** and, more importantly, what is an **iterator**.\n", @@ -4207,7 +4223,7 @@ }, { "cell_type": "markdown", - "id": "356", + "id": "357", "metadata": { "lines_to_next_cell": 2 }, @@ -4220,7 +4236,7 @@ { "cell_type": "code", "execution_count": null, - "id": "357", + "id": "358", "metadata": {}, "outputs": [], "source": [ @@ -4243,7 +4259,7 @@ { "cell_type": "code", "execution_count": null, - "id": "358", + "id": "359", "metadata": { "lines_to_next_cell": 2 }, @@ -4255,7 +4271,7 @@ }, { "cell_type": "markdown", - "id": "359", + "id": "360", "metadata": {}, "source": [ "We see that we can indeed loop over our custom `Squares` class.\n", @@ -4268,7 +4284,7 @@ }, { "cell_type": "markdown", - "id": "360", + "id": "361", "metadata": {}, "source": [ "As you might have learned by now, we can implement some built-in behavior in our classes by using the so-called \"special methods\" or **dunder methods**: `__method__`.\n", @@ -4282,7 +4298,7 @@ }, { "cell_type": "markdown", - "id": "361", + "id": "362", "metadata": {}, "source": [ "Python also has the built-in `next()` which does what you think: it takes an **iterator** object and returns the **next element** in the stream of data by calling the `__next__` method implemented by that object.\n", @@ -4295,7 +4311,7 @@ }, { "cell_type": "markdown", - "id": "362", + "id": "363", "metadata": { "lines_to_next_cell": 2 }, @@ -4306,7 +4322,7 @@ { "cell_type": "code", "execution_count": null, - "id": "363", + "id": "364", "metadata": {}, "outputs": [], "source": [ @@ -4327,7 +4343,7 @@ { "cell_type": "code", "execution_count": null, - "id": "364", + "id": "365", "metadata": { "lines_to_next_cell": 2 }, @@ -4339,7 +4355,7 @@ }, { "cell_type": "markdown", - "id": "365", + "id": "366", "metadata": {}, "source": [ "If the value returned by `square()` is 25 (our sentinel), then a `StopIteration` is raised." @@ -4347,7 +4363,7 @@ }, { "cell_type": "markdown", - "id": "366", + "id": "367", "metadata": {}, "source": [ "These two ways are identical: in the first case (the class), we built the iterator ourselves. In the second case, Python built it for us.\n", @@ -4358,7 +4374,7 @@ }, { "cell_type": "markdown", - "id": "367", + "id": "368", "metadata": { "lines_to_next_cell": 2 }, @@ -4377,7 +4393,7 @@ { "cell_type": "code", "execution_count": null, - "id": "368", + "id": "369", "metadata": {}, "outputs": [], "source": [ @@ -4393,7 +4409,7 @@ { "cell_type": "code", "execution_count": null, - "id": "369", + "id": "370", "metadata": {}, "outputs": [], "source": [ @@ -4403,7 +4419,7 @@ }, { "cell_type": "markdown", - "id": "370", + "id": "371", "metadata": {}, "source": [ "Here it is: our function returned _something_ different than the usual \"function\" object.\n", @@ -4413,7 +4429,7 @@ { "cell_type": "code", "execution_count": null, - "id": "371", + "id": "372", "metadata": {}, "outputs": [], "source": [ @@ -4423,7 +4439,7 @@ { "cell_type": "code", "execution_count": null, - "id": "372", + "id": "373", "metadata": {}, "outputs": [], "source": [ @@ -4433,7 +4449,7 @@ { "cell_type": "code", "execution_count": null, - "id": "373", + "id": "374", "metadata": {}, "outputs": [], "source": [ @@ -4443,7 +4459,7 @@ { "cell_type": "code", "execution_count": null, - "id": "374", + "id": "375", "metadata": {}, "outputs": [], "source": [ @@ -4452,7 +4468,7 @@ }, { "cell_type": "markdown", - "id": "375", + "id": "376", "metadata": {}, "source": [ "A `StopIteration` is raised if we are trying to go past the last `yield` statement.\n", @@ -4465,7 +4481,7 @@ { "cell_type": "code", "execution_count": null, - "id": "376", + "id": "377", "metadata": {}, "outputs": [], "source": [ @@ -4474,7 +4490,7 @@ }, { "cell_type": "markdown", - "id": "377", + "id": "378", "metadata": {}, "source": [ "And also the `__next__` method" @@ -4483,7 +4499,7 @@ { "cell_type": "code", "execution_count": null, - "id": "378", + "id": "379", "metadata": {}, "outputs": [], "source": [ @@ -4492,7 +4508,7 @@ }, { "cell_type": "markdown", - "id": "379", + "id": "380", "metadata": {}, "source": [ "We can also check that `iter()` applied on our object returns indeed the same thing.\n", @@ -4502,7 +4518,7 @@ { "cell_type": "code", "execution_count": null, - "id": "380", + "id": "381", "metadata": {}, "outputs": [], "source": [ @@ -4512,7 +4528,7 @@ { "cell_type": "code", "execution_count": null, - "id": "381", + "id": "382", "metadata": {}, "outputs": [], "source": [ @@ -4521,7 +4537,7 @@ }, { "cell_type": "markdown", - "id": "382", + "id": "383", "metadata": {}, "source": [ "Precisely the same object." @@ -4529,7 +4545,7 @@ }, { "cell_type": "markdown", - "id": "383", + "id": "384", "metadata": {}, "source": [ "How does Python know when to stop the iteration?\n", @@ -4546,7 +4562,7 @@ }, { "cell_type": "markdown", - "id": "384", + "id": "385", "metadata": {}, "source": [ "Here's a generator that yields numbers and returns a message at the end:" @@ -4555,7 +4571,7 @@ { "cell_type": "code", "execution_count": null, - "id": "385", + "id": "386", "metadata": { "lines_to_next_cell": 1 }, @@ -4570,7 +4586,7 @@ }, { "cell_type": "markdown", - "id": "386", + "id": "387", "metadata": {}, "source": [ "When we iterate through the generator in a for loop, we only see the yielded values:" @@ -4579,7 +4595,7 @@ { "cell_type": "code", "execution_count": null, - "id": "387", + "id": "388", "metadata": {}, "outputs": [], "source": [ @@ -4589,7 +4605,7 @@ }, { "cell_type": "markdown", - "id": "388", + "id": "389", "metadata": {}, "source": [ "But we can capture the return value by catching the StopIteration exception:" @@ -4598,7 +4614,7 @@ { "cell_type": "code", "execution_count": null, - "id": "389", + "id": "390", "metadata": { "lines_to_next_cell": 1 }, @@ -4615,7 +4631,7 @@ }, { "cell_type": "markdown", - "id": "390", + "id": "391", "metadata": {}, "source": [ "This ability to attach return values to generators is used internally by Python features like `yield from`.\n", @@ -4626,7 +4642,7 @@ { "cell_type": "code", "execution_count": null, - "id": "391", + "id": "392", "metadata": { "lines_to_next_cell": 1 }, @@ -4642,7 +4658,7 @@ { "cell_type": "code", "execution_count": null, - "id": "392", + "id": "393", "metadata": { "lines_to_next_cell": 1 }, @@ -4658,7 +4674,7 @@ { "cell_type": "code", "execution_count": null, - "id": "393", + "id": "394", "metadata": {}, "outputs": [], "source": [ @@ -4669,7 +4685,7 @@ }, { "cell_type": "markdown", - "id": "394", + "id": "395", "metadata": { "lines_to_next_cell": 2 }, @@ -4683,7 +4699,7 @@ { "cell_type": "code", "execution_count": null, - "id": "395", + "id": "396", "metadata": {}, "outputs": [], "source": [ @@ -4700,7 +4716,7 @@ { "cell_type": "code", "execution_count": null, - "id": "396", + "id": "397", "metadata": {}, "outputs": [], "source": [ @@ -4711,7 +4727,7 @@ { "cell_type": "code", "execution_count": null, - "id": "397", + "id": "398", "metadata": {}, "outputs": [], "source": [ @@ -4721,7 +4737,7 @@ { "cell_type": "code", "execution_count": null, - "id": "398", + "id": "399", "metadata": {}, "outputs": [], "source": [ @@ -4731,7 +4747,7 @@ { "cell_type": "code", "execution_count": null, - "id": "399", + "id": "400", "metadata": { "lines_to_next_cell": 2 }, @@ -4742,7 +4758,7 @@ }, { "cell_type": "markdown", - "id": "400", + "id": "401", "metadata": {}, "source": [ "Note how in the generator function above we incremented the number `i` **after** the `yield` statement.\n", @@ -4751,7 +4767,7 @@ }, { "cell_type": "markdown", - "id": "401", + "id": "402", "metadata": {}, "source": [ "### Create an interable from a generator" @@ -4759,7 +4775,7 @@ }, { "cell_type": "markdown", - "id": "402", + "id": "403", "metadata": {}, "source": [ "As we know, generators are iterators.\n", @@ -4771,7 +4787,7 @@ }, { "cell_type": "markdown", - "id": "403", + "id": "404", "metadata": { "lines_to_next_cell": 2 }, @@ -4782,7 +4798,7 @@ { "cell_type": "code", "execution_count": null, - "id": "404", + "id": "405", "metadata": {}, "outputs": [], "source": [ @@ -4794,7 +4810,7 @@ { "cell_type": "code", "execution_count": null, - "id": "405", + "id": "406", "metadata": {}, "outputs": [], "source": [ @@ -4804,7 +4820,7 @@ { "cell_type": "code", "execution_count": null, - "id": "406", + "id": "407", "metadata": {}, "outputs": [], "source": [ @@ -4814,7 +4830,7 @@ }, { "cell_type": "markdown", - "id": "407", + "id": "408", "metadata": {}, "source": [ "But our generator is now exhausted and it has nothing left to return:" @@ -4823,7 +4839,7 @@ { "cell_type": "code", "execution_count": null, - "id": "408", + "id": "409", "metadata": { "lines_to_next_cell": 2 }, @@ -4834,7 +4850,7 @@ }, { "cell_type": "markdown", - "id": "409", + "id": "410", "metadata": { "lines_to_next_cell": 2 }, @@ -4846,7 +4862,7 @@ { "cell_type": "code", "execution_count": null, - "id": "410", + "id": "411", "metadata": {}, "outputs": [], "source": [ @@ -4861,7 +4877,7 @@ { "cell_type": "code", "execution_count": null, - "id": "411", + "id": "412", "metadata": {}, "outputs": [], "source": [ @@ -4871,7 +4887,7 @@ }, { "cell_type": "markdown", - "id": "412", + "id": "413", "metadata": {}, "source": [ "And we can do it again:" @@ -4880,7 +4896,7 @@ { "cell_type": "code", "execution_count": null, - "id": "413", + "id": "414", "metadata": { "lines_to_next_cell": 2 }, @@ -4891,7 +4907,7 @@ }, { "cell_type": "markdown", - "id": "414", + "id": "415", "metadata": { "lines_to_next_cell": 2 }, @@ -4902,7 +4918,7 @@ { "cell_type": "code", "execution_count": null, - "id": "415", + "id": "416", "metadata": {}, "outputs": [], "source": [ @@ -4922,7 +4938,7 @@ { "cell_type": "code", "execution_count": null, - "id": "416", + "id": "417", "metadata": {}, "outputs": [], "source": [ @@ -4932,7 +4948,7 @@ { "cell_type": "code", "execution_count": null, - "id": "417", + "id": "418", "metadata": {}, "outputs": [], "source": [ @@ -4942,7 +4958,7 @@ { "cell_type": "code", "execution_count": null, - "id": "418", + "id": "419", "metadata": { "lines_to_next_cell": 2 }, @@ -4953,7 +4969,7 @@ }, { "cell_type": "markdown", - "id": "419", + "id": "420", "metadata": {}, "source": [ "### Combining generators" @@ -4961,7 +4977,7 @@ }, { "cell_type": "markdown", - "id": "420", + "id": "421", "metadata": { "lines_to_next_cell": 2 }, @@ -4973,7 +4989,7 @@ { "cell_type": "code", "execution_count": null, - "id": "421", + "id": "422", "metadata": {}, "outputs": [], "source": [ @@ -4985,7 +5001,7 @@ { "cell_type": "code", "execution_count": null, - "id": "422", + "id": "423", "metadata": {}, "outputs": [], "source": [ @@ -4995,7 +5011,7 @@ { "cell_type": "code", "execution_count": null, - "id": "423", + "id": "424", "metadata": {}, "outputs": [], "source": [ @@ -5004,7 +5020,7 @@ }, { "cell_type": "markdown", - "id": "424", + "id": "425", "metadata": {}, "source": [ "Now, `enumerate` builds a generator itself, so `sq` had not been consumed yet at this point:" @@ -5013,7 +5029,7 @@ { "cell_type": "code", "execution_count": null, - "id": "425", + "id": "426", "metadata": {}, "outputs": [], "source": [ @@ -5023,7 +5039,7 @@ { "cell_type": "code", "execution_count": null, - "id": "426", + "id": "427", "metadata": {}, "outputs": [], "source": [ @@ -5032,7 +5048,7 @@ }, { "cell_type": "markdown", - "id": "427", + "id": "428", "metadata": {}, "source": [ "But since we now have consumed **2 elements** from `sq`, when we use `enumerate` it will also have two less items from `sq`:" @@ -5041,7 +5057,7 @@ { "cell_type": "code", "execution_count": null, - "id": "428", + "id": "429", "metadata": { "lines_to_next_cell": 1 }, @@ -5052,7 +5068,7 @@ }, { "cell_type": "markdown", - "id": "429", + "id": "430", "metadata": {}, "source": [ "And this might not be what you expected: the value is the **third** element of `sq` ($2^2$), while the index is `0`, as if we were starting from the beginning.\n", @@ -5063,7 +5079,7 @@ }, { "cell_type": "markdown", - "id": "430", + "id": "431", "metadata": {}, "source": [ "### Beware: Generators can only be consumed once!\n", @@ -5074,7 +5090,7 @@ { "cell_type": "code", "execution_count": null, - "id": "431", + "id": "432", "metadata": { "lines_to_next_cell": 1 }, @@ -5088,7 +5104,7 @@ { "cell_type": "code", "execution_count": null, - "id": "432", + "id": "433", "metadata": {}, "outputs": [], "source": [ @@ -5100,7 +5116,7 @@ { "cell_type": "code", "execution_count": null, - "id": "433", + "id": "434", "metadata": {}, "outputs": [], "source": [ @@ -5113,7 +5129,7 @@ { "cell_type": "code", "execution_count": null, - "id": "434", + "id": "435", "metadata": {}, "outputs": [], "source": [ @@ -5125,7 +5141,7 @@ }, { "cell_type": "markdown", - "id": "435", + "id": "436", "metadata": {}, "source": [ "Compare this with a list, which can be iterated multiple times:" @@ -5134,7 +5150,7 @@ { "cell_type": "code", "execution_count": null, - "id": "436", + "id": "437", "metadata": {}, "outputs": [], "source": [ @@ -5146,7 +5162,7 @@ { "cell_type": "code", "execution_count": null, - "id": "437", + "id": "438", "metadata": {}, "outputs": [], "source": [ @@ -5159,7 +5175,7 @@ { "cell_type": "code", "execution_count": null, - "id": "438", + "id": "439", "metadata": {}, "outputs": [], "source": [ @@ -5171,7 +5187,7 @@ }, { "cell_type": "markdown", - "id": "439", + "id": "440", "metadata": {}, "source": [ "If you need to iterate through generator values multiple times, convert it to a list first:\n", @@ -5184,7 +5200,7 @@ }, { "cell_type": "markdown", - "id": "440", + "id": "441", "metadata": {}, "source": [ "### Quiz: Generators" @@ -5193,7 +5209,7 @@ { "cell_type": "code", "execution_count": null, - "id": "441", + "id": "442", "metadata": {}, "outputs": [], "source": [ @@ -5204,7 +5220,7 @@ }, { "cell_type": "markdown", - "id": "442", + "id": "443", "metadata": {}, "source": [ "## Exercises" @@ -5213,7 +5229,7 @@ { "cell_type": "code", "execution_count": null, - "id": "443", + "id": "444", "metadata": {}, "outputs": [], "source": [ @@ -5222,7 +5238,7 @@ }, { "cell_type": "markdown", - "id": "444", + "id": "445", "metadata": {}, "source": [ "### Exercise 1: Password checker factory" @@ -5230,7 +5246,7 @@ }, { "cell_type": "markdown", - "id": "445", + "id": "446", "metadata": { "jp-MarkdownHeadingCollapsed": true }, @@ -5254,7 +5270,7 @@ }, { "cell_type": "markdown", - "id": "446", + "id": "447", "metadata": {}, "source": [ "For example, to create a password checker that requires a password to have at least 2 uppercase letters, at least 3 lowercase letters, at least 1 punctuation mark, and at least 4 digits, we can write\n", @@ -5284,7 +5300,7 @@ { "cell_type": "code", "execution_count": null, - "id": "447", + "id": "448", "metadata": {}, "outputs": [], "source": [ @@ -5310,20 +5326,24 @@ " min_dig: Minimum number of digits\n", " Returns:\n", " - A function that checks if a password meets the criteria\n", - " \"\"\"" + " \"\"\"\n", + " def noop(*args, **kwargs): ...\n", + " return noop" ] }, { "cell_type": "markdown", - "id": "448", - "metadata": {}, + "id": "449", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "### Exercise 2: String range" ] }, { "cell_type": "markdown", - "id": "449", + "id": "450", "metadata": {}, "source": [ "Complete the function called `solution_str_range` so that it emulates the built-in `range`, but for characters.\n", @@ -5337,7 +5357,7 @@ }, { "cell_type": "markdown", - "id": "450", + "id": "451", "metadata": {}, "source": [ "
\n", @@ -5347,7 +5367,7 @@ }, { "cell_type": "markdown", - "id": "451", + "id": "452", "metadata": {}, "source": [ "
\n", @@ -5358,7 +5378,7 @@ { "cell_type": "code", "execution_count": null, - "id": "452", + "id": "453", "metadata": {}, "outputs": [], "source": [ @@ -5381,15 +5401,17 @@ }, { "cell_type": "markdown", - "id": "453", - "metadata": {}, + "id": "454", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "### Exercise 3: Read `n` lines" ] }, { "cell_type": "markdown", - "id": "454", + "id": "455", "metadata": {}, "source": [ "Create a function called `read_n_lines` that takes two arguments: the filename from which to read, and the **maximum number of lines** that should be returned with each iteration.\n", @@ -5433,7 +5455,7 @@ }, { "cell_type": "markdown", - "id": "455", + "id": "456", "metadata": {}, "source": [ "We could also do:\n", @@ -5459,7 +5481,7 @@ }, { "cell_type": "markdown", - "id": "456", + "id": "457", "metadata": {}, "source": [ "
\n", @@ -5470,7 +5492,7 @@ { "cell_type": "code", "execution_count": null, - "id": "457", + "id": "458", "metadata": {}, "outputs": [], "source": [ @@ -5489,7 +5511,7 @@ }, { "cell_type": "markdown", - "id": "458", + "id": "459", "metadata": {}, "source": [ "### Exercise 4: Only run once" @@ -5497,7 +5519,7 @@ }, { "cell_type": "markdown", - "id": "459", + "id": "460", "metadata": {}, "source": [ "Create a decorator called `solution_once` that restricts a function to run at most **once every `allowed_time` seconds**, where `allowed_time` is a parameter with a default value of `15`.\n", @@ -5508,7 +5530,7 @@ }, { "cell_type": "markdown", - "id": "460", + "id": "461", "metadata": {}, "source": [ "For example, the following code:\n", @@ -5532,7 +5554,7 @@ }, { "cell_type": "markdown", - "id": "461", + "id": "462", "metadata": {}, "source": [ "Should print something like:\n", @@ -5557,7 +5579,7 @@ }, { "cell_type": "markdown", - "id": "462", + "id": "463", "metadata": {}, "source": [ "
\n", @@ -5576,7 +5598,7 @@ { "cell_type": "code", "execution_count": null, - "id": "463", + "id": "464", "metadata": {}, "outputs": [], "source": [ @@ -5597,8 +5619,17 @@ " allowed_time: The time in seconds to wait before allowing the function to run again. Default is 15 seconds.\n", " Returns:\n", " - A decorator that runs the function at most once per given seconds\n", - " \"\"\"" + " \"\"\"\n", + " return" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "465", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -5620,7 +5651,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.10" + "version": "3.12.11" } }, "nbformat": 4, diff --git a/extra/12a_decorators_retry_validate.ipynb b/extra/12a_decorators_retry_validate.ipynb new file mode 100644 index 00000000..576c5de5 --- /dev/null +++ b/extra/12a_decorators_retry_validate.ipynb @@ -0,0 +1,591 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Decorators example\n", + "## Retry & Validate\n", + "\n", + "In this notebook we build two practical, parametrized decorators and stack them together.\n", + "The goal is to see how decorators let us compose **different concerns** (like retrying on failure and validating inputs) without touching the function body.\n", + "\n", + "**Prerequisites:** closures, `functools.wraps`, decorator factories, stacking\n", + "(see [Functions (advanced)](../12_functions_advanced.ipynb))." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from functools import wraps\n", + "import time\n", + "import random" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## 1. The `@retry` decorator" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "Network calls fail, file I/O can be flaky, external services go down.\n", + "Instead of writing a try/except loop every time we call an unreliable function, we can wrap it once with a decorator.\n", + "\n", + "Let's start with a function that simulates a flaky service:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "\n", + "random.seed(None)\n", + "\n", + "\n", + "def flaky_function():\n", + " \"\"\"Simulate an unreliable network call.\"\"\"\n", + " if random.random() < 0.6:\n", + " raise ConnectionError(\"Server not responding\")\n", + " return {\"status\": \"ok\", \"data\": 42}" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "Try calling it a few times — sometimes it works, sometimes it doesn't:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(5):\n", + " try:\n", + " result = flaky_function()\n", + " print(f\"Call {i + 1}: {result}\")\n", + " except ConnectionError as e:\n", + " print(f\"Call {i + 1}: FAILED — {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "We *could* wrap every call site in a retry loop, but that quickly becomes tedious and clutters our code.\n", + "Let's build a decorator instead." + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "### First version: simple retry (hard-coded)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [], + "source": [ + "def retry_simple(fn):\n", + " \"\"\"Retry a function up to 3 times with a 0.5s delay.\"\"\"\n", + "\n", + " @wraps(fn)\n", + " def wrapper(*args, **kwargs):\n", + " last_exception = None\n", + " for attempt in range(1, 4):\n", + " try:\n", + " return fn(*args, **kwargs)\n", + " except Exception as e:\n", + " last_exception = e\n", + " print(f\" Attempt {attempt} failed: {e}\")\n", + " time.sleep(0.5)\n", + " raise last_exception\n", + "\n", + " return wrapper\n", + "\n", + "\n", + "@retry_simple\n", + "def flaky_function():\n", + " if random.random() < 0.6:\n", + " raise ConnectionError(\"Server not responding\")\n", + " return {\"status\": \"ok\", \"data\": 42}\n", + "\n", + "\n", + "flaky_function()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "This works, but the retry count, delay, and which exceptions to catch are all hard-coded.\n", + "Let's make it configurable — we need a **decorator factory**." + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "### Parametrized `@retry`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "def retry(max_attempts=3, delay=0.5, exceptions=(Exception,)):\n", + " \"\"\"\n", + " Decorator factory: retry a function up to `max_attempts` times,\n", + " waiting `delay` seconds between attempts.\n", + " Only catches exceptions listed in `exceptions`.\n", + " \"\"\"\n", + "\n", + " def decorator(fn):\n", + " @wraps(fn)\n", + " def wrapper(*args, **kwargs):\n", + " last_exception = None\n", + " for attempt in range(1, max_attempts + 1):\n", + " try:\n", + " return fn(*args, **kwargs)\n", + " except exceptions as e:\n", + " last_exception = e\n", + " print(f\" [{fn.__name__}] Attempt {attempt}/{max_attempts} failed: {e}\")\n", + " if attempt < max_attempts:\n", + " time.sleep(delay)\n", + " raise last_exception\n", + "\n", + " return wrapper\n", + "\n", + " return decorator" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "Let's try it out:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [], + "source": [ + "@retry(max_attempts=5, delay=0.3, exceptions=(ConnectionError,))\n", + "def flaky_function():\n", + " if random.random() < 0.6:\n", + " raise ConnectionError(\"Server not responding\")\n", + " return {\"status\": \"ok\", \"data\": 42}\n", + "\n", + "\n", + "flaky_function()" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "Notice how only `ConnectionError` is caught.\n", + "If the function raises something else, the decorator lets it propagate immediately — no retries wasted:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [], + "source": [ + "@retry(max_attempts=5, delay=0.3, exceptions=(ConnectionError,))\n", + "def broken_function():\n", + " raise ValueError(\"This is a bug, not a transient failure\")\n", + "\n", + "\n", + "try:\n", + " broken_function()\n", + "except ValueError as e:\n", + " print(f\"ValueError propagated immediately: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## 2. The `@validate_types` decorator" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "Python is dynamically typed, which is flexible but can lead to confusing errors deep inside a function.\n", + "A type-checking decorator catches bad inputs early, at the \"front door.\"\n", + "\n", + "Consider a simple calculation:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": { + "lines_to_next_cell": 1 + }, + "outputs": [], + "source": [ + "def compute(x, y):\n", + " return x * y + 1\n", + "\n", + "\n", + "# Works fine with numbers:\n", + "print(compute(3, 4.5))\n", + "\n", + "# But with wrong types, the error is confusing:\n", + "try:\n", + " compute(\"hello\", 4.5)\n", + "except TypeError as e:\n", + " print(f\"Confusing error: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "Let's build a decorator that checks types before the function runs.\n", + "\n", + "To keep things simple, our decorator will require the function to be called with **keyword arguments**.\n", + "This way we can match argument names directly — no introspection needed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "def validate_types(**expected_types):\n", + " \"\"\"\n", + " Decorator factory: check that keyword arguments match the specified types.\n", + "\n", + " Usage: @validate_types(x=int, y=float)\n", + " \"\"\"\n", + "\n", + " def decorator(fn):\n", + " @wraps(fn)\n", + " def wrapper(**kwargs):\n", + " for param_name, expected_type in expected_types.items():\n", + " if param_name in kwargs:\n", + " value = kwargs[param_name]\n", + " if not isinstance(value, expected_type):\n", + " raise TypeError(\n", + " f\"Argument '{param_name}' must be {expected_type.__name__}, \"\n", + " f\"got {type(value).__name__}: {value!r}\"\n", + " )\n", + " return fn(**kwargs)\n", + "\n", + " return wrapper\n", + "\n", + " return decorator" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "Let's try it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "@validate_types(x=int, y=(int, float))\n", + "def compute(x, y):\n", + " return x * y + 1\n", + "\n", + "\n", + "# Correct types — works fine:\n", + "print(compute(x=3, y=4.5))\n", + "\n", + "# Wrong types — clear error message:\n", + "try:\n", + " compute(x=\"hello\", y=4.5)\n", + "except TypeError as e:\n", + " print(f\"Clear error: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "The function body never changed.\n", + "The validation concern is completely **separate** from the business logic.\n", + "\n", + "> **Note:** requiring keyword arguments is a simplification. A production-grade version would use\n", + "> `inspect.signature` to handle positional arguments too — but that's beyond our scope here.\n", + "> In practice, you'd use a library like [`pydantic`](https://docs.pydantic.dev/) for this." + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "### Advanced alternative: handling positional arguments" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "If you want `validate_types` to work with **both** positional and keyword arguments,\n", + "you can use `inspect.signature` to bind them to parameter names:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": { + "jupyter": { + "source_hidden": true + }, + "lines_to_next_cell": 1 + }, + "outputs": [], + "source": [ + "import inspect\n", + "\n", + "\n", + "def validate_types_advanced(**expected_types):\n", + " \"\"\"Version that handles both positional and keyword arguments.\"\"\"\n", + "\n", + " def decorator(fn):\n", + " sig = inspect.signature(fn)\n", + "\n", + " @wraps(fn)\n", + " def wrapper(*args, **kwargs):\n", + " bound = sig.bind(*args, **kwargs)\n", + " bound.apply_defaults()\n", + "\n", + " for param_name, expected_type in expected_types.items():\n", + " if param_name in bound.arguments:\n", + " value = bound.arguments[param_name]\n", + " if not isinstance(value, expected_type):\n", + " raise TypeError(\n", + " f\"Argument '{param_name}' must be {expected_type.__name__}, \"\n", + " f\"got {type(value).__name__}: {value!r}\"\n", + " )\n", + " return fn(*args, **kwargs)\n", + "\n", + " return wrapper\n", + "\n", + " return decorator\n", + "\n", + "\n", + "# Now positional calls work too:\n", + "@validate_types_advanced(x=int, y=(int, float))\n", + "def compute(x, y):\n", + " return x * y + 1\n", + "\n", + "\n", + "print(compute(3, 4.5)) # positional argumnets work!\n", + "\n", + "compute(\"hello\", y=4.5)" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "## 3. Stacking them together" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "Now the real power: combining both decorators on a single function.\n", + "\n", + "Imagine we have a flaky sensor API. We want to:\n", + "1. **Validate** that we're passing the right types\n", + "2. **Retry** if the sensor doesn't respond\n", + "\n", + "The stacking order matters! The decorator **closest to `def`** wraps first (its wrapper is the **innermost** layer):\n", + "\n", + "```python\n", + "@retry(...) # outer: retries the validated call\n", + "@validate_types(...) # inner: validates first, then calls the function\n", + "def my_function(...):\n", + " ...\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "@retry(max_attempts=4, delay=0.3, exceptions=(ConnectionError,))\n", + "@validate_types(sensor_id=int, threshold=(int, float))\n", + "def fetch_sensor_reading(sensor_id, threshold):\n", + " \"\"\"Fetch a reading from a flaky sensor API.\"\"\"\n", + " if random.random() < 0.5:\n", + " raise ConnectionError(f\"Sensor {sensor_id} not responding\")\n", + " reading = random.uniform(0, 100)\n", + " return round(reading, 2) if reading > threshold else 0.0" + ] + }, + { + "cell_type": "markdown", + "id": "31", + "metadata": {}, + "source": [ + "Call with correct types — retries happen on `ConnectionError`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "result = fetch_sensor_reading(sensor_id=42, threshold=10.0)\n", + "print(f\"Reading: {result}\")" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "Call with wrong types — `TypeError` is raised **immediately**, no retries attempted\n", + "(because `TypeError` is not in the retry `exceptions` tuple):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " fetch_sensor_reading(sensor_id=\"abc\", threshold=10.0)\n", + "except TypeError as e:\n", + " print(f\"Caught immediately: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "This is exactly the point: each decorator handles **one concern**, and stacking them composes behavior cleanly." + ] + }, + { + "cell_type": "markdown", + "id": "36", + "metadata": {}, + "source": [ + "## Recap\n", + "\n", + "- **Decorator factories** use three nested functions: factory → decorator → wrapper.\n", + "- Always use `functools.wraps` to preserve the original function's name and docstring.\n", + "- **Stacking order matters:** the decorator closest to `def` wraps first (runs first when the function is called).\n", + "- These patterns are used in well-known libraries. Use these and do not reivent the well for production code!\n", + " - Retry → [`tenacity`](https://tenacity.readthedocs.io/)\n", + " - Type validation → [`pydantic`](https://docs.pydantic.dev/)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "formats": "ipynb,py:percent" + }, + "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.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}