Days 2-8 of my #100DaysOfCode journey: Whew!
Tags: python, 100daysofcode, beginners, programming
Published: 2023-04-08 20:34:31

During days 2-8 of my #100DaysOfCode journey, I faced some challenges. Although I breezed through simpler concepts in the beginning, work picked up as we entered the second quarter of the year. Despite this, I persevered and completed lessons from Day 4 through Day 24 of the Udemy course. Topics covered included randomization, lists, for-loops, range, functions, dictionaries, basic Turtle graphics, and even a Snake game project, which was incredibly fun. I concluded with File, Directories, and Paths at Day 24.

At this point, I decided to delve deeper into Object-Oriented Programming in Python. I found "[Python 3: Object-Oriented Programming](https://subscription.packtpub.com/search?query=Python+3%3A+Object-Oriented+Programming)" by [Dusty Phillips](https://www.packtpub.com/authors/dusty-phillips) on my [Packt Publishing](https://www.packtpub.com/) account and worked through nearly half of the book before taking a breather. 
The author's explanations were excellent. I would highly recommend it.

I was pleasantly surprised by the similarities between Java and Python, despite some syntax differences. The code snippet below demonstrates familiar concepts such as classes, inheritance, constructors, super, getters, setters, try-catch-finally, custom exceptions, throwing (raising) exceptions, overriding, and toString (repr). I found even more similarities as I continued. Here are a few examples:

```
class InvalidAge(Exception):
    pass


class Thing:
    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError("Name should be string")
        self._name = value

    @name.deleter
    def name(self):
        """ this would be weird though in this particular case """
        print("Name deleted")
        del self._name

    def __repr__(self):
        return f"{self.name}"


class Person:
    def __init__(self, name):
        self.name = name

    def say_something(self):
        print(f"Hello, my name is {self.name}")

    def __repr__(self):
        return f"{self.name}"


class Friend(Person):
    def __init__(self, name, email, age):
        super().__init__(name)
        self.email = email
        if not isinstance(age, int):
            raise TypeError("Age should be integer")
        if age < 1 or age >= 120:
            raise InvalidAge("Age should be greater than 0 or less than 120")
        self.age = age

    def say_something(self):
        print(f"Hey, What's up?")

    def __repr__(self):
        return f"{self.name} {self.email} {self.age}"


if __name__ == "__main__":
    try:
        person = Person("John")
        friend = Friend("Jack", "jack@test", 70)
        thing = Thing("thing1")
        print(person)
        print(friend)
        print(thing.name)
        del thing.name
    except TypeError as e:
        print("Error: ", e)
    finally:
        print("finished")
```

As you can see, a lot of familiar things are demonstrated above like classes, inheritance, constructors, super, getters, setters, try-catch-finally, custom exceptions, throwing (raising) exceptions, overriding and toString (__repr__). There are even more similarities that I found.

Upon reaching the Data Structures chapter, things became more exciting. I was already familiar with the concept of tuples, 
which are immutable objects that store other objects, from my experience with Java and C#. I was pleasantly surprised by tuple unpacking, which is similar to JavaScript's destructuring assignment statement.

```
name, email, age = ("Matt", "matt@example.com", 25)
print(name, email, age)
```

Named tuples and data classes were entirely new concepts for me. Named tuples are created using a factory function in the collections package, while data classes are similar to tuples but allow for mutable attributes.

```
from collections import namedtuple

Superhero = namedtuple("Superhero", ["name", "alias", "level"])
superman = Superhero(name="superman", alias="Clark Kent", level=95)
print(superman)
print(superman.name)
print(superman.alias)
print(superman.level)
```

Dataclasses on the other hand are almost quite the same as Tuple in that it allows named fields however, one huge difference is that Dataclasses are not immutable. It's attributes can be updated and one could even add more attributes to them.

```
from dataclasses import make_dataclass

Superhero = make_dataclass("Superhero", ["name", "alias", "level"])
superman = Superhero(name="superman", alias="Clark Kent", level=95)
print(superman)
print(superman.name)
print(superman.alias)
print(superman.level)
```

A more common way to define dataclasses is to use a class decorate called @dataclass

```
from dataclasses import dataclass

@dataclass
class Superhero:
    name: str
    alias: str
    level: int

superman = Superhero(name="superman", alias="Clark Kent", level=95)
print(superman)
print(superman.name)
print(superman.alias)
print(superman.level)
```

Dictionaries seemed pretty standard at first glance. However, defaultdict and Counter methods really caught my attention due to their elegance and compactness.

```
superheroes = {
    "superman": ("Clark Kent", 95),
    "batman": ("Bruce Wayne", 40)
}

print(superheroes["superman"])
print(superheroes["batman"])
```

As expected, we get a KeyError when we try to access a key that doesn’t exist in the dictionary.

`print(superheroes["flash"])`

And one way around it is the use the get method on the dictionary: 

`print(superheroes.get("flash", "Not a superhero"))`

Now, these next two methods really caught me by surprise and ones that I really like: defaultdict and Counter.

```
from collections import defaultdict
def num_frequency(num_list):
    totals = defaultdict(int)
    for value in num_list:
        totals[value] += 1
    return totals

numbers = [5, 3, 2, 5, 2]
print(num_frequency(numbers))
```

The code above figures out the frequency of numbers and puts them in a dictionary. But, if you think it's not possible to make 
the code even more compact, you're in for a big surprise. I certainly was:

```
from collections import Counter

numbers = [5, 3, 2, 5, 2]
print(Counter(numbers))
```

Guess what, it does the same thing but with even less code!

And another equally impressive example:

```
from collections import Counter

responses = ["superman", "batman", "superman", "superman", "flash", "batman", "superman"]

print(f"The children voted for {Counter(responses).most_common(1)[0][0]}")
print(f"The number of votes for this superhero were: {Counter(responses).most_common(1)[0][1]}")
```

It's only been eight days, and I feel like I'm making excellent progress. I hope to maintain this pace for the remaining 92 days. 
Until next time!