Refactor: deep methods reworked for better performance

This commit is contained in:
LmeSzinc 2025-03-17 01:38:51 +08:00
parent 93644384cf
commit 477f917262
16 changed files with 560 additions and 120 deletions

View File

@ -9,7 +9,7 @@ from cached_property import cached_property
from module.base.decorator import del_cached_property from module.base.decorator import del_cached_property
from module.config.config import AzurLaneConfig, TaskEnd from module.config.config import AzurLaneConfig, TaskEnd
from module.config.utils import deep_get, deep_set from module.config.deep import deep_get, deep_set
from module.exception import * from module.exception import *
from module.logger import logger from module.logger import logger
from module.notify import handle_notify from module.notify import handle_notify

View File

@ -1,16 +1,17 @@
import copy import copy
import datetime
import operator import operator
import threading import threading
from datetime import datetime, timedelta
import pywebio import pywebio
from module.base.filter import Filter from module.base.filter import Filter
from module.config.config_generated import GeneratedConfig from module.config.config_generated import GeneratedConfig
from module.config.config_manual import ManualConfig, OutputConfig from module.config.config_manual import ManualConfig, OutputConfig
from module.config.config_updater import ConfigUpdater from module.config.config_updater import ConfigUpdater, ensure_time, get_server_next_update, nearest_future
from module.config.deep import deep_get, deep_set
from module.config.utils import DEFAULT_TIME, dict_to_kv, filepath_config, get_os_reset_remain, path_to_arg
from module.config.watcher import ConfigWatcher from module.config.watcher import ConfigWatcher
from module.config.utils import *
from module.exception import RequestHumanTakeover, ScriptError from module.exception import RequestHumanTakeover, ScriptError
from module.logger import logger from module.logger import logger
from module.map.map_grids import SelectedGrids from module.map.map_grids import SelectedGrids

View File

@ -6,10 +6,11 @@ from cached_property import cached_property
from deploy.utils import DEPLOY_TEMPLATE, poor_yaml_read, poor_yaml_write from deploy.utils import DEPLOY_TEMPLATE, poor_yaml_read, poor_yaml_write
from module.base.timer import timer from module.base.timer import timer
from module.config.deep import deep_default, deep_get, deep_iter, deep_pop, deep_set
from module.config.env import IS_ON_PHONE_CLOUD from module.config.env import IS_ON_PHONE_CLOUD
from module.config.redirect_utils.utils import *
from module.config.server import VALID_CHANNEL_PACKAGE, VALID_PACKAGE, VALID_SERVER_LIST, to_package, to_server from module.config.server import VALID_CHANNEL_PACKAGE, VALID_PACKAGE, VALID_SERVER_LIST, to_package, to_server
from module.config.utils import * from module.config.utils import *
from module.config.redirect_utils.utils import *
CONFIG_IMPORT = ''' CONFIG_IMPORT = '''
import datetime import datetime

533
module/config/deep.py Normal file
View File

