# 15. Slot Classes

**Summary of Benefits:**

1. **Memory Efficiency**: `__slots__` allows you to define a fixed set of attributes, eliminating the overhead of the default `__dict__` storage.
2. **Faster Attribute Access**: By avoiding the dictionary lookup, attribute access can be faster.
3. **Prevention of Dynamic Attributes**: Helps prevent accidental creation of new attributes.

This technique is particularly beneficial when you're working with a large number of objects and need to save memory, such as in data-driven applications or high-performance computing.

***

**1. Basic Use of `__slots__`**

```python
class Point:
    __slots__ = ['x', 'y']  # Define allowed attributes

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2)
print(p.x, p.y)  # Output: 1 2
```

***

**2. Memory Optimization with `__slots__`**

```python
import sys

class Person:
    __slots__ = ['name', 'age']

    def __init__(self, name, age):
        self.name = name
        self.age = age

person = Person("John", 30)
print(sys.getsizeof(person))  # Memory usage for an instance with __slots__

# Without __slots__
class PersonWithoutSlots:
    def __init__(self, name, age):
        self.name = name
        self.age = age

person_no_slots = PersonWithoutSlots("John", 30)
print(sys.getsizeof(person_no_slots))  # Memory usage without __slots__
```

***

**3. Preventing Dynamic Attribute Assignment**

```python
class Rectangle:
    __slots__ = ['width', 'height']

    def __init__(self, width, height):
        self.width = width
        self.height = height

r = Rectangle(5, 10)
try:
    r.color = 'red'  # Raises AttributeError
except AttributeError as e:
    print(f"Error: {e}")
```

***

**4. Using `__slots__` with Inheritance**

```python
class Vehicle:
    __slots__ = ['make', 'model']

    def __init__(self, make, model):
        self.make = make
        self.model = model

class Car(Vehicle):
    __slots__ = ['year']

    def __init__(self, make, model, year):
        super().__init__(make, model)
        self.year = year

car = Car("Toyota", "Corolla", 2020)
print(car.make, car.model, car.year)  # Output: Toyota Corolla 2020
```

***

**5. `__slots__` in Combination with `__dict__`**

```python
class Employee:
    __slots__ = ['name', 'position']

    def __init__(self, name, position):
        self.name = name
        self.position = position

emp = Employee("Alice", "Manager")
print(emp.__dict__)  # Raises AttributeError since __dict__ is not available
```

***

**6. Using `__slots__` for Larger Classes**

```python
class Product:
    __slots__ = ['product_id', 'product_name', 'price', 'category']

    def __init__(self, product_id, product_name, price, category):
        self.product_id = product_id
        self.product_name = product_name
        self.price = price
        self.category = category

product = Product(1, "Laptop", 1200, "Electronics")
print(product.product_name)  # Output: Laptop
```

***

**7. Dynamic Behavior with `__slots__`**

```python
class DynamicPerson:
    __slots__ = ['name', 'age']

    def __init__(self, name, age):
        self.name = name
        self.age = age

dp = DynamicPerson("Bob", 25)
try:
    dp.address = "1234 Elm Street"  # Raises AttributeError
except AttributeError as e:
    print(f"Error: {e}")
```

***

**8. Memory Usage Comparison Between `__slots__` and Normal Classes**

```python
import sys

class ClassWithSlots:
    __slots__ = ['a', 'b']
    def __init__(self, a, b):
        self.a = a
        self.b = b

class NormalClass:
    def __init__(self, a, b):
        self.a = a
        self.b = b

slots_obj = ClassWithSlots(1, 2)
normal_obj = NormalClass(1, 2)

print(f"Slots object size: {sys.getsizeof(slots_obj)}")  # Optimized memory usage
print(f"Normal object size: {sys.getsizeof(normal_obj)}")  # Larger memory usage
```

***

**9. `__slots__` with Class Variables**

```python
class Animal:
    __slots__ = ['name', 'species']
    species = "Unknown"  # Class variable

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

a = Animal("Lion")
print(a.name)      # Output: Lion
print(a.species)   # Output: Unknown
```

***

**10. Trying to Add New Attributes to a Class with `__slots__`**

```python
class Book:
    __slots__ = ['title', 'author']

    def __init__(self, title, author):
        self.title = title
        self.author = author

book = Book("1984", "George Orwell")
try:
    book.publisher = "Secker & Warburg"  # Raises AttributeError
except AttributeError as e:
    print(f"Error: {e}")
```

***
