# Introduction and Background

By definition, a **decorator** is a function that takes another function and extends the behavior of the latter function without explicitly modifying it. Decorators provide a simple syntax for calling [higher-order functions](https://en.wikipedia.org/wiki/Higher-order_function).

## Functions

There are two kinds of functions:
1. **First-order function**:
     - Takes one or more non-function objects, like string, int, float, list, and so on, as arguments and return non-function objects

2. **Higher-order function**:
    - takes one or more functions as arguments (i.e. a procedural parameter, which is a parameter of a procedure that is itself a procedure);
    - returns a function as its result.
    - In mathematics higher-order functions are also termed operators or functionals

3. **Inner Functions**:
    - Defined inside other functions
    - they are locally scoped to their parents, and only exist inside the parent() function as local variables
    - the inner functions are not defined until the parent function is called

**First-Class Objects**:
In Python, functions are first-class objects. This means that functions can be passed around and used as arguments, just like any other object (string, int, float, list, and so on). Consider the following three functions:

```python
## first and higher function Examples###

# first-order function
def say_hello(name):
    return f"Hello {name}"

# first-order function
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

# higher-order function taking a function as parameter
def greet_bob(greeter_func):
    return greeter_func("Bob")
```

```python
# Note that greet_bob(say_hello) refers to two functions, but in different ways: greet_bob() and say_hello.
# The say_hello function is named without parentheses. This means that only a reference to the function is passed.
# The function is not executed. The greet_bob() function, on the other hand, is written with parentheses,
# so it will be called as usual.
greet_bob(say_hello)
```

    'Hello Bob'

```python
greet_bob(be_awesome)
```

    'Yo Bob, together we are the awesomest!'

```python
## inner function Examples###

def parent():
    print("Printing from the parent() function")

    # inner function
    def first_child():
        print("Printing from the first_child() function")

    # inner function
    def second_child():
        print("Printing from the second_child() function")

    second_child()
    first_child()
```

```python
# Note that the order in which the inner functions are defined does not matter.
# Like with any other functions, the printing only happens when the inner functions are executed.
parent()
```

    Printing from the parent() function
    Printing from the second_child() function
    Printing from the first_child() function

```python
## Returning Functions From Functions

def parent(num):
    def first_child():
        return "Hi, I am Emma"

    def second_child():
        return "Call me Liam"

    if num == 1:
        return first_child
    else:
        return second_child
```

Note that you are returning first_child without the parentheses. Recall that this means that you are **returning a reference to the function** first_child. In contrast first_child() with parentheses refers to the result of evaluating the function. This can be seen in the following example

```python
first = parent(1)
second = parent(2)
print(f"first: {first}")
print(f"second: {second}")
print(first())
print(second())
```

    first: <function parent.<locals>.first_child at 0x7fb9a8b5d320>
    second: <function parent.<locals>.second_child at 0x7fb9a8b5d170>
    Hi, I am Emma
    Call me Liam

## My First Decorators

Now that you’ve seen that functions are just like any other object in Python, you’re ready to move on and see the magical beast that is the Python decorator. Let’s start with an example:

```python
def my_first_decorator(func):
    def wrapper():
        """
        Define a inner wrapper function wraping an argument functor
        with pre-defined behaviors. In this example, we use two print statement
        to wrap "func()"
        """
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

def say_whee():
    print("Whee!")
# Call [my_first_decorator] function taking [say_whee] as a argument.
say_whee = my_first_decorator(say_whee)

# In effect, the name say_whee now points to the wrapper() inner function.
# Remember that you return wrapper as a function when you call my_decorator(say_whee)
say_whee
```

    <function __main__.my_first_decorator.<locals>.wrapper()>

```python
say_whee()
```

    Something is happening before the function is called.
    Whee!
    Something is happening after the function is called.

Put simply: **decorators wrap a function, modifying its behavior.**

Because wrapper() is a regular Python function, **the way a decorator modifies a function can change dynamically.** So as not to disturb your neighbors, the following example will only run the decorated code during the day:

```python
from datetime import datetime

def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep
    return wrapper

def say_whee():
    print("Whee!")

say_whee = not_during_the_night(say_whee)
print(say_whee)
```

    <function not_during_the_night.<locals>.wrapper at 0x7fb9a8b5d7a0>

```python
say_whee()
```

    Whee!

## Revisting code using decorator syntactic sugar

```python
def my_first_decorator(func):
    def wrapper():
        """
        Define a inner wrapper function wraping an argument functor
        with pre-defined behaviors. In this example, we use two print statement
        to wrap "func()"
        """
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

"""
So, @my_first_decorator is just an easier way of saying:
  say_whee_decor = my_first_decorator(say_whee_decor)

It’s how you apply a decorator to a function.
"""
@my_first_decorator
def say_whee_decor():
    print("Whee!")

def say_whee():
    print("Whee!")

print(say_whee_decor)
print(say_whee)
```

    <function my_first_decorator.<locals>.wrapper at 0x7fb9a8b69830>
    <function say_whee at 0x7fb9a8b69290>

# Decorator Syntax Alternatives

## Reusing Decorators defined in an own module
Defining decorators in a module file and import them for easy reusability are available.

Let’s move the decorator to its own module that can be used in many other functions.

Create a file called decorators.py with the following content:

```python
def do_twice(func):
    """
    You can name your inner function whatever you want, and a generic name like wrapper()
    is usually okay.
    """
    def wrapper_do_twice():
        func()
        func()
    return wrapper_do_twice
```

```python
from decorators import do_twice

@do_twice
def say_whee():
    print("Whee!")
```

## Decorating Functions With Arguments

Say that you have a function that accepts some arguments. Can you still decorate it?

```python
def do_twice(func):
    """
    Taking [func] without any arguments
    """
    def wrapper():
        func()
        func()
    return wrapper

@do_twice
def greet(name):
    """
    Define a function decorated by [do_twice]
    """
    print(f"Hello {name}")
```

```python
"""
The problem is that the inner function wrapper() does not take any arguments,
but name="World" was passed to it. You could fix this by letting wrapper_do_twice()
accept one argument, but then it would not work for the say_whee() function you
created earlier.
"""
greet("World")
```

The solution is to use **\*args** and **\*\*kwargs** in the inner wrapper function. Then it will accept an arbitrary number of positional and keyword arguments.
- **\*args**:
- **\*\*kwargs**:

```python
def do_twice(func):
    def wrapper(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)
    return wrapper

@do_twice
def greet(name):
    print(f"Hello {name}")
```

```python
print(greet())
print(greet("Bing"))
```

## Returning Values From Decorated Functions
What happens to the return value of decorated functions? Well, that’s up to the decorator to decide.

```python
def do_twice(func):
    def wrapper(*args, **kwargs):
        """
        Using a return statement to return an object from the calling of "func"
        We need to make sure the wrapper function returns the return value of
        the decorated function.
        """
        func(*args, **kwargs)
        return func(*args, **kwargs)
    return wrapper

@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"

hi_adam = return_greeting("Adam")
print(hi_adam)
```

    Creating greeting
    Creating greeting
    Hi Adam

## Multiple decorators

```
@dec2
@dec1
def func(arg1, arg2, ...):
    pass
```
This is equivalent to:
```
def func(arg1, arg2, ...):
    pass
func = dec2(dec1(func))
```

# A Few Real World Examples

Let’s look at a few more useful examples of decorators. You’ll notice that they’ll mainly follow the same pattern that you’ve learned so far.

```python
# This formula is a good boilerplate template for building more complex decorators.
import functools

def decorator(func):
    @functools.wraps(func)
    def wrapper_decorator(*args, **kwargs):
        # Do something before
        value = func(*args, **kwargs)
        # Do something after
        return value
    return wrapper_decorator
```

## Define functions to be executed at entry or exist

```python
def on_exit(f):
    import atexit
    atexit.register(f)
    return f

@on_exit
def func():
    pass
```

## Timing Functions

Let’s start by creating a @timer decorator. It will [measure the time a function takes to execute](https://realpython.com/python-timer/) and print the duration to the console. Here’s the code:

```python
import functools
import time

def timer(func):
    """Print the runtime of the decorated function"""
    @functools.wraps(func)
    def wrapper_timer(*args, **kwargs):
        start_time = time.perf_counter()    # 1
        value = func(*args, **kwargs)
        end_time = time.perf_counter()      # 2
        run_time = end_time - start_time    # 3
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return value
    return wrapper_timer

@timer
def waste_some_time(num_times):
    for _ in range(num_times):
        sum([i**2 for i in range(10000)])

waste_some_time(100)
```

    Finished 'waste_some_time' in 0.1792 secs

**Note:** The @timer decorator is great if you just want to get an idea about the runtime of your functions. If you want to do more precise measurements of code, you should instead consider the [timeit module](https://docs.python.org/3/library/timeit.html) in the standard library. It temporarily disables garbage collection and runs multiple trials to strip out noise from quick function calls.

## Debugging Code
The following @debug decorator will print the arguments a function is called with as well as its return value every time the function is called:

```python
import functools

def debug(func):
    """Print the function signature and return value"""
    @functools.wraps(func)
    def wrapper_debug(*args, **kwargs):
        args_repr = [repr(a) for a in args]                      # 1
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]  # 2
        signature = ", ".join(args_repr + kwargs_repr)           # 3
        print(f"Calling {func.__name__}({signature})")
        value = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {value!r}")           # 4
        return value
    return wrapper_debug
```

The signature is created by joining the string representations of all the arguments. The numbers in the following list correspond to the numbered comments in the code:
1. Create a list of the positional arguments. Use repr() to get a nice string representing each argument.
2. Create a list of the keyword arguments. The f-string formats each argument as key=value where the !r specifier means that repr() is used to represent the value.
3. The lists of positional and keyword arguments is joined together to one signature string with each argument separated by a comma.
4. The return value is printed after the function is executed.

Let’s see how the decorator works in practice by applying it to a simple function with one position and one keyword argument:

```python
@debug
def make_greeting(name, age=None):
    if age is None:
        return f"Howdy {name}!"
    else:
        return f"Whoa {name}! {age} already, you are growing up!"
make_greeting("Benjamin")
```

    Calling make_greeting('Benjamin')
    'make_greeting' returned 'Howdy Benjamin!'

    'Howdy Benjamin!'

```python
make_greeting("Richard", age=112)
```

    Calling make_greeting('Richard', age=112)
    'make_greeting' returned 'Whoa Richard! 112 already, you are growing up!'

    'Whoa Richard! 112 already, you are growing up!'

```python
make_greeting(name="Dorrisile", age=116)
```

    Calling make_greeting(name='Dorrisile', age=116)
    'make_greeting' returned 'Whoa Dorrisile! 116 already, you are growing up!'

    'Whoa Dorrisile! 116 already, you are growing up!'

This example also shows how you can apply a decorator to a function that has already been defined. The approximation of e is based on the following series expansion:
\begin{equation}
e = \sum_{n=0}^{}\frac{1}{n!} = \frac{1}{0!} + \frac{1}{1!} + \frac{1}{2!} + \ldots
\end{equation}

```python
import math
# from decorators import debug

# Apply a decorator to a standard library function
math.factorial = debug(math.factorial)

def approximate_e(terms=18):
    return sum(1 / math.factorial(n) for n in range(terms))
```

```python
# When calling the approximate_e() function, you can see the @debug decorator at work:
approximate_e(5)
```

    Calling factorial(0)
    Calling factorial(0)
    Calling factorial(0)
    'factorial' returned 1
    'factorial' returned 1
    'factorial' returned 1
    Calling factorial(1)
    Calling factorial(1)
    Calling factorial(1)
    'factorial' returned 1
    'factorial' returned 1
    'factorial' returned 1
    Calling factorial(2)
    Calling factorial(2)
    Calling factorial(2)
    'factorial' returned 2
    'factorial' returned 2
    'factorial' returned 2
    Calling factorial(3)
    Calling factorial(3)
    Calling factorial(3)
    'factorial' returned 6
    'factorial' returned 6
    'factorial' returned 6
    Calling factorial(4)
    Calling factorial(4)
    Calling factorial(4)
    'factorial' returned 24
    'factorial' returned 24
    'factorial' returned 24

    2.708333333333333

## Slowing Down Code
This next example might not seem very useful. Why would you want to slow down your Python code? Probably the most common use case is that you want to rate-limit a function that continuously checks whether a resource—like a web page—has changed. The @slow_down decorator will sleep one second before it calls the decorated function:

```python
import functools
import time

def slow_down(func):
    """Sleep 1 second before calling the function"""
    @functools.wraps(func)
    def wrapper_slow_down(*args, **kwargs):
        time.sleep(1)
        return func(*args, **kwargs)
    return wrapper_slow_down

@slow_down
def countdown(from_number):
    if from_number < 1:
        print("Liftoff!")
    else:
        print(from_number)
        countdown(from_number - 1)
countdown(3)
```

    3
    2
    1
    Liftoff!

## Registering Plugins
Decorators don’t have to wrap the function they’re decorating. They can also simply register that a function exists and return it unwrapped. This can be used, for instance, to create a light-weight plug-in architecture:

```python
import random
PLUGINS = dict()

def register(func):
    """Register a function as a plug-in"""
    PLUGINS[func.__name__] = func
    return func

@register
def say_hello(name):
    return f"Hello {name}"

@register
def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def randomly_greet(name):
    greeter, greeter_func = random.choice(list(PLUGINS.items()))
    print(f"Using {greeter!r}")
    return greeter_func(name)
PLUGINS
```

    {'say_hello': <function __main__.say_hello(name)>,
     'be_awesome': <function __main__.be_awesome(name)>}

```python
randomly_greet("Alice")
```

    Using 'be_awesome'

    'Yo Alice, together we are the awesomest!'

Using the @register decorator, you can create your own curated list of interesting variables, effectively hand-picking some functions from globals().

```python

```

```python

```

```python

```

```python

```

## Define a class with a singleton instance.

Note that once the class disappears enterprising programmers would have to be more creative to create more instances.

```python
def singleton(cls):
    instances = {}
    def getinstance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return getinstance

@singleton
class MyClass:
    ...
```

## Add attributes to a function

```python
def attrs(**kwds):
    def decorate(f):
        for k in kwds:
            setattr(f, k, kwds[k])
        return f
    return decorate

@attrs(versionadded="2.2",
       author="Guido van Rossum")
def mymethod(f):
    ...
```

## Enforce function argument and return types

```python
def accepts(*types):
    def check_accepts(f):
        assert len(types) == f.func_code.co_argcount
        def new_f(*args, **kwds):
            for (a, t) in zip(args, types):
                assert isinstance(a, t), \
                       "arg %r does not match %s" % (a,t)
            return f(*args, **kwds)
        new_f.func_name = f.func_name
        return new_f
    return check_accepts

def returns(rtype):
    def check_returns(f):
        def new_f(*args, **kwds):
            result = f(*args, **kwds)
            assert isinstance(result, rtype), \
                   "return value %r does not match %s" % (result,rtype)
            return result
        new_f.func_name = f.func_name
        return new_f
    return check_returns

@accepts(int, (int,float))
@returns((int,float))
def func(arg1, arg2):
    return arg1 * arg2
```

## Declare that a class implements a particular (set of) interface(s).

```python
def provides(*interfaces):
     """
     An actual, working, implementation of provides for
     the current implementation of PyProtocols.  Not
     particularly important for the PEP text.
     """
     def provides(typ):
         declareImplementation(typ, instancesProvide=interfaces)
         return typ
     return provides

class IBar(Interface):
     """Declare something about IBar here"""

@provides(IBar)
class Foo(object):
        """Implement something here..."""
```

# Fancy Decorators

So far, you’ve seen how to create simple decorators. You already have a pretty good understanding of what decorators are and how they work. Feel free to take a break from this article to practice everything you’ve learned.

## Decorating Classes

There are two different ways you can use decorators on classes:
1. decorate the methods of a class using [@classmethod, @staticmethod](https://realpython.com/instance-class-and-static-methods-demystified/) and [@property](https://docs.python.org/3/library/functions.html#property)
2. decorate the whole class

```python

```

## Nesting Decorators
You can apply several decorators to a function by stacking them on top of each other

```python
from decorators import debug, do_twice

@debug
@do_twice
def greet(name):
    print(f"Hello {name}")
```

Think about this as the decorators being executed in the order they are listed. In other words, @debug calls @do_twice, which calls greet(), or debug(do_twice(greet())):

## Decorators with arguments

```python

```

```python

```

## Decorators that can optionally take arguments

```python

```

## Stateful decorators

```python

```

```python

```

## Classes as decorators

```python

```

# Reference

1. [Primer on Python Decorators](https://realpython.com/primer-on-python-decorators/)
2. [PEP 318 – Decorators for Functions and Methods](https://peps.python.org/pep-0318/)
3. [The Decorator Pattern in Depth](https://blogs.oracle.com/javamagazine/post/the-decorator-pattern-in-depth)
4. [A Gentle Introduction to Decorators in Python](https://machinelearningmastery.com/a-gentle-introduction-to-decorators-in-python/)

```python

```


Examen Primer parcial
--
Alejandro Rojas Barba

Robótica industrial

25/02/2019

Ejercicio 1
--

```python
def quitar_espacios(entrada_funcion):
    n=0
    bandera=False
    frase_aux=[]

    frase=list(entrada_funcion)
    if frase[-1]!='$':
        frase.append('$')

    for i in frase:
        if i!=' ':
            frase_aux.append(i)
            bandera=True
        else:
            if bandera==True and frase[n+1]!='$' and frase[n+1]!=' ':
                frase_aux.append(i)
        n=n+1

    bandera=False

    for i in frase_aux:
        if bandera==False:
            frase_output=i
            bandera=True
        else:
            frase_output=frase_output+i

    return frase_output

```

```python
def separar_signos(entrada_funcion2):
    n=0
    bandera=False
    frase=list(entrada_funcion2)

    for i in frase:
        if i=='+' or i=='-':
            frase[n]=" "+i+" "
        n=n+1

    for i in frase:
        if bandera==False:
            frase_output=i
            bandera=True
        else:
            frase_output=frase_output+i

    return frase_output
```

```python
def separar_punto(entrada_funcion3):
    Numeros="1234567890"
    n=0
    bandera=False
    bandera1=False
    bandera2=False
    frase=list(entrada_funcion3)

    for i in frase:
        if i=='.':
            for j in Numeros:
                if j==frase[n-1]:
                    bandera1=True

                if j==frase[n+1]:
                    bandera2=True

            if bandera1==True and bandera2==False:
                frase[n]=frase[n]+" "
            elif bandera1==False and bandera2==True:
                frase[n]=" "+frase[n]
            elif bandera1==False and bandera2==False:
                frase[n]=" "+frase[n]+" "

            bandera1=False
            bandera2=False
        n=n+1

    for i in frase:
        if bandera==False:
            frase_output=i
            bandera=True
        else:
            frase_output=frase_output+i

    return frase_output
```

```python
def separar_LetraNumero(entrada_funcion4):
    Numeros="1234567890"
    Abecedario="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    frase=list(entrada_funcion4)
    n=0
    bandera=False

    for i in frase:
        for j in Numeros:
            if j==i:
                for k in Abecedario:
                    if k==frase[n+1]:
                        frase[n]=frase[n]+" "
        n=n+1

    for i in frase:
        if bandera==False:
            frase_output=i
            bandera=True
        else:
            frase_output=frase_output+i

    return frase_output
```

```python
def separar_CaracterDesconocido(entrada_funcion5):
    Numeros="1234567890"
    Abecedario="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    frase=list(entrada_funcion5)
    n=0
    bandera=False
    bandera2=False

    for i in frase:
        for j in Numeros:
            if j==i:
                bandera=True
        for k in Abecedario:
            if k==i:
                bandera=True
        if i==' ' or i=='+' or i=='-' or i=='.' or i=='$':
            bandera=True

        if bandera==False:
            frase[n]=' '+frase[n]+' '

        bandera=False
        n=n+1

    for i in frase:
        if bandera2==False:
            frase_output=i
            bandera2=True
        else:
            frase_output=frase_output+i

    return frase_output
```

```python
def construir_lista(entrada_funcion6):
    frase=list(entrada_funcion6)
    n=0
    lista=[]
    bandera=False

    for i in frase:
        if i!=' ' and i!='$':
            if bandera==False:
                auxiliar=i
                bandera=True
            else:
                auxiliar=auxiliar+i
        else:
            lista.append(auxiliar)
            bandera=False
    return lista
```

```python
def imprimir_salida(lista):
    Abecedario="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    Numeros="1234567890"
    indicador=[]
    bandera=False
    n=0
    salida=[]
    banderas=[False, False, False, False, False, False]

    for i in lista:
        for j in i:
            for k in Abecedario:
                if j==k:
                    #Letra
                    banderas[0]=True
                    bandera=True
            if bandera==False:
                for k in Numeros:
                    if j==k:
                        #Numero
                        banderas[1]=True
                        bandera=True
            if bandera==False:
                if j=='.':
                    #Punto
                    banderas[2]=True
                    if i[0]=='.' or i[len(i)-1]=='.':
                        banderas[5]=True
                elif j=='+' or j=='-':
                    #Signo
                    banderas[3]=True
                else:
                    #Otro símbolo
                    banderas[4]=True
            bandera=False

        if banderas[0]==True and banderas[2]==False and banderas[3]==False and banderas[4]==False:
            salida.append('Id')

        elif banderas[0]==False and banderas[1]==True and banderas[2]==False and banderas[3]==False and banderas[4]==False:
            salida.append('Entero')

        elif banderas[0]==False and banderas[1]==True and banderas[2]==True and banderas[3]==False and banderas[4]==False:
            if banderas[5]==True:
                salida.append('Real inválido')
                banderas[5]=False
            else:
                salida.append('Real')

        elif banderas[3]==True:
            salida.append('Op '+i)

        else:
            salida.append('Error')

        salida.append(i)
        print(salida)

        salida=[]
        banderas=[False, False, False, False, False,False]
    salida.append('Fin de archivo')
    salida.append('$')
    print(salida)
```

```python
entrada=input("Escribe la frase: ")

entrada=quitar_espacios(entrada)
entrada=separar_signos(entrada)
entrada=quitar_espacios(entrada)
entrada=separar_punto(entrada)
entrada=quitar_espacios(entrada)
entrada=separar_LetraNumero(entrada)
entrada=quitar_espacios(entrada)
entrada=separar_CaracterDesconocido(entrada)
entrada=quitar_espacios(entrada)
entrada=construir_lista(entrada)
imprimir_salida(entrada)
```

    Escribe la frase: HOLA12ADIOA
    ['Id', 'HOLA12ADIOA']
    ['Fin de archivo', '$']

Ejercicio 2
--

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
```

```python
x,y=var("x y")
```

```python
x=np.arange(-30,30,0.1)
f=np.sin(np.exp(x))-np.cos(x)
plt.plot(x,f)
#print(t)
#plt.plot(f)
```

```python
%matplotlib notebook
import matplotlib.pyplot as plt
from sympy import *
init_printing()
```

```python
x,y=var("x y")
f=sin(exp(x))-cos(x)
plot (f)
```

    <IPython.core.display.Javascript object>

    <sympy.plotting.plot.Plot at 0x2211ceacda0>

Ejercicio 3
--

```python
%matplotlib notebook
import matplotlib.pyplot as plt
from sympy import *
init_printing()
```

```python
Eq_1=x+y-25
Eq_2=2*x-14
y_=solve(Eq_2,x)
y_
Num1=solve(Eq_1.subs(x,y_[0]),y)
Num2=solve(Eq_1.subs(y,Num1[0]),x)
print("Y:",Num1)
print("X:",Num2)
```

    Y: [18]
    X: [7]

Ejercicio 4: Utilizando Numpy resuelve
--

```python
import numpy as np
```

```python
A=np.array([ [3,0,0],
             [1,8,0],
             [0,4,-2] ])

B=np.array([ [30],
             [18],
             [2] ])

C=np.linalg.inv(A)@B
print(C)

solucion=C[0]+C[1]+3*C[2]
print(solucion)
```

    [[10.]
     [ 1.]
     [ 1.]]
    [14.]

Ejercicio 5
--

```python
Abecedario="abcdefghijklmnopqrstuvwxyz"
frase="g fmnc wms bgblr rpylqjyrc gr zw fylb. rfyrq ufyr amknsrcpq ypc dmp. bmgle gr gl zw fylb gq glcddgagclr ylb rfyr'q ufw rfgq rcvr gq qm jmle. sqgle qrpgle.kyicrpylq() gq pcamkkclbcb"
n=0
m=0
frase=list(frase)

for i in frase:
    for j in Abecedario:
        if j==i:
            if j=='y':
                frase[m]='a'
            elif j=='z':
                frase[m]='b'
            else:
                frase[m]=Abecedario[n+2]
        n=n+1
    m=m+1
    n=0

for i in frase:
    print(i,end='')
```

    i hope you didnt translate it by hand. thats what computers are for. doing it in by hand is inefficient and that's why this text is so long. using string.maketrans() is recommended

Ejercicio 6
--

```python
!pip install pulp
```

    Requirement already satisfied: pulp in c:\users\usuario\anaconda3\lib\site-packages (1.6.9)
    Requirement already satisfied: pyparsing>=2.0.1 in c:\users\usuario\anaconda3\lib\site-packages (from pulp) (2.3.0)

```python
from pulp import *
```

```python
pulp.LpVariable?
```

```python
mi_lp_problema=pulp.LpProblem("Mi LP Problema", pulp.LpMinimize)

x11 = pulp.LpVariable('x11', lowBound=0, cat='Continuous')
x12 = pulp.LpVariable('x12', lowBound=0, cat='Continuous')
x13 = pulp.LpVariable('x13', lowBound=0, cat='Continuous')
x21 = pulp.LpVariable('x21', lowBound=0, cat='Continuous')
x22 = pulp.LpVariable('x22', lowBound=0, cat='Continuous')
x23 = pulp.LpVariable('x23', lowBound=0, cat='Continuous')

#Objective function
mi_lp_problema+=8*x11 +6*x12 +10*x13 +10*x21 +4*x22 +9*x23

#Consraints
mi_lp_problema += x11 + x12 + x13 ==2000
mi_lp_problema += x21 + x22 + x23 ==2500
mi_lp_problema += x11 + x21 ==1500
mi_lp_problema += x12 + x22 ==2000
mi_lp_problema += x13 + x23 ==1000
mi_lp_problema += x11>=0
mi_lp_problema += x12>=0
mi_lp_problema += x13>=0
mi_lp_problema += x21>=0
mi_lp_problema += x22>=0
mi_lp_problema += x23>=0

```

```python
mi_lp_problema
```

    Mi LP Problema:
    MINIMIZE
    8*x11 + 6*x12 + 10*x13 + 10*x21 + 4*x22 + 9*x23 + 0
    SUBJECT TO
    _C1: x11 + x12 + x13 = 2000

    _C2: x21 + x22 + x23 = 2500

    _C3: x11 + x21 = 1500

    _C4: x12 + x22 = 2000

    _C5: x13 + x23 = 1000

    _C6: x11 >= 0

    _C7: x12 >= 0

    _C8: x13 >= 0

    _C9: x21 >= 0

    _C10: x22 >= 0

    _C11: x23 >= 0

    VARIABLES
    x11 Continuous
    x12 Continuous
    x13 Continuous
    x21 Continuous
    x22 Continuous
    x23 Continuous

```python
mi_lp_problema.solve()
pulp.LpStatus[mi_lp_problema.status]
```

    'Optimal'

```python
for variable in mi_lp_problema.variables():
    print ("{}={}".format(variable.name, variable.varValue))
```

    x11=1500.0
    x12=0.0
    x13=500.0
    x21=0.0
    x22=2000.0
    x23=500.0

```python
x11=1500.0
x12=0.0
x13=500.0
x21=0.0
x22=2000.0
x23=500.0

solucion=8*x11 +6*x12 +10*x13 +10*x21 +4*x22 +9*x23
print(solucion)
```

    29500.0