@ -0,0 +1,533 @@
from collections import deque
# deep_* functions are used for access nested dictionary.
# They target for high performance so code are complicated to read
# In general performance practise, time costs are as below:
# - When key exists
# try: dict[key] except KeyError << dict.get(key) < if key in dict: dict[key]
# - When not key exists
# if key in dict: dict[key] < dict.get(key) <<< try: dict[key] except KeyError
OP_ADD = 'add'
OP_SET = 'set'
OP_DEL = 'del'
def deep_get(d, keys, default=None):
"""
Get value from nested dict and list
https://stackoverflow.com/questions/25833613/safe-method-to-get-value-of-nested-dictionary
Args:
d (dict):
keys (list[str], str): Such as ['Scheduler', 'NextRun', 'value']
default: Default return if key not found.
Returns:
Value on given keys
"""
# 240 + 30 * depth (ns)
if type(keys) is str:
keys = keys.split('.')
try:
for k in keys:
d = d[k]
return d
# No such key
except KeyError:
return default
# No such key
except IndexError:
return default
# Input `keys` is not iterable or input `d` is not dict
# list indices must be integers or slices, not str
except TypeError:
return default
def deep_get_with_error(d, keys):
"""
Get value from nested dict and list, raise KeyError if key not exists
Args:
d (dict):
keys (list[str], str): Such as ['Scheduler', 'NextRun', 'value']
Returns:
Value on given keys
Raises:
KeyError: If key not exists
"""
# 240 + 30 * depth (ns)
if type(keys) is str:
keys = keys.split('.')
try:
for k in keys:
d = d[k]
return d
# No such key
# except KeyError:
# raise
# No such key
except IndexError:
raise KeyError
# Input `keys` is not iterable or input `d` is not dict
# list indices must be integers or slices, not str
except TypeError:
raise KeyError
def deep_exist(d, keys):
"""
Check if keys exists in nested dict or list
Args:
d (dict):
keys (str, list): Such as `Scheduler.NextRun.value`
Returns:
bool: If key exists
"""
# 240 + 30 * depth (ns)
if type(keys) is str:
keys = keys.split('.')
try:
for k in keys:
d = d[k]
return True
# No such key
except KeyError:
return False
# No such key
except IndexError:
return False
# Input `keys` is not iterable or input `d` is not dict
# list indices must be integers or slices, not str
except TypeError:
return False
def deep_set(d, keys, value):
"""
Set value into nested dict safely, imitating deep_get().
Can only set dict
"""
# 150 * depth (ns)
if type(keys) is str:
keys = keys.split('.')
first = True
exist = True
prev_d = None
prev_k = None
prev_k2 = None
try:
for k in keys:
if first:
prev_d = d
prev_k = k
first = False
continue
try:
# if key in dict: dict[key] > dict.get > dict.setdefault > try dict[key] except
if exist and prev_k in d:
prev_d = d
d = d[prev_k]
else:
exist = False
new = {}
d[prev_k] = new
d = new
except TypeError:
# `d` is not dict
exist = False
d = {}
prev_d[prev_k2] = {prev_k: d}
prev_k2 = prev_k
prev_k = k
# prev_k2, prev_k = prev_k, k
# Input `keys` is not iterable
except TypeError:
return
# Last key, set value
try:
d[prev_k] = value
return
# Last value `d` is not dict
except TypeError:
prev_d[prev_k2] = {prev_k: value}
return
def deep_default(d, keys, value):
"""
Set value into nested dict safely, imitating deep_get().
Can only set dict
"""
# 150 * depth (ns)
if type(keys) is str:
keys = keys.split('.')
first = True
exist = True
prev_d = None
prev_k = None
prev_k2 = None
try:
for k in keys:
if first:
prev_d = d
prev_k = k
first = False
continue
try:
# if key in dict: dict[key] > dict.get > dict.setdefault > try dict[key] except
if exist and prev_k in d:
prev_d = d
d = d[prev_k]
else:
exist = False
new = {}
d[prev_k] = new
d = new
except TypeError:
# `d` is not dict
exist = False
d = {}
prev_d[prev_k2] = {prev_k: d}
prev_k2 = prev_k
prev_k = k
# prev_k2, prev_k = prev_k, k
# Input `keys` is not iterable
except TypeError:
return
# Last key, set value
try:
d.setdefault(prev_k, value)
return
# Last value `d` is not dict
except AttributeError:
prev_d[prev_k2] = {prev_k: value}
return
def deep_pop(d, keys, default=None):
"""
Pop value from nested dict and list
"""
if type(keys) is str:
keys = keys.split('.')
try:
for k in keys[:-1]:
d = d[k]
# No `pop(k, default)` so it can pop list
return d.pop(keys[-1])
# No such key
except KeyError:
return default
# Input `keys` is not iterable or input `d` is not dict
# list indices must be integers or slices, not str
except TypeError:
return default
# Input `keys` out of index
except IndexError:
return default
# Last `d` is not dict
except AttributeError:
return default
def deep_iter_depth1(data):
"""
Equivalent to data.items() but suppress error if data is not a dict
Args:
data:
Yields:
Any: Key
Any: Value
"""
try:
for k, v in data.items():
yield k, v
return
except AttributeError:
# `data` is not dict
return
def deep_iter_depth2(data):
"""
Iter key and value in nested dict of depth 2
A simplified deep_iter
Args:
data:
Yields:
Any: Key1
Any: Key2
Any: Value
"""
try:
for k1, v1 in data.items():
if type(v1) is dict:
for k2, v2 in v1.items():
yield k1, k2, v2
except AttributeError:
# `data` is not dict
return
def deep_iter(data, min_depth=None, depth=3):
"""
Iter key and value in nested dict
300us on alas.json depth=3 (530+ rows)
Can only iter dict
Args:
data:
min_depth:
depth:
Yields:
list[str]: Key path
Any: Value
"""
if min_depth is None:
min_depth = depth
assert 1 <= min_depth <= depth
# Equivalent to dict.items()
try:
if depth == 1:
for k, v in data.items():
yield [k], v
return
# Iter first depth
elif min_depth == 1:
q = deque()
for k, v in data.items():
key = [k]
if type(v) is dict:
q.append((key, v))
else:
yield key, v
# Iter target depth only
else:
q = deque()
for k, v in data.items():
key = [k]
if type(v) is dict:
q.append((key, v))
except AttributeError:
# `data` is not dict
return
# Iter depths
current = 2
while current <= depth:
new_q = deque()
# max depth
if current == depth:
for key, data in q:
for k, v in data.items():
yield key + [k], v
# in target depth
elif min_depth <= current < depth:
for key, data in q:
for k, v in data.items():
subkey = key + [k]
if type(v) is dict:
new_q.append((subkey, v))
else:
yield subkey, v
# Haven't reached min depth
else:
for key, data in q:
for k, v in data.items():
subkey = key + [k]
if type(v) is dict:
new_q.append((subkey, v))
q = new_q
current += 1
def deep_values(data, min_depth=None, depth=3):
"""
Iter value in nested dict
300us on alas.json depth=3 (530+ rows)
Can only iter dict
Args:
data:
min_depth:
depth:
Yields:
Any: Value
"""
if min_depth is None:
min_depth = depth
assert 1 <= min_depth <= depth
# Equivalent to dict.items()
try:
if depth == 1:
for v in data.values():
yield v
return
# Iter first depth
elif min_depth == 1:
q = deque()
for v in data.values():
if type(v) is dict:
q.append(v)
else:
yield v
# Iter target depth only
else:
q = deque()
for v in data.values():
if type(v) is dict:
q.append(v)
except AttributeError:
# `data` is not dict
return
# Iter depths
current = 2
while current <= depth:
new_q = deque()
# max depth
if current == depth:
for data in q:
for v in data.values():
yield v
# in target depth
elif min_depth <= current < depth:
for data in q:
for v in data.values():
if type(v) is dict:
new_q.append(v)
else:
yield v
# Haven't reached min depth
else:
for data in q:
for v in data.values():
if type(v) is dict:
new_q.append(v)
q = new_q
current += 1
def deep_iter_diff(before, after):
"""
Iter diff between 2 dict.
Pretty fast to compare 2 deeply nested dict,
time cost increases with the number of differences.
Args:
before:
after:
Yields:
list[str]: Key path
Any: Value in before, or None if not exists
Any: Value in after, or None if not exists
"""
if before == after:
return
if type(before) is not dict or type(after) is not dict:
yield [], before, after
return
queue = deque([([], before, after)])
while True:
new_queue = deque()
for path, d1, d2 in queue:
keys1 = set(d1.keys())
keys2 = set(d2.keys())
for key in keys1.union(keys2):
try:
val2 = d2[key]
except KeyError:
# Safe to access d1[key], because key came from the union of both
# If it's not in d2 then it's in d1
yield path + [key], d1[key], None
continue
try:
val1 = d1[key]
except KeyError:
yield path + [key], None, val2
continue
# Compare dict first, which is pretty fast
if val1 != val2:
if type(val1) is dict and type(val2) is dict:
new_queue.append((path + [key], val1, val2))
else:
yield path + [key], val1, val2
queue = new_queue
if not queue:
break
def deep_iter_patch(before, after):
"""
Iter patch event from before to after, like creating a json-patch
Pretty fast to compare 2 deeply nested dict,
time cost increases with the number of differences.
Args:
before:
after:
Yields:
str: OP_ADD, OP_SET, OP_DEL
list[str]: Key path
Any: Value in after,
or None of event is OP_DEL
"""
if before == after:
return
if type(before) is not dict or type(after) is not dict:
yield OP_SET, [], after
return
queue = deque([([], before, after)])
while True:
new_queue = deque()
for path, d1, d2 in queue:
keys1 = set(d1.keys())
keys2 = set(d2.keys())
for key in keys1.union(keys2):
try:
val2 = d2[key]
except KeyError:
yield OP_DEL, path + [key], None
continue
try:
val1 = d1[key]
except KeyError:
yield OP_ADD, path + [key], val2
continue
# Compare dict first, which is pretty fast
if val1 != val2:
if type(val1) is dict and type(val2) is dict:
new_queue.append((path + [key], val1, val2))
else:
yield OP_SET, path + [key], val2
queue = new_queue
if not queue:
break

