# Making Objects Behave Like Built-ins
# __str__, __repr__, __len__, __eq__, __contains__
# redhorndev.com

# ─────────────────────────────────────────────
# Without special methods
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, species, age):
        self.name    = name
        self.species = species
        self.age     = age
        self.status  = "available"

lassie = Animal("Lassie", "dog", 4)
print(lassie)       # <__main__.Animal object at 0x...> — useless

# ─────────────────────────────────────────────
# __str__ — readable string representation
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, species, age):
        self.name    = name
        self.species = species
        self.age     = age
        self.status  = "available"

    def __str__(self):
        return f"{self.name} | {self.species} | age: {self.age} | {self.status}"

lassie = Animal("Lassie", "dog", 4)
print(lassie)           # Lassie | dog | age: 4 | available
print(str(lassie))      # same

# ─────────────────────────────────────────────
# __repr__ — developer representation
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, species, age):
        self.name    = name
        self.species = species
        self.age     = age
        self.status  = "available"

    def __str__(self):
        return f"{self.name} | {self.species} | age: {self.age} | {self.status}"

    def __repr__(self):
        return f"Animal('{self.name}', '{self.species}', {self.age})"

lassie = Animal("Lassie", "dog", 4)
print(repr(lassie))     # Animal('Lassie', 'dog', 4)

# __str__ for users — __repr__ for developers
# __repr__ used as fallback if __str__ not defined

# ─────────────────────────────────────────────
# __len__ — support for len()
# ─────────────────────────────────────────────

class Shelter:
    def __init__(self, name):
        self.name    = name
        self.animals = []

    def add(self, animal):
        self.animals.append(animal)

    def __len__(self):
        return len(self.animals)

    def __str__(self):
        return f"{self.name} — {len(self)} animals"

shelter = Shelter("Safe Paws")
shelter.add(Animal("Lassie",   "dog", 4))
shelter.add(Animal("Whiskers", "cat", 2))
shelter.add(Animal("Rex",      "dog", 6))

print(len(shelter))     # 3
print(shelter)          # Safe Paws — 3 animals

# ─────────────────────────────────────────────
# __eq__ — support for ==
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, species, age):
        self.name    = name
        self.species = species
        self.age     = age

    def __eq__(self, other):
        return self.name == other.name and self.species == other.species

a = Animal("Lassie", "dog", 4)
b = Animal("Lassie", "dog", 6)  # same name and species, different age
c = Animal("Rex",    "dog", 4)  # different name

print(a == b)   # True  — name and species match
print(a == c)   # False — different name

# ─────────────────────────────────────────────
# __contains__ — support for in
# ─────────────────────────────────────────────

class Shelter:
    def __init__(self):
        self.animals = []

    def add(self, animal):
        self.animals.append(animal)

    def __contains__(self, animal):
        return animal in self.animals

    def __len__(self):
        return len(self.animals)

shelter = Shelter()
lassie  = Animal("Lassie", "dog", 4)
shelter.add(lassie)

print(lassie in shelter)                    # True
print(Animal("Rex", "dog", 6) in shelter)   # False

# ─────────────────────────────────────────────
# Quick reference
# ─────────────────────────────────────────────

# __str__(self)              — print(obj), str(obj)       → readable string
# __repr__(self)             — repr(obj), shell display   → developer string
# __len__(self)              — len(obj)                   → integer
# __eq__(self, other)        — obj == other               → bool
# __contains__(self, item)   — item in obj                → bool
#
# dunder methods called automatically — never call directly
# always define __str__ — makes debugging much easier
# __repr__ is fallback when __str__ not defined
