Improve your Python Skills with Classes, decorators, and implicit parameters
Published: 2023-12-01 03:45:11

Python is a very common programming language. It is easy to learn the basics and start using it right away, this is specially useful for data scientists, and people from other backgrounds that don't have a lot of coding experience.

This guide aims to introduce 3 concepts that if applied correctly, you could simplify your code, add functionalities to your current methods, and pass arguments without specifying them. Also, your code will look cooler 😎.

The concepts are:
1. Classes.
2. Decorators.
3. *Args and **kwargs.

## Classes
Python is an Object Oriented Programming Language (OOP Language), this means that we can use classes to organize our code and group related methods together.

A Python Class could be something like:
```python
class Pokemon:
   """Class containing the available Pokemon methods"""
   def evolve(...):
   ...
```

The interesting part about classes is that they introduce one of the most useful things of OOP, heritage.

For example, if we have multiple classes that are somewhat related, let's say, Bulbasaur, Squirtle, and Charmander, they all should have an evolve method:

```python
class Bulbasaur:
    def evolve(...):
    ...

class Squirtle:
    def evolve(...):
    ...

class Charmander:
    def evolve(...):
    ...
```

But using the example above, we could simply define a Pokemon parent class that contains the method, and all three pokemons will have it through heritage.

```python
class Pokemon:
   """Class containing the available Pokemon methods"""
   def evolve(...):
   ...

class Bulbasaur(Pokemon):
    ...

class Squirtle(Pokemon):
    ...

class Charmander(Pokemon):
    ...

eric = Bulbasaur()
eric.evolve()
# >>Evolving to Ivisaur
```
And with that all three pokemons could make use of the method defined in the parent class.

This helps us make our code scalable, maintainable and also easier to read.

## Decorators
Decorators are a way of altering a method's functionality by using another method that receives the target method as an argument.

For example, let's say that we are going to make the evolve method of the pokemons to print the elapsed time that took the pokemon to evolve.

There are several ways of doing this, but we are using a decorator for it.

```python
def measure_time(func):
    start = time.time()
    func()
    end = time.time()
    print(f"The elapsed time was {end - start} seconds!")
```
To use it we should use the decorator with an "@" above the function definition, something like this:

```python
@measure_time
def evolve(...)
    ...

print(evolve)
# >>The elapsed time was 7.152557373046875e-07 seconds!
```

We could also have decorators with parameters. One useful resource for understanding this is [this stackoverflow's answer](https://stackoverflow.com/a/26151604).

Let's say that we have two Bulbasaurs and one of them tackles the other.

Example:
```python
def print_used_attack(f):
    def attack(*xs, **kws):
        return f(*xs, **kws)
    print(f"Used {f.__name__}")
    return attack
    
class Bulbasaur():
    def __init__(self):
        self.health = 100

    @staticmethod
    @print_used_attack
    def tackle(target_pokemon):
        return target_pokemon.health - 15
    
eric = Bulbasaur()
tommy = Bulbasaur()

tommy.health = eric.tackle(tommy)
# >>Used Tackle
print(tommy.health)
# >>85
```
Here, anytime the tackle method is used, a string like "Used `attack_name`" will be printed in the console, this is useful if we want to add logs to a method that receives multiple arguments. 

And, the magic of this decorator is that if we define more attacks like `vine_whip` or `leech_seed`, and we use the decorator with them, a log will be printed in the console with the used attack!

## *Args and **Kwargs
These are abreviations for Arguments and Key-arguments. 

_Note that the important part is the use of `"*"` and `"**"`, the name could be different form args and kwargs, those names are just conventions._

### *Args
The `*args` abreviation allow us to define a method without argument constraints.

Example:
```python
damage_history = [3,3,5,6,6,7]

def sum_attack_damage(*args):
    total = 0
    for num in args:
        total += num
    return total

print(sum_attack_damage(*damage_history))
# >>30
```
The use of the `*args` as a form of receiving the parameters give us the possibility of passing multiple arguments, and being able to sum them without limiting the number!

### **Kwargs
The `**kwargs` abreviation allows us to define a method that could receive a dict with the parameters' names as keys.

Example:
```python
def evolve_pokemon(pokemon, base_evolution, first_evolution, second_evolution):
    if pokemon == base_evolution:
         return first_evolution
    elif pokemon == first_evolution:
        return second_evolution
    else: return pokemon

pokemon_data={
    "base_evolution": "Pichu",
    "first_evolution":"Pikachu",
    "second_evolution":"Raichu"
}

print(evolve_pokemon("Pichu",**pokemon_data)) 
# >>Pikachu
print(evolve_pokemon("Raichu",**pokemon_data)) 
# >>Raichu
```
As you see in the example, it is not necessary to pass parameter by parameter to the function, we just passed a dict with the `**` at the beginning to signal that it should be treated as a kwarg.

## Conclusion
Learning python could be a long process, but I hope that with this article, and some future ones, I could explain in a simple manner difficult concepts that are really useful for our journey. 

I hope you find this interesting, and if you want to deepen on something, have suggestions, or want me to explain some other concept, you could leave a comment below.

Happy Programming!