View File

@ -181,101 +181,6 @@ def alas_instance():
return out return out
def deep_get(d, keys, default=None):
"""
Get values in dictionary safely.
https://stackoverflow.com/questions/25833613/safe-method-to-get-value-of-nested-dictionary
Args:
d (dict):
keys (str, list): Such as `Scheduler.NextRun.value`
default: Default return if key not found.
Returns:
"""
if isinstance(keys, str):
keys = keys.split('.')
assert type(keys) is list
if d is None:
return default
if not keys:
return d
return deep_get(d.get(keys[0]), keys[1:], default)
def deep_set(d, keys, value):
"""
Set value into dictionary safely, imitating deep_get().
"""
if isinstance(keys, str):
keys = keys.split('.')
assert type(keys) is list
if not keys:
return value
if not isinstance(d, dict):
d = {}
d[keys[0]] = deep_set(d.get(keys[0], {}), keys[1:], value)
return d
def deep_pop(d, keys, default=None):
"""
Pop value from dictionary safely, imitating deep_get().
"""
if isinstance(keys, str):
keys = keys.split('.')
assert type(keys) is list
if not isinstance(d, dict):
return default
if not keys:
return default
elif len(keys) == 1:
return d.pop(keys[0], default)
return deep_pop(d.get(keys[0]), keys[1:], default)
def deep_default(d, keys, value):
"""
Set default value into dictionary safely, imitating deep_get().
Value is set only when the dict doesn't contain such keys.
"""
if isinstance(keys, str):
keys = keys.split('.')
assert type(keys) is list
if not keys:
if d:
return d
else:
return value
if not isinstance(d, dict):
d = {}
d[keys[0]] = deep_default(d.get(keys[0], {}), keys[1:], value)
return d
def deep_iter(data, depth=0, current_depth=1):
"""
Iter a dictionary safely.
Args:
data (dict):
depth (int): Maximum depth to iter
current_depth (int):
Returns:
list: Key path
Any:
"""
if isinstance(data, dict) \
and (depth and current_depth <= depth):
for key, value in data.items():
for child_path, child_value in deep_iter(value, depth=depth, current_depth=current_depth + 1):
yield [key] + child_path, child_value
else:
yield [], data
def parse_value(value, data): def parse_value(value, data):
""" """
Convert a string to float, int, datetime, if possible. Convert a string to float, int, datetime, if possible.

