Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New Concept]: Decorators #2838

Merged
merged 49 commits into from
Jun 16, 2022
Merged
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
9093c00
`about.md` - how to use decorators
mathstrains21 Jan 6, 2022
bf8e6d2
Add `do_nothing` decorator
mathstrains21 Jan 6, 2022
8398ddc
Example that actually does something
mathstrains21 Jan 6, 2022
6e45226
First decorator modifying the original function
mathstrains21 Jan 7, 2022
59b9a3c
Explain the example
mathstrains21 Jan 7, 2022
a05cd9c
Function parameters and varying numbers of parameters
mathstrains21 Jan 7, 2022
935fbc8
Update about.md
bobahop May 29, 2022
3195b1d
Update about.md
bobahop May 29, 2022
f1b475e
Update about.md
bobahop May 29, 2022
fec71c7
Update about.md
bobahop May 29, 2022
3f063cc
Update about.md
bobahop May 29, 2022
2617405
Update about.md
bobahop May 29, 2022
20c603e
Update about.md
bobahop May 29, 2022
38bc6cc
Update concepts/decorators/about.md
bobahop Jun 13, 2022
f0ed0b8
Update concepts/decorators/about.md
bobahop Jun 13, 2022
2e5a3ce
Update concepts/decorators/about.md
bobahop Jun 13, 2022
bb1b05a
Update concepts/decorators/about.md
bobahop Jun 13, 2022
b7c51be
Update concepts/decorators/about.md
bobahop Jun 13, 2022
dc9d705
Update concepts/decorators/about.md
bobahop Jun 13, 2022
066b8fd
Update concepts/decorators/about.md
bobahop Jun 13, 2022
9f27365
Update concepts/decorators/about.md
bobahop Jun 13, 2022
56d7a6e
Update concepts/decorators/about.md
bobahop Jun 13, 2022
961d917
Update concepts/decorators/about.md
bobahop Jun 13, 2022
97e06a2
Update concepts/decorators/about.md
bobahop Jun 13, 2022
8a26678
Update concepts/decorators/about.md
bobahop Jun 13, 2022
155b722
Update concepts/decorators/about.md
bobahop Jun 13, 2022
23ad694
Update concepts/decorators/about.md
bobahop Jun 13, 2022
5897d62
Update concepts/decorators/about.md
bobahop Jun 13, 2022
4cf2503
Update concepts/decorators/about.md
bobahop Jun 13, 2022
bea08e9
Update concepts/decorators/about.md
bobahop Jun 13, 2022
936193d
Update concepts/decorators/about.md
bobahop Jun 13, 2022
bc3d624
Update concepts/decorators/about.md
bobahop Jun 13, 2022
99c8662
Update concepts/decorators/about.md
bobahop Jun 13, 2022
6721bd4
Update concepts/decorators/about.md
bobahop Jun 13, 2022
89e5971
Update concepts/decorators/about.md
bobahop Jun 13, 2022
f7d7bd4
Update concepts/decorators/about.md
bobahop Jun 13, 2022
844d117
Update concepts/decorators/about.md
bobahop Jun 13, 2022
7f7cc5b
Update concepts/decorators/about.md
bobahop Jun 13, 2022
324c224
Update concepts/decorators/about.md
bobahop Jun 13, 2022
0634ecc
Update concepts/decorators/about.md
bobahop Jun 13, 2022
cc93f0a
Update concepts/decorators/about.md
bobahop Jun 13, 2022
467d18c
Update about.md
bobahop Jun 13, 2022
19c4cc6
Update concepts/decorators/about.md
bobahop Jun 13, 2022
3a59ddd
White space "corrections"
Jun 15, 2022
26a5434
Update concepts/decorators/about.md
bobahop Jun 16, 2022
970ddd5
Update concepts/decorators/about.md
bobahop Jun 16, 2022
1dffa4d
Update concepts/decorators/about.md
bobahop Jun 16, 2022
a1066e0
Update concepts/decorators/about.md
bobahop Jun 16, 2022
fa7101f
Update about.md
bobahop Jun 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 246 additions & 2 deletions concepts/decorators/about.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,246 @@
#TODO: Add about for this concept.

