# 96. Class Decorators

***

**1. Basic Class Decorator**

```python
def add_method(cls):
    cls.new_method = lambda self: "New Method Added"
    return cls

@add_method
class MyClass:
    def existing_method(self):
        return "Existing Method"

obj = MyClass()
print(obj.existing_method())  # Output: Existing Method
print(obj.new_method())       # Output: New Method Added
```

***

**2. Logging Class Instantiation**

```python
def log_instantiation(cls):
    original_init = cls.__init__

    def new_init(self, *args, **kwargs):
        print(f"Creating an instance of {cls.__name__}")
        original_init(self, *args, **kwargs)

    cls.__init__ = new_init
    return cls

@log_instantiation
class MyClass:
    def __init__(self, name):
        self.name = name

obj = MyClass("Test")  # Output: Creating an instance of MyClass
```

***

**3. Singleton Pattern with Class Decorator**

```python
def singleton(cls):
    instances = {}

    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance

@singleton
class MyClass:
    pass

a = MyClass()
b = MyClass()
print(a is b)  # Output: True
```

***

**4. Adding Class Attributes**

```python
def add_class_attributes(cls):
    cls.category = "Example Class"
    return cls

@add_class_attributes
class MyClass:
    pass

print(MyClass.category)  # Output: Example Class
```

***

**5. Validating Attribute Values**

```python
def validate_values(cls):
    original_setattr = cls.__setattr__

    def new_setattr(self, key, value):
        if key == "age" and value < 0:
            raise ValueError("Age cannot be negative")
        original_setattr(self, key, value)

    cls.__setattr__ = new_setattr
    return cls

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

person = Person("John", 25)
person.age = 30  # Valid
# person.age = -5  # Raises ValueError
```

***

**6. Counting Instances of a Class**

```python
def count_instances(cls):
    cls.instance_count = 0
    original_init = cls.__init__

    def new_init(self, *args, **kwargs):
        cls.instance_count += 1
        original_init(self, *args, **kwargs)

    cls.__init__ = new_init
    return cls

@count_instances
class MyClass:
    def __init__(self):
        pass

a = MyClass()
b = MyClass()
print(MyClass.instance_count)  # Output: 2
```

***

**7. Enforcing Method Call Logging**

```python
def log_method_calls(cls):
    for attr_name, attr_value in cls.__dict__.items():
        if callable(attr_value):
            def logged_method(method):
                def wrapper(*args, **kwargs):
                    print(f"Calling {method.__name__}")
                    return method(*args, **kwargs)
                return wrapper
            setattr(cls, attr_name, logged_method(attr_value))
    return cls

@log_method_calls
class MyClass:
    def greet(self):
        return "Hello!"

obj = MyClass()
print(obj.greet())  # Output: Calling greet \n Hello!
```

***

**8. Extending Class with a Mixin**

```python
def add_mixin(cls):
    class Mixin:
        def mixin_method(self):
            return "Mixin Method"

    cls.mixin_method = Mixin().mixin_method
    return cls

@add_mixin
class MyClass:
    pass

obj = MyClass()
print(obj.mixin_method())  # Output: Mixin Method
```

***

**9. Restricting Attribute Creation**

```python
def restrict_attributes(cls):
    cls.__slots__ = ['name', 'age']
    return cls

@restrict_attributes
class Person:
    pass

p = Person()
p.name = "John"
p.age = 30
# p.address = "123 Street"  # AttributeError: 'Person' object has no attribute 'address'
```

***

**10. Timing Class Method Execution**

```python
import time

def time_methods(cls):
    for attr_name, attr_value in cls.__dict__.items():
        if callable(attr_value):
            def timed_method(method):
                def wrapper(*args, **kwargs):
                    start = time.time()
                    result = method(*args, **kwargs)
                    end = time.time()
                    print(f"Execution time for {method.__name__}: {end - start:.6f} seconds")
                    return result
                return wrapper
            setattr(cls, attr_name, timed_method(attr_value))
    return cls

@time_methods
class MyClass:
    def slow_method(self):
        time.sleep(1)
        return "Finished"

obj = MyClass()
print(obj.slow_method())  
# Output: Execution time for slow_method: 1.000xxx seconds \n Finished
```

***

These examples show how to use class decorators to modify or extend class behavior, from adding attributes or methods to enforcing constraints or improving functionality.