View File

@ -8,7 +8,7 @@ from adbutils import AdbClient, AdbDevice
from module.base.decorator import cached_property from module.base.decorator import cached_property
from module.config.config import AzurLaneConfig from module.config.config import AzurLaneConfig
from module.config.env import IS_ON_PHONE_CLOUD from module.config.env import IS_ON_PHONE_CLOUD
from module.config.utils import deep_iter from module.config.deep import deep_iter
from module.device.method.utils import get_serial_pair from module.device.method.utils import get_serial_pair
from module.exception import RequestHumanTakeover from module.exception import RequestHumanTakeover
from module.logger import logger from module.logger import logger

View File

@ -11,7 +11,7 @@ import numpy as np
from module.base.decorator import cached_property, del_cached_property, has_cached_property from module.base.decorator import cached_property, del_cached_property, has_cached_property
from module.base.timer import Timer from module.base.timer import Timer
from module.base.utils import ensure_time from module.base.utils import ensure_time
from module.config.utils import deep_get from module.config.deep import deep_get
from module.device.env import IS_WINDOWS from module.device.env import IS_WINDOWS
from module.device.method.minitouch import insert_swipe, random_rectangle_point from module.device.method.minitouch import insert_swipe, random_rectangle_point
from module.device.method.pool import JobTimeout, WORKER_POOL from module.device.method.pool import JobTimeout, WORKER_POOL

View File

@ -10,7 +10,7 @@ from requests.adapters import HTTPAdapter
from module.base.utils import save_image from module.base.utils import save_image
from module.config.config import AzurLaneConfig from module.config.config import AzurLaneConfig
from module.config.utils import deep_get from module.config.deep import deep_get
from module.exception import ScriptError from module.exception import ScriptError
from module.logger import logger from module.logger import logger
from module.statistics.utils import pack from module.statistics.utils import pack

View File

