# 28. Weak References

This Python code snippets demonstrates the use of the `weakref` module to manage objects without increasing their reference count:

***

**1. Creating a Weak Reference**

```python
import weakref

class Demo:
    pass

obj = Demo()
weak_ref = weakref.ref(obj)  # Create a weak reference
print("Object via weak reference:", weak_ref())  # Access the object

del obj  # Delete the strong reference
print("Object after deletion:", weak_ref())  # None: object has been garbage collected
```

***

**2. Using `weakref.proxy`**

```python
import weakref

class Demo:
    def greet(self):
        return "Hello!"

obj = Demo()
proxy = weakref.proxy(obj)  # Create a proxy weak reference
print(proxy.greet())  # Access methods via the proxy

del obj
try:
    print(proxy.greet())  # Raises ReferenceError since the object is gone
except ReferenceError:
    print("Object has been garbage collected.")
```

***

**3. Weak Reference Callbacks**

```python
import weakref

class Demo:
    pass

def on_finalize(weak_ref):
    print("Object has been finalized and garbage collected.")

obj = Demo()
weak_ref = weakref.ref(obj, on_finalize)  # Attach a callback
del obj  # Triggers the callback
```

***

**4. Weak References in a Dictionary**

```python
import weakref

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

obj1 = Demo("Object1")
obj2 = Demo("Object2")

weak_dict = weakref.WeakValueDictionary()
weak_dict["key1"] = obj1
weak_dict["key2"] = obj2

print("Before deletion:", weak_dict.keys())
del obj1
print("After deletion of obj1:", weak_dict.keys())
```

***

**5. Weak References and Cyclic Garbage Collection**

```python
import gc
import weakref

class Demo:
    pass

a = Demo()
b = Demo()

a.ref = b
b.ref = weakref.ref(a)  # Use weak reference to break the strong reference cycle

del a
del b

print("Unreachable objects:", gc.collect())
```

***

**6. Weak References with Custom Objects**

```python
import weakref

class Custom:
    def __init__(self, value):
        self.value = value

obj = Custom(42)
weak_ref = weakref.ref(obj)
print("Value via weak reference:", weak_ref().value)

del obj
print("After deletion:", weak_ref())
```

***

**7. Tracking Object Lifecycle with Weak References**

```python
import weakref

class Demo:
    def __del__(self):
        print("Demo object deleted.")

obj = Demo()
weak_ref = weakref.ref(obj)
print("Object exists:", weak_ref() is not None)

del obj
print("Object after deletion:", weak_ref())
```

***

**8. Using `WeakSet`**

```python
import weakref

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

obj1 = Demo("Obj1")
obj2 = Demo("Obj2")

weak_set = weakref.WeakSet([obj1, obj2])
print("WeakSet contents:", [obj.name for obj in weak_set])

del obj1
print("After deletion of obj1:", [obj.name for obj in weak_set])
```

***

**9. Preventing Reference Count Increments**

```python
import weakref

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

obj = Demo("MyObject")
weak_ref = weakref.ref(obj)

print("Reference count before accessing weak ref:", weakref.getweakrefcount(obj))
_ = weak_ref()  # Accessing weak reference doesn't increase reference count
print("Reference count after accessing weak ref:", weakref.getweakrefcount(obj))
```

***

**10. WeakValueDictionary for Cache Management**

```python
import weakref

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

cache = weakref.WeakValueDictionary()

obj1 = Demo("CacheObject1")
cache["obj1"] = obj1

print("Cache keys before deletion:", list(cache.keys()))
del obj1
print("Cache keys after deletion:", list(cache.keys()))
```

***

These examples illustrate how the `weakref` module can be used to manage object lifetimes without increasing reference counts, enabling efficient memory management and avoiding memory leaks caused by cyclic references.
