Skip to content

Commit dea70ad

Browse files
committed
Add dict index to CommandDefinitions and States for O(1) lookups
Both containers used O(n) linear scans for __contains__, __getitem__, get, and select. These are called on hot paths during Home Assistant polling cycles across many devices.
1 parent d6edb32 commit dea70ad

1 file changed

Lines changed: 17 additions & 18 deletions

File tree

pyoverkiz/models.py

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -445,38 +445,37 @@ class CommandDefinitions:
445445
"""Container for command definitions providing convenient lookup by name."""
446446

447447
_commands: list[CommandDefinition]
448+
_index: dict[str, CommandDefinition]
448449

449450
def __init__(self, commands: list[dict[str, Any]]):
450451
"""Build the inner list of CommandDefinition objects from raw data."""
451452
self._commands = [CommandDefinition(**command) for command in commands]
453+
self._index = {cd.command_name: cd for cd in self._commands}
452454

453455
def __iter__(self) -> Iterator[CommandDefinition]:
454456
"""Iterate over defined commands."""
455457
return self._commands.__iter__()
456458

457459
def __contains__(self, name: object) -> bool:
458460
"""Return True if a command with `name` exists."""
459-
return any(cd.command_name == name for cd in self._commands)
461+
return name in self._index
460462

461463
def __getitem__(self, command: str) -> CommandDefinition:
462464
"""Return the command definition or raise KeyError if missing."""
463-
result = next((cd for cd in self._commands if cd.command_name == command), None)
464-
if result is None:
465-
raise KeyError(command)
466-
return result
465+
return self._index[command]
467466

468467
def __len__(self) -> int:
469468
"""Return number of command definitions."""
470469
return len(self._commands)
471470

472471
def get(self, command: str) -> CommandDefinition | None:
473472
"""Return the command definition or None if missing."""
474-
return next((cd for cd in self._commands if cd.command_name == command), None)
473+
return self._index.get(command)
475474

476475
def select(self, commands: list[str | OverkizCommand]) -> str | None:
477476
"""Return the first command name that exists in this definition, or None."""
478477
return next(
479-
(str(command) for command in commands if str(command) in self), None
478+
(str(command) for command in commands if str(command) in self._index), None
480479
)
481480

482481
def has_any(self, commands: list[str | OverkizCommand]) -> bool:
@@ -584,49 +583,49 @@ class States:
584583
"""Container of State objects providing lookup and mapping helpers."""
585584

586585
_states: list[State]
586+
_index: dict[str, State]
587587

588588
def __init__(self, states: list[dict[str, Any]] | None = None) -> None:
589589
"""Create a container of State objects from raw state dicts or an empty list."""
590590
if states:
591591
self._states = [State(**state) for state in states]
592592
else:
593593
self._states = []
594+
self._index = {state.name: state for state in self._states}
594595

595596
def __iter__(self) -> Iterator[State]:
596597
"""Return an iterator over contained State objects."""
597598
return self._states.__iter__()
598599

599600
def __contains__(self, name: object) -> bool:
600601
"""Return True if a state with the given name exists in the container."""
601-
return any(state.name == name for state in self._states)
602+
return name in self._index
602603

603604
def __getitem__(self, name: str) -> State:
604605
"""Return the State with the given name or raise KeyError if missing."""
605-
result = next((state for state in self._states if state.name == name), None)
606-
if result is None:
607-
raise KeyError(name)
608-
return result
606+
return self._index[name]
609607

610608
def __setitem__(self, name: str, state: State) -> None:
611609
"""Set or append a State identified by name."""
612-
found = self.get(name)
613-
if found is None:
614-
self._states.append(state)
610+
if name in self._index:
611+
idx = self._states.index(self._index[name])
612+
self._states[idx] = state
615613
else:
616-
self._states[self._states.index(found)] = state
614+
self._states.append(state)
615+
self._index[name] = state
617616

618617
def __len__(self) -> int:
619618
"""Return number of states in the container."""
620619
return len(self._states)
621620

622621
def get(self, name: str) -> State | None:
623622
"""Return the State with the given name or None if missing."""
624-
return next((state for state in self._states if state.name == name), None)
623+
return self._index.get(name)
625624

626625
def select(self, names: list[str]) -> State | None:
627626
"""Return the first State that exists and has a non-None value, or None."""
628627
for name in names:
629-
state = self.get(name)
628+
state = self._index.get(name)
630629
if state is not None and state.value is not None:
631630
return state
632631
return None

0 commit comments

Comments
 (0)