@ -37,17 +37,15 @@ from pywebio.output import (
use_scope, use_scope,
) )
from pywebio.pin import pin, pin_on_change from pywebio.pin import pin, pin_on_change
from pywebio.session import (download, go_app, info, local, register_thread, run_js, set_env) from pywebio.session import download, go_app, info, local, register_thread, run_js, set_env
import module.webui.lang as lang import module.webui.lang as lang
from module.config.config import AzurLaneConfig, Function from module.config.config import AzurLaneConfig, Function
from module.config.deep import deep_get, deep_iter, deep_set
from module.config.env import IS_ON_PHONE_CLOUD from module.config.env import IS_ON_PHONE_CLOUD
from module.config.utils import ( from module.config.utils import (
alas_instance, alas_instance,
alas_template, alas_template,
deep_get,
deep_iter,
deep_set,
dict_to_kv, dict_to_kv,
filepath_args, filepath_args,
filepath_config, filepath_config,

View File

@ -1,8 +1,9 @@
from typing import Dict from typing import Dict
from module.config.utils import * from module.config.deep import deep_iter
from module.webui.setting import State from module.config.utils import LANGUAGES, filepath_i18n, read_file
from module.submodule.utils import list_mod_dir from module.submodule.utils import list_mod_dir
from module.webui.setting import State
LANG = "zh-CN" LANG = "zh-CN"
TRANSLATE_MODE = False TRANSLATE_MODE = False

View File

@ -6,8 +6,8 @@ from pywebio.output import put_buttons, put_markdown
from pywebio.session import defer_call, hold, run_js, set_env from pywebio.session import defer_call, hold, run_js, set_env
import module.webui.lang as lang import module.webui.lang as lang
from module.config.utils import (LANGUAGES, deep_get, deep_iter, deep_set, from module.config.deep import deep_get, deep_iter, deep_set
filepath_i18n, read_file, write_file) from module.config.utils import LANGUAGES, filepath_i18n, read_file, write_file
def translate(): def translate():

View File

@ -9,17 +9,15 @@ from queue import Queue
from typing import Callable, Generator, List from typing import Callable, Generator, List
import pywebio import pywebio
from module.config.utils import deep_iter
from module.logger import logger
from module.webui.setting import State
from pywebio.input import PASSWORD, input from pywebio.input import PASSWORD, input
from pywebio.output import PopupSize, popup, put_html, toast from pywebio.output import PopupSize, popup, put_html, toast
from pywebio.session import eval_js from pywebio.session import eval_js, info as session_info, register_thread, run_js
from pywebio.session import info as session_info from rich.console import Console
from pywebio.session import register_thread, run_js
from rich.console import Console, ConsoleOptions
from rich.terminal_theme import TerminalTheme from rich.terminal_theme import TerminalTheme
from module.config.deep import deep_iter
from module.logger import logger
from module.webui.setting import State
RE_DATETIME = ( RE_DATETIME = (
r"\d{4}\-(0\d|1[0-2])\-([0-2]\d|[3][0-1]) " r"\d{4}\-(0\d|1[0-2])\-([0-2]\d|[3][0-1]) "

View File

@ -2,6 +2,7 @@ from cached_property import cached_property
from module.base.timer import timer from module.base.timer import timer
from module.config import config_updater from module.config import config_updater
from module.config.deep import deep_get, deep_set, deep_iter
from module.config.utils import * from module.config.utils import *

View File

@ -2,6 +2,7 @@ from cached_property import cached_property
from module.base.timer import timer from module.base.timer import timer
from module.config import config_updater from module.config import config_updater
from module.config.deep import deep_get, deep_iter, deep_set
from module.config.utils import * from module.config.utils import *

View File

@ -10,7 +10,8 @@ from cached_property import cached_property
from deploy.config import DeployConfig from deploy.config import DeployConfig
from module.base.timer import Timer from module.base.timer import Timer
from module.config.utils import read_file, deep_get, get_server_last_update from module.config.deep import deep_get
from module.config.utils import read_file, get_server_last_update
from module.device.connection_attr import ConnectionAttr from module.device.connection_attr import ConnectionAttr
from module.exception import RequestHumanTakeover from module.exception import RequestHumanTakeover
from module.logger import logger from module.logger import logger

View File

@ -1,7 +1,7 @@
import typing as t import typing as t
from module.base.decorator import cached_property from module.base.decorator import cached_property
from module.config.utils import deep_get from module.config.deep import deep_get
from module.logger import logger from module.logger import logger