# Decorators — להוסיף התנהגות לפונקציה בלי לגעת בה<no value>

## מה נלמד?

- מה זה **Decorator** (מעטר) ולמה הוא שימושי.
- איך בונים Decorator: פונקציה שמקבלת פונקציה ומחזירה **wrapper**.
- התחביר **`@`**.
- wrapper שמתאים **לכל** פונקציה, עם `*args` ו-`**kwargs`.

## למה צריך את זה?

רוצים להדפיס הודעה בכל פעם שקוראים לפונקציה — לצורכי דיבאג:

```python
def add(a, b):
    print("Calling add")
    return a + b


def multiply(a, b):
    print("Calling multiply")
    return a * b
```

הבעיות:

- **אותה שורה מועתקת** לכל פונקציה — שוב הכפילות ש-DRY אוסר.
- **הלוגיקה מתערבבת** עם קוד עזר שאין לו קשר אליה.
- כדי **להסיר** את ההדפסות, צריך לעבור על כל הפונקציות ולמחוק.

**Decorator** מאפשר לכתוב את ההתנהגות הנוספת **פעם אחת**, ולהלביש אותה על כל פונקציה בשורה אחת.

## הסבר מהבסיס

### ממה Decorator בנוי

Decorator משלב שני דברים שלמדנו בשיעורים הקודמים:

- **Higher-Order Function** — הוא מקבל פונקציה ומחזיר פונקציה.
- **Closure** — הפונקציה שהוא מחזיר זוכרת את הפונקציה המקורית.

המבנה תמיד אותו מבנה:

1. ה-Decorator מקבל את הפונקציה המקורית (`func`).
2. בתוכו מגדירים פונקציה חדשה — **`wrapper`** (עטיפה) — שעושה משהו נוסף, **וקוראת ל-`func`**.
3. ה-Decorator מחזיר את `wrapper` — **בלי סוגריים**.

### התחביר @

```python
@log_call
def add(a, b):
    return a + b
```

זה בדיוק אותו דבר כמו לכתוב אחרי ההגדרה:

```python
add = log_call(add)
```

כלומר: השם `add` מצביע עכשיו על ה-**wrapper**, וה-wrapper קורא ל-`add` המקורית.

{{< box "analogy" "אנלוגיה: עטיפת מתנה" >}}
המתנה (הפונקציה) לא משתנה. עוטפים אותה בנייר (wrapper) — ומי שמקבל אותה פוגש קודם את העטיפה, ורק אחר כך את המתנה. אפשר לעטוף כל מתנה באותו סוג נייר, ואפשר להוריד את העטיפה בלי לפגוע במתנה.
{{< /box >}}

## תחביר

```python
def my_decorator(func):              # 1. receive the original function
    def wrapper(*args, **kwargs):    # 2. a new function that wraps it
        # ... something before ...
        result = func(*args, **kwargs)
        # ... something after ...
        return result                # return the original result
    return wrapper                   # 3. return the wrapper (no parentheses)


@my_decorator                        # same as: my_function = my_decorator(my_function)
def my_function():
    ...
```

## דוגמת קוד ראשונה

`week-2/log_call.py`:

```python
def log_call(func):
    def wrapper(a, b):
        print("Calling", func.__name__)
        return func(a, b)
    return wrapper


@log_call
def add(a, b):
    return a + b


@log_call
def multiply(a, b):
    return a * b


print(add(2, 3))
print(multiply(4, 5))
```

**פלט:**

```text
Calling add
5
Calling multiply
20
```

## פירוק הקוד שורה אחר שורה

| שורה | הסבר |
|---|---|
| `def log_call(func):` | ה-Decorator מקבל את הפונקציה המקורית. |
| `def wrapper(a, b):` | העטיפה — מקבלת את אותם פרמטרים כמו המקורית. |
| `print("Calling", func.__name__)` | ההתנהגות הנוספת. `__name__` הוא השם של הפונקציה. |
| `return func(a, b)` | קוראים למקורית, ו**מחזירים** את התוצאה שלה. |
| `return wrapper` | ה-Decorator מחזיר את העטיפה — בלי סוגריים. |
| `@log_call` | `add = log_call(add)`. מעכשיו `add(2, 3)` מפעיל את ה-wrapper. |

## דוגמה נוספת

