Skip to main content

Dynamic Attributes

Source: Fluent Python, 2nd Edition — Chapter 23 by Luciano Ramalho (OSCON JSON through @cached_property)

JSON as nested dicts works; feed['Schedule']['events'][40]['name'] does not feel like JavaScript’s feed.Schedule.events[40].name. This lesson: a read-only façade (FrozenJSON), then records keyed by serial, then properties that dereference foreign keys — including the trap where a property has the same name as the field. Read/write properties are the next slice.

OSCON feed​

One object, key Schedule, four lists: conferences (1), events (484), speakers (357), venues (53). Every record has a unique serial. Events store lists of speaker serials, not nested speaker objects — so events[40]['speakers'] is [3471, 5199], not names.

FrozenJSON​

__getattr__ runs only after the instance, class, and bases fail. Two methods + __data:

  1. getattr(self.__data, name) — so .keys() / .items() are dict methods, not JSON keys.
  2. Else FrozenJSON.build(self.__data[name]).

build: Mapping → FrozenJSON; MutableSequence → list of builds; else leave str/int. Goose typing (abc.Mapping). Nested dicts become FrozenJSON as you walk; no cache. Missing key: Ramalho left KeyError (not AttributeError) to keep __getattr__ short.

Lists stay lists; elements that were objects become FrozenJSON (type(talk)).

wrap already uses getattr for a possibly-invalid identifier. Your __getattr__ + build must turn outer into another FrozenJSON.

Invalid names​

{'class': 1982} → student.class is a SyntaxError. keyword.iskeyword: append _ in __init__ → student.class_. getattr(student, 'class') still works on the raw key if you did not rename.

'2be' is not an identifier (str.isidentifier()). Raising or attr_0 — book skips it.

__new__ is the constructor​

__init__(self, ...) is an initializer; the object exists; it must return None. __new__(cls, ...) is a classmethod without @classmethod. Interpreter: obj = C.__new__(...); if isinstance(obj, C) then C.__init__(obj, ...). If __new__ returns a list or an int, __init is skipped.

explore2: build logic lives in __new__. Mapping → super().__new__(cls) (actually object.__new__(FrozenJSON)). List → [cls(item) for item in arg]. Else return arg. __getattr__ then does FrozenJSON(self.__data[name]) instead of .build.

Quiz

__new__ and keywords

1 / 2

FrozenJSON.__new__ returns a list for a JSON array

Pick an answer to see if you are right.

Record: data-driven attributes​

Converted OSCON is flat: strings, ints, lists of those — no nested mappings. You do not need FrozenJSON’s .keys() on __data.

class Record:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)

Same idea as argparse.Namespace / multiprocessing.Namespace. Keys speaker.3471. load: for each Schedule list, record_type = collection[:-1] (speakers → speaker), Record(**raw_record). __slots__ would block this __dict__ trick.

Invalid keys in the wild: same as FrozenJSON; OSCON is clean.

Linked records: Event.venue​

Want event.venue.name like a Django ForeignKey. Record.fetch(key) is a @staticmethod on Record.__index (lazy load()). Not a @classmethod — cls would be a lie if you call Event.fetch.

@property
def venue(self):
key = f'venue.{self.venue_serial}'
return self.__class__.fetch(key)

self.fetch(key) is wrong if any event JSON has a field named fetch — that instance attribute shadows the method. self.__class__.fetch stays the staticmethod. A mapping with __getitem__ would not have this clash; we are here to feel dynamic attributes.

load v2: cls_name = record_type.capitalize() → globals().get(cls_name, Record) if inspect.isclass and issubclass(..., Record) else Record. Only Event exists at first; add Speaker later without touching the loop.

Event.__repr__: use .name if present.

Property that overrides a field: speakers​

The JSON field is speakers (list of ints). The property is also speakers (list of Record). self.speakers inside the property → RecursionError. Bypass: self.__dict__['speakers']. Lookup class first: a property wins over the instance dict — Chapter 24 will call that an overriding descriptor.

Handmade cache self.__speaker_objs after init breaks PEP 412 key-sharing. Pre-create None in Event.__init__. Two threads filling the cache: torn reads. @cached_property (3.8) is thread-safe (reentrant lock) but is not a real property: non-overriding descriptor — first miss runs the method and writes self.venue; later reads/writes are the attribute. Delete the attr to recompute. Unsuitable for speakers (already an instance field of that name). No __slots__. Still creates a dict key after __init__.

Docs: stack @property on @cache (order: speakers = property(cache(speakers))) so the name clash is handled like a normal property and the function is memoized.

Quiz

Shadowing

1 / 2

Inside Event.speakers property, self.speakers

Pick an answer to see if you are right.

If you write self.speakers in the getter, the process dies on recursion (or you never return the scaled list).