> For the complete documentation index, see [llms.txt](https://afsh4ck.gitbook.io/desarrollo-con-python/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://afsh4ck.gitbook.io/desarrollo-con-python/orientacion-a-objetos/decorators.md).

# Decorators

## <mark style="color:blue;">1. Conceptos avanzados sobre funciones</mark>

En Python las funciones también se consideran objetos y como consecuencia de esto se pueden asignar a una variable, almacenarlas en estructuras de datos (listas, tuplas, diccionarios...) o incluso pasarlas como argumento de otras funciones.

```python
def func():
    print("Hola mundo")
    
func()
Hola mundo

func
<function __main__.func()>

var = func

var()
Hola mundo

def func2(funcion):
    funcion()
    
func2(func)
Hola mundo
```

## <mark style="color:blue;">2. ¿Qué es un</mark> <mark style="color:blue;"></mark>*<mark style="color:blue;">decorator</mark>*<mark style="color:blue;">?</mark>

Los *decorator* envuelven una función, modificando su comportamiento realizando una combinación de todas las propiedades que hemos visto anteriormente.

```python
def mi_funcion():
    print("Hola mundo")
    
# Añadimos nuevas funcionalidades a una función ya existente
def mi_decorator(func):
    def wrapper():
        print("Ejecución antes de la llamada a la funcion")
        func()
        print("Ejecución después de la llamada a la funcion")
    return wrapper
    
mi_funcion_mod = mi_decorator(mi_funcion)

mi_funcion_mod()
Ejecución antes de la llamada a la funcion
Hola mundo
Ejecución después de la llamada a la funcion
```

Podemos utilizar *decorators* para modificar el comportamiento de una función que programemos, por ejemplo, añadiendo condiciones que se evalúen antes de ejecutar la función ya existente que no queremos modificar.

```python
def funcion():
    print("Hola mundo")
    
def mi_decorator(func):
    def wrapper():
        if var < 5:
            func()
        else:
            print("No se puede ejecutar la funcion")
    return wrapper
    
funcion = mi_decorator(funcion)

var = 2

funcion()
Hola mundo

var = 10

funcion()
No se puede ejecutar la funcion
```

## <mark style="color:blue;">3.</mark> <mark style="color:blue;"></mark>*<mark style="color:blue;">Syntactic Sugar</mark>*

La sintaxis que hemos utilizado en el apartado anterior para definir el *decorator* es bastante compleja, por ello, Python nos proporciona una alternativa mucho más sencilla.

```python
def mi_decorator(func):
    def wrapper():
        print("Ejecución antes de la llamada a la funcion")
        func()
        print("Ejecución después de la llamada a la funcion")
    return wrapper

# Añadimos @ y el decorador a aplicar a una función  
@mi_decorator
def funcion():
    print("Hola mundo")
    
funcion()
Ejecución antes de la llamada a la funcion
Hola mundo
Ejecución después de la llamada a la funcion
```

## <mark style="color:blue;">4.</mark> <mark style="color:blue;"></mark>*<mark style="color:blue;">Decorators</mark>* <mark style="color:blue;"></mark><mark style="color:blue;">en las Clases</mark>

Una de las cosas interesantes sobre los *decorators* es que Python nos proporciona varios definidos por defecto que podemos utilizar dentro de una clase.

Uno de los *decorators* más interesantes que podemos utilizar es `@property`, que nos permite definir métodos en una clase para consultar y modificar un atributo interno.

{% code overflow="wrap" %}

```python
# Clase modificada

class Coche():
    """Esta clase representa un coche."""
    def __init__(self, modelo, potencia, consumo):
        """Inicializa los atributos de instancia.
        Argumentos posicionales:
        modelo -- string que representa el modelo del coche
        potencia -- int que representa la potencia en cv
        conumo -- int que representa el consumo en l/100km
        """
        self._modelo = modelo
        self._potencia = potencia
        self._consumo = consumo
        self._km_actuales = 0
    def especificaciones(self):
        """Muestra las especicificaciones del coche."""
        print("Modelo:", self._modelo,
             "\nPotencia: {} cv".format(self._potencia),
             "\nConsumo: {} l/100km".format(self._consumo),
             "\nKilometros actuales:", self._km_actuales)
             
    @property
    def kilometros(self):
        return self._km_actuales
        
    @kilometros.setter
    def kilometros(self, kilometros):
        """Actualiza los kilometros del coche."""
        if kilometros > self._km_actuales:
            self._km_actuales = kilometros
        else:
            print("ERROR: No se puede establecer un numero de kilometros inferior al actual")
            
    def consumo_total(self):
        """Muestra el consumo total del coche desde el kilometro 0."""
        consumo_total = (self._km_actuales / 100) * self._consumo
        print("El consumo total es de {} litros".format(consumo_total))
```

{% endcode %}

```python
bmw = Coche("bmw i3", 150, 6)

# Esto es una mala práctica
bmw._km_actuales
0

# Buena práctica
bmw.kilometros
0

bmw.kilometros = 500

bmw.especificaciones()
Modelo: bmw i3 
Potencia: 150 cv 
Consumo: 6 l/100km 
Kilometros actuales: 500

bmw.kilometros
500

bmw.kilometros = 200
ERROR: No se puede establecer un numero de kilometros inferior al actual
```