**wrapper שמתאים לכל פונקציה.** ה-wrapper הקודם עובד רק עם פונקציות שמקבלות בדיוק `a, b`. עם `*args` ו-`**kwargs` הוא מקבל **כל** פרמטר ומעביר אותו הלאה. `week-2/log_any.py`:

```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__, "with", args, kwargs)
        return func(*args, **kwargs)
    return wrapper


@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name


@log_call
def total(*prices):
    return sum(prices)


print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

**פלט:**

```text
Calling greet with ('Dana',) {'greeting': 'Hi'}
Hi, Dana
Calling total with (10, 20, 30) {}
60
```

**דוגמה שימושית: מדידת זמן.** `week-2/timed.py`:

```python
import time


def timed(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        seconds = round(time.time() - start, 2)
        print(func.__name__, "took", seconds, "seconds")
        return result
    return wrapper


@timed
def slow_sum(n):
    time.sleep(0.5)
    return sum(range(n))


print(slow_sum(1000))
```

**פלט:**

```text
slow_sum took 0.5 seconds
499500
```

(הזמן המדויק משתנה מעט בין הרצות.) את אותו `@timed` אפשר להלביש על כל פונקציה בתוכנית — בלי לשנות שורה אחת בתוכה.

## טעות נפוצה

**שתי טעויות שקורות כמעט לכל מי שכותב Decorator ראשון:**

```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        func(*args, **kwargs)
    return wrapper()
```

1. **אין `return` בתוך wrapper.** הפונקציה המקורית רצה, אבל התוצאה שלה הולכת לאיבוד — והפונקציה המעוטרת תמיד מחזירה `None`. התיקון: `return func(*args, **kwargs)`.
2. **`return wrapper()` עם סוגריים.** זה **קורא** ל-wrapper כבר בזמן ההגדרה, ומחזיר את התוצאה שלו במקום את הפונקציה. התיקון: `return wrapper`.

**הכלל:** ה-wrapper מחזיר את **התוצאה**, וה-Decorator מחזיר את **ה-wrapper**.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## תרגול

התרגילים מבוססים רק על מה שנלמד בשיעור הזה ובשיעורים שלפניו. נסו לבד, ורק אז פתחו את הפתרון.

### תרגיל 1: Decorator ראשון

כתבו Decorator בשם `log_call` שמדפיס `Calling <name>` לפני כל קריאה לפונקציה, והחילו אותו עם `@` על `add` ועל `multiply`.

```python
print(add(2, 3))
print(multiply(4, 5))
```

**פלט צפוי:**

```text
Calling add
5
Calling multiply
20
```

{{< solution >}}
```python
def log_call(func):
    def wrapper(a, b):
        print("Calling", func.__name__)
        return func(a, b)
    return wrapper


@log_call
def add(a, b):
    return a + b


@log_call
def multiply(a, b):
    return a * b


print(add(2, 3))
print(multiply(4, 5))
```

```text
Calling add
5
Calling multiply
20
```

`@log_call` מעל `def add` שקול ל-`add = log_call(add)`. מעכשיו `add` מצביע על ה-wrapper.
{{< /solution >}}

### תרגיל 2: wrapper שמתאים לכל פונקציה

ה-wrapper הקודם עובד רק עם פונקציות שמקבלות בדיוק שני פרמטרים. שכתבו את `log_call` כך שיעבוד עם כל פונקציה.

```python
@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name

@log_call
def total(*prices):
    return sum(prices)

print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

**פלט צפוי:**

```text
Calling greet
Hi, Dana
Calling total
60
```

{{< solution >}}
```python
def log_call(func):
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper


@log_call
def greet(name, greeting="Hello"):
    return greeting + ", " + name


@log_call
def total(*prices):
    return sum(prices)


print(greet("Dana", greeting="Hi"))
print(total(10, 20, 30))
```

```text
Calling greet
Hi, Dana
Calling total
60
```

`*args` ו-`**kwargs` אוספים את כל הארגומנטים, ו-`func(*args, **kwargs)` מעביר אותם הלאה כמו שהם.
{{< /solution >}}

### תרגיל 3: בדיקת קלט

כתבו Decorator בשם `positive_only` לפונקציות שמקבלות מספר אחד: אם המספר שלילי, הוא זורק `ValueError` ולא קורא לפונקציה בכלל. ההודעה כוללת את שם הפונקציה (`func.__name__`) ואת המספר שהתקבל, כמו בפלט הצפוי.

```python
@positive_only
def square_root(n):
    return n ** 0.5

print(square_root(16))
try:
    square_root(-4)
except ValueError as error:
    print("error:", error)
```

**פלט צפוי:**

```text
4.0
error: square_root needs a positive number, got -4
```

{{< solution >}}
```python
def positive_only(func):
    def wrapper(n):
        if n < 0:
            raise ValueError(func.__name__ + " needs a positive number, got " + str(n))
        return func(n)
    return wrapper


@positive_only
def square_root(n):
    return n ** 0.5


print(square_root(16))
try:
    square_root(-4)
except ValueError as error:
    print("error:", error)
```

```text
4.0
error: square_root needs a positive number, got -4
```

הבדיקה קורית **לפני** הקריאה ל-`func` — אם הקלט לא תקין, הפונקציה המקורית לא רצה בכלל.
{{< /solution >}}

### תרגיל 4: ספירת קריאות

כתבו Decorator בשם `count_calls`, שסופר כמה פעמים קראו לפונקציה. בכל קריאה, **לפני** שהפונקציה רצה, הוא מדפיס את שם הפונקציה ואת מספר הקריאה — כמו בפלט הצפוי.

```python
@count_calls
def say_hi():
    print("Hi")

say_hi()
say_hi()
```

**פלט צפוי:**

```text
say_hi call #1
Hi
say_hi call #2
Hi
```

{{< solution >}}
```python
def count_calls(func):
    count = 0

    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1
        print(func.__name__, "call #" + str(count))
        return func(*args, **kwargs)

    return wrapper


@count_calls
def say_hi():
    print("Hi")


say_hi()
say_hi()
```

```text
say_hi call #1
Hi
say_hi call #2
Hi
```

`count` שייך ל-`count_calls`, וה-wrapper זוכר אותו (Closure). כי הוא **משנה** את `count`, צריך `nonlocal`.
{{< /solution >}}

### תרגיל 5: שתי טעויות

```python
def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        result.upper()
    return wrapper()
```

ה-Decorator אמור להחזיר את התוצאה של הפונקציה באותיות גדולות. מצאו שתי טעויות ותקנו.

```python
@shout
def greet(name):
    return "hello " + name

print(greet("dana"))
```

**פלט צפוי:**

```text
HELLO DANA
```

{{< solution >}}
1. **`return wrapper()` עם סוגריים** — קורא ל-wrapper כבר בזמן ההגדרה, במקום להחזיר אותו. צריך `return wrapper`.
2. **ה-wrapper לא מחזיר כלום** — `result.upper()` יוצר מחרוזת חדשה, אבל היא לא מוחזרת. צריך `return result.upper()`.

```python
def shout(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        return result.upper()
    return wrapper


@shout
def greet(name):
    return "hello " + name


print(greet("dana"))
```

```text
HELLO DANA
```

הכלל: ה-wrapper מחזיר את **התוצאה**, וה-Decorator מחזיר את **ה-wrapper**.
{{< /solution >}}

<!-- exercises:end -->

## סיכום

- **Decorator** = פונקציה שמקבלת פונקציה, עוטפת אותה ב-**wrapper** עם התנהגות נוספת, ומחזירה את ה-wrapper.
- הוא בנוי מ-**Higher-Order Function** ומ-**Closure** — שני הנושאים של השיעורים הקודמים.
- `@decorator` מעל `def` = `func = decorator(func)`.
- עם `*args` ו-`**kwargs`, ה-wrapper מתאים לכל פונקציה.
- ה-wrapper מחזיר את **התוצאה** (`return func(...)`), וה-Decorator מחזיר את **ה-wrapper** (`return wrapper`, בלי סוגריים).

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## דוקומנטציה רשמית

- [Python Glossary — decorator](https://docs.python.org/3/glossary.html#term-decorator) — מה זה Decorator
- [Function definitions](https://docs.python.org/3/reference/compound_stmts.html#function-definitions) — התחביר @decorator
- [functools.wraps](https://docs.python.org/3/library/functools.html#functools.wraps) — שמירת השם של הפונקציה המקורית

<!-- docs:end -->