# About

Decorators are functions that take another function as an argument for the purpose of extending or replacing the behavior of the passed-in function.
If function `A` is a decorator, and function `B` is its argument, then function `A` modifies, extends, or replaces function `B`'s **behavior** _without modifying_ function `B`'s code.
We say that the decorator function `A` _wraps_ function `B`.
While we talk about "modifying" behavior, the wrapped function is _not actually changed_.
Behavior is either added _around_ the wrapped function (_and what it returns_), or the wrapped function's behavior is _substituted_ for some other behavior.

## A Decorator is a Higher-Order Function

A [higher-order function][higher-order functions] is a function that accepts one or more functions as arguments and/or returns one or more functions.
A function, used as an argument or returned from another function, is a [first-class function][first-class functions].
A Python function, as a [callable object][callable objects], is a first-class function which can be stored in a variable or used as an argument, much like any other value or object.
Higher-order functions and first-class functions work together to make decorators possible.

## What Using a Decorator Looks Like

The `@` symbol is prepended to the name of the decorator function and placed just above the function to be decorated, like so:

```python
@decorator
def decorated_function():
pass
```

Some decorators accept arguments:

```python
@decorator_with_arg(name="Bob")
def decorated_function2():
pass
```

If a decorator has defined default arguments, you must use parenthesis in the `@decorator()` call for the decorator to work, as you would in calling any function:

```python
@decorator_with_default_arg()
def decorated_function3():
pass
```

If a decorator takes a _positional_ arguments, not supplying the arguments will result in an error which will look something like:

```
TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'
```

The `@decorator` syntax is syntactic sugar or a shorthand for calling the _decorating function_ and passing the _decorated function_ to it as an argument.
Following are examples of alternative ways for calling a decorator:

```python
def function():
pass

function = decorator(function)


def function2():
pass

function2 = decorator_with_arg(name="Bob")(function2)


def function3():
pass

function3 = decorator_with_default_arg()(function3)
```
Comment on lines +51 to +68
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am finding this really hard to follow with all the repeated use of function. Could we perhaps disambiguate this a bit by using decorating and decorated or wrapping and wrapped or inner and outer?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to change it however you'd like.


## Writing a Simple Decorator

Most decorators are intended to _extend_ or _replace_ the behavior of another function, but some decorators may do nothing but return the functions they are wrapping.

Decorators are functions which take at least one argument - the function which they are wrapping.
They usually return either the wrapped function or the result of an expression that uses the wrapped function.
Comment on lines +74 to +75
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two sentences seem to repeat whats been said above, so I think they should come out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind them.;


A simple decorator - one that simply returns its wrapped function - can be written as follows:
```python
>>> def do_nothing(func):
... return func
...
... @do_nothing
... def function4():
... return 4
...
>>> print(function4())
4
```

A decorator may only add side effects, such as additional information used for logging:

```python
>>> def my_logger(func):
... print(f"Entering {func.__name__}")
... return func
...
... @my_logger
... def my_func():
... print("Hello")
...
>>> my_func()
Entering my_func
Hello
```

A decorator does not return itself.
It may return its function arguments, another function, or one or more values that replace the return from the passed-in or decorated function.
If a decorator returns another function, it will usually return an [inner function][inner functions].

## Inner Functions

A function can be defined within a function.
Such a nested function is called an [inner function][inner functions].
A decorator may use an inner function to wrap its function argument.
The decorator then returns its inner function.
The inner function may then return the original function argument.

### A Validating Decorator Using an Inner Function

Following is an example of a decorator being used for validation:

```python
>>> def my_validator(func):
... def my_wrapper(world):
... print(f"Entering {func.__name__} with {world} argument")
... if ("Pluto" == world):
... print("Pluto is not a planet!")
... else:
... return func(world)
... return my_wrapper
...
... @my_validator
... def my_func(planet):
... print(f"Hello, {planet}!")
...
>>> my_func("World")
Entering my_func with World argument
Hello, World!
...
>>> my_func("Pluto")
Entering my_func with Pluto argument
Pluto is not a planet!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

```

On the first line, we have the definition for the decorator with its `func` argument.
On the next line is the definition for the decorators _inner function_, which wraps the `func` argument.
Since the _inner function_ wraps the decorator's `func` argument, it is passed the same argument that is passed to `func`.
Note that the wrapper doesn't have to use the same name for the argument that was defined in `func`.
The original function uses `planet` and the decorator uses `world`, and the decorator still works.

The inner function returns either `func` or, if `planet` equals `Pluto`, it will print that Pluto is not a planet.
It could be coded to raise a `ValueError` instead.
So, the inner function wraps `func`, and returns either `func` or does something that substitutes what `func` would do.
BethanyG marked this conversation as resolved.
Show resolved Hide resolved
The decorator returns its _inner function_.
The _inner_function_ may or may not return the original, passed-in function.
Depending on what code conditionally executes in the wrapper function or _inner_function_, `func` may be returned, an error could be raised, or a value of `func`'s return type could be returned.

### Decorating a Function that Takes an Arbitrary Number of Arguments

Decorators can be written for functions that take an arbitrary number of arguments by using the `*args` and `**kwargs` syntax.

Following is an example of a decorator for a function that takes an arbitrary number of arguments:

```python
>>> def double(func):
... def wrapper(*args, **kwargs):
... return func(*args, **kwargs) * 2
... return wrapper
...
... @double
... def add(*args):
... return sum(args)
...
>>> print(add(2, 3, 4))
18
>>> print(add(2, 3, 4, 5, 6))
40
```

This works for doubling the return value from the function argument.
If we want to triple, quadruple, etc. the return value, we can add a parameter to the decorator itself, as we show in the next section.

### Decorators Which Have Their own Parameters

Following is an example of a decorator that can be configured to multiply the decorated function's return value by an arbitrary amount:

```python
>>> def multi(factor=1):
... if (factor == 0):
... raise ValueError("factor must not be 0")
...
... def outer_wrapper(func):
... def inner_wrapper(*args, **kwargs):
... return func(*args, **kwargs) * factor
... return inner_wrapper
... return outer_wrapper
...
... @multi(factor=3)
... def add(*args):
... return sum(args)
...
>>> print(add(2, 3, 4))
27
>>> print(add(2, 3, 4, 5, 6))
60
```

The first lines validate that `factor` is not `0`.
Then the outer wrapper is defined.
This has the same signature we expect for an unparameterized decorator.
The outer wrapper has an inner function with the same signature as the original function.
The inner wrapper does the work of multiplying the returned value from the original function by the argument passed to the decorator.
The outer wrapper returns the inner wrapper, and the decorator returns the outer wrapper.

Following is an example of a parameterized decorator that controls whether it validates the argument passed to the original function:

```python
>>> def check_for_pluto(check=True):
... def my_validator(func):
... def my_wrapper(world):
... print(f"Entering {func.__name__} with {world} argument")
... if (check and "Pluto" == world):
... print("Pluto is not a planet!")
... else:
... return func(world)
... return my_wrapper
... return my_validator
...
... @check_for_pluto(check=False)
... def my_func(planet):
... print(f"Hello, {planet}!")
...
>>> my_func("World")
Entering my_func with World argument
Hello, World!
>>> my_func("Pluto")
Entering my_func with Pluto argument
Hello, Pluto!
```

This allows for easy toggling between checking for `Pluto` or not, and is done without having to modify `my_func`.

[callable objects]: https://www.pythonmorsels.com/callables/
[first-class functions]: https://www.geeksforgeeks.org/first-class-functions-python/
[higher-order functions]: https://www.geeksforgeeks.org/higher-order-functions-in-python/
[inner functions]: https://www.geeksforgeeks.org/python-inner-functions/