diff --git a/53.typing_protocols_and_generics.py b/53.typing_protocols_and_generics.py new file mode 100644 index 0000000..95739d1 --- /dev/null +++ b/53.typing_protocols_and_generics.py @@ -0,0 +1,77 @@ +"""Small, commented examples: Protocols, Generics and ParamSpec. + +Keep this file short and runnable. Intended as a quick demo for contributors. +""" + +from typing import Protocol, runtime_checkable, TypeVar, Generic, Callable, ParamSpec + +P = ParamSpec("P") +R = TypeVar("R") +T = TypeVar("T") + + +@runtime_checkable +class SupportsClose(Protocol): + """Structural type: any object with a close() method fits.""" + def close(self) -> None: ... + + +class Socket: + """Simple object that implements close().""" + def __init__(self, name: str) -> None: + self.name = name + + def close(self) -> None: + print(f"Socket {self.name} closed") + + +def close_if_supported(obj: SupportsClose) -> None: + """Call .close() on anything that matches the Protocol. + + This shows structural typing: no inheritance required. + """ + obj.close() + + +class Box(Generic[T]): + """Tiny generic container. + + Static type checkers will remember the element type. + """ + def __init__(self, value: T) -> None: + self._v = value + + def get(self) -> T: + return self._v + + +# ParamSpec decorator: preserves caller signature for typing tools. +def make_logged(func: Callable[P, R]) -> Callable[P, R]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # type: ignore[misc] + print(f"[log] {func.__name__} args={args} kwargs={kwargs}") + res = func(*args, **kwargs) + print(f"[log] {func.__name__} -> {res}") + return res + + return wrapper + + +@make_logged +def greet(name: str, excited: bool = False) -> str: + return "Hello, " + name + ("!!!" if excited else ".") + + +if __name__ == "__main__": + # Protocol demo + s = Socket("A") + close_if_supported(s) + + # Generic demo + bi = Box[int](10) + bs = Box[str]("ok") + print(type(bi.get()), bi.get()) + print(type(bs.get()), bs.get()) + + # ParamSpec demo (runtime logs and preserved signature for mypy) + print(greet("World")) + print(greet("Lin", excited=True)) diff --git a/54.descriptors.py b/54.descriptors.py new file mode 100644 index 0000000..4a77376 --- /dev/null +++ b/54.descriptors.py @@ -0,0 +1,87 @@ +"""Short descriptor examples with human-like comments. + +Shows a Typed data descriptor and a simple cached property. +""" + +from typing import Any + + +class Typed: + """Data descriptor that enforces the value type. + + We store the value in the instance.__dict__ under the provided name. + This raises on wrong types so callers get early feedback. + """ + + def __init__(self, name: str, expected_type: type) -> None: + self.name = name + self.expected_type = expected_type + + def __set_name__(self, owner, name): + # saves the attribute name if the class assigns a different one + self.public_name = name + + def __get__(self, instance: Any, owner: type = None) -> Any: + # when accessed on the class, return the descriptor itself + if instance is None: + return self + return instance.__dict__.get(self.name) + + def __set__(self, instance: Any, value: Any) -> None: + # enforce the expected type + if not isinstance(value, self.expected_type): + raise TypeError(f"{self.name} must be {self.expected_type.__name__}") + instance.__dict__[self.name] = value + + +class CachedProperty: + """A tiny cached-property: compute once, then store on the instance. + + This is a non-data descriptor (no __set__), so assigning the cached name + on the instance bypasses future descriptor lookups. + """ + + def __init__(self, func): + self.func = func + self.__doc__ = getattr(func, '__doc__') + + def __get__(self, instance, owner=None): + if instance is None: + return self + value = self.func(instance) + instance.__dict__[self.func.__name__] = value + return value + + +class Point: + # descriptors: declare at the class level + x = Typed('x', int) + y = Typed('y', int) + + def __init__(self, x: int, y: int) -> None: + # these assignments go through Typed.__set__ + self.x = x + self.y = y + + @CachedProperty + def hypot(self): + # small computation that we'd like to cache + print('computing hypot') + from math import hypot + return hypot(self.x, self.y) + + +if __name__ == '__main__': + p = Point(3, 4) + print('x, y:', p.x, p.y) + + # wrong type assignment shows the guard + try: + p.x = 2.5 + except TypeError as e: + print('caught type error:', e) + + # cached property: first access computes, second returns cached value + print('first hypot:', p.hypot) + print('second hypot (cached):', p.hypot) + print('instance keys:', list(p.__dict__.keys())) diff --git a/handbook/50_main_concepts_53_54.md b/handbook/50_main_concepts_53_54.md new file mode 100644 index 0000000..29d45be --- /dev/null +++ b/handbook/50_main_concepts_53_54.md @@ -0,0 +1,37 @@ +# Advanced Python Interview Reference + +## Concepts 53–54 (Senior-Level) + +--- + +## 53 — Typing: Protocols & Generics + + +Protocols and generics are tools from Python’s static typing system that allow you to write flexible APIs without losing type safety. + +Common use cases: + +- Polymorphic interfaces (duck typing) +- Flexible function/type arguments +- Safer, clearer APIs + +**Related Examples** +- [`53.typing_protocols_and_generics.py`](../53.typing_protocols_and_generics.py) + +--- + +## 54 — Descriptors (Typed + CachedProperty) + +Descriptors let you control how attributes are accessed and stored on objects. They're used to add type enforcement, lazy computation, or caching to attribute access. + +Common use cases: + +- Type validation on assignment +- Cached/computed properties (like @property but cached) +- Logging or auditing attribute changes +- Read-only or write-only attributes +- Custom attribute behavior in libraries and frameworks + + +**Related Examples** +- [`54.descriptors.py`](../54.descriptors.py)