Replace lazyproperty with functools.cached_property (#4282)
## Summary
- Replace custom `lazyproperty` descriptor with stdlib
`functools.cached_property`
- Fix bug where 26 properties returning `None` were re-evaluated on
every access instead of caching — `lazyproperty.__get__` uses `if value
is None` to detect a cache miss, so any property that legitimately
returns `None` re-runs on every access
- Slight performance improvement on cached reads — `cached_property` is
a non-data descriptor, so after first access the `__dict__` entry
shadows the descriptor directly (plain dict lookup vs `__get__` call)
- Nothing in the codebase ever assigns to a lazyproperty-decorated
attribute, so dropping the write-protection from the data descriptor has
no behavioral impact