# 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
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://afsh4ck.gitbook.io/desarrollo-con-python/orientacion-a-objetos/decorators.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
