xuegy
V2EX  ›  Python

大量操作 dict 内元素时有什么能省略 dict 名字的语法糖?

By xuegy at 2023 年 9 月 21 日 · 3910 次点击

标题可能描述的不够具体。大致操作是从 json 读入数据,经过一通计算,用 python-docx-replace 替换模版快速生成文档。代码举例(省略数字格式化字符串部分):

dict = json.load(xxx)
dict[A]=dict[B]+dict[C]
dict[D]=dict[E]-dict[F]
dict[G]=dict[H]*dict[I]
dict[J]=dict[K]/dict[L]
...
docx_replace(doc, **dict)

因为实际代码是很长的数学公式,个人觉得写这么多dict[]可读性实在太差,于是采用了如下写法(我知道不妥,但是想不出更好的),被 LD 批评很不 Pythonic 。

dict = json.load(xxx)
locals().update(dict)
A=B+C
D=E-F
G=H*I
J=K/L
...
dict.update({key: value for (key, value) in locals().items() if type(value) == int or type(value) == float})
docx_replace(doc, **dict)

求助各位高人有没有更合理的写法?

34 条回复  •  2023-09-21 18:44:28 +08:00
thinkershare
   1
thinkershare  
   2023 年 9 月 21 日   ❤️ 2
没有办法,python 不支持这种类似 js 的 with 临时作用域附加(js 的 with 大多数时候也被认为是一个不好的东西),因为 python 字典的 key 很可能不是一个合法的变量名称,而且它的类型也不一定是一个字符串。
不管使用 local()或者 global 都是一个糟糕的实现方式。
xuegy
   2
xuegy  
OP
   2023 年 9 月 21 日
@thinkershare 有个前提是,我的 json 文件可以保证所有的 key 都是合法的变量名称
hitmanx
   3
hitmanx  
   2023 年 9 月 21 日
想到一个更 hack 的方法,把计算放到一个 function 里,定义一个类似 C/C++里面"preprocess"的 decorator 加在函数上。

在这个 decorator 的实现:通过 inspect.getsource(func)去拿到 source 。然后每一行里把 dict 里的存在的 token 替换成 dict[token],最后调用 exec()去执行替换完的字符串。

相当于你自己实现了一个 preprocessor
Ricardoo
   4
Ricardoo  
   2023 年 9 月 21 日   ❤️ 1
这种我一般会转成类
my_json = type('MyJson', (object,), my_json)
my_json.A = my_json.B + my_json.C

只比 dict 方式强一丢丢
thinkershare
   5
thinkershare  
   2023 年 9 月 21 日   ❤️ 1
@xuegy 没有办法,因为变量在现代编程语言中都是很特殊的存在。任何尝试动态解构变量的做法都会导致性能的下降,因为编译器会尝试在今日函数前对函数做优化。确定要给整个函数分配的堆栈空间大小,如果搞动态变量注入到局部,那么函数将无法优化,函数的机器码也无法被缓存重用,这些都是实实在在的性能问题。javascript 当初对 with 的支持就是一个错误。python 已经有很多语法都是因为容易写但性能差而被滥用了,你看看现在 python 写的很多程序的性能为什么如此差就明白了,python 现在支持的语法已经非常容易让人写出性能极差,时间复杂度极高的代码了。可读性和性能有时候就是相互矛盾的。
我们目前一般是这么做的 d=EasyDict(json.load(txt), d.a+d.b 这样。
stein42
   6
stein42  
   2023 年 9 月 21 日
exec 函数了解一下
```
env = {'a': 1, 'b': 2}
exec('c = a + b', None, env)
print(env)
```
FYFX
   7
FYFX  
   2023 年 9 月 21 日
你字符串转换成运行时的变量名, 不管怎么实现都是一种不安全的操作了
xuegy
   8
xuegy  
OP
   2023 年 9 月 21 日
@thinkershare 这个 easydict 看起来是一个可以接受的折中方案
wuwukai007
   9
wuwukai007  
   2023 年 9 月 21 日
BingoXuan
   10
BingoXuan  
   2023 年 9 月 21 日
def some_cal(a=None,b=None,c=None):
# return anything serializable, like namedtuple

data = json.loads(json_str)
try:
res=some_cal(**data)
except:
...# handle exception
else:
json.dumps(res)
Tanix2
   11
Tanix2  
   2023 年 9 月 21 日
如果计算都是示例那样两元素的加减乘除,那么可以使用如下代码

import re

d = {
'B': 2,
'C': 3,
}


def dict_calc(d: dict, text: str):
for line in text.splitlines(keepends=False):
sp = re.split(r'([=+\-*/])', line)
if len(sp) == 5:
sp = map(str.strip, sp)
a, eq, b, op, c = sp
if eq == '=' and op in '+-*/':
d[a] = eval(f'{d[b]}{op}{d[c]}')


dict_calc(d, '''
A = B + C
D = A - C
E = A * D
F = E / D
''')

print(d)
# Output:
# {'B': 2, 'C': 3, 'A': 5, 'D': 2, 'E': 10, 'F': 5.0}
xuegy
   12
xuegy  
OP
   2023 年 9 月 21 日
@Tanix2 是很长很长的数学公式,什么都有,甚至还要用一下 numpy
Leviathann
   13
Leviathann  
   2023 年 9 月 21 日
你这个本质上是在造 dsl
mylifcc
   14
mylifcc  
   2023 年 9 月 21 日
我想说 如果是我会写成
dict[A]=dict.get("B", 0) + dict.get("C", 0)
......
awsl2333
   15
awsl2333  
   2023 年 9 月 21 日
看起来你这个 json 结构是固定的,为什么不考虑 parse 成 pydantic 对象呢,感觉很多人写 python 就不考虑创建 model 类了
Tanix2
   16
Tanix2  
   2023 年 9 月 21 日
@xuegy 如果你能找到 A 、B 、C 这样的名称的规律,可以用正则表达式把它们都找出来(只找等号右侧),然后再 eval ,不过这样写是没有代码提示的,也存在安全性问题。
iOCZ
   17
iOCZ  
   2023 年 9 月 21 日
没必要
LandCruiser
   18
LandCruiser  
   2023 年 9 月 21 日
没有人说第二种可读性太差吗
xuegy
   19
xuegy  
OP
   2023 年 9 月 21 日
@LandCruiser 你告诉我,像这种东西:
PL = PA * C3 * FR * k/(k-1.0) * N * C4 * (np.power(P0 / float(PA), (k-1.0) / (k*N)) - 1.0) / (EA * EM)
全写成 dict 还能看吗?
yesterdaysun
   20
yesterdaysun  
   2023 年 9 月 21 日
要不用解构的方式导出本地变量计算, 算完再导回 dict

A, B, C, D, E, F, G, H, I, J, K, L = dict.values()

A = B + C
D = E - F
G = H * I
J = K / L

dict = {"A": A, "B": B, "C": C, "D": D, "E": E, "F": F, "G": G, "H": H, "I": I, "J": J, "K": K, "L": L}
aloxaf
   21
aloxaf  
   2023 年 9 月 21 日
我感觉只能用 locals 了,如果你们 leader 觉得不够 pythonic ,你可以间接使用,就像这样

def calc():
return A + B

env = {"A": 1, "B": 2}
print(eval(calc.__code__, env))
hitmanx
   22
hitmanx  
   2023 年 9 月 21 日
>> PL = PA * C3 * FR * k/(k-1.0) * N * C4 * (np.power(P0 / float(PA), (k-1.0) / (k*N)) - 1.0) / (EA * EM)

@xuegy 可能可以把表达式通过 Python AST( https://docs.python.org/3/library/ast.html)转成抽象语法树(AST),然后在 iterate 这个 AST 的时候把 node 替换成对应的 dict value
hitmanx
   23
hitmanx  
   2023 年 9 月 21 日
这是我让 chatgpt4 根据这个 idea 写的代码:
```
import ast
import json

data = json.load(xxx) # some sample data source


class TransformVarToDict(ast.NodeTransformer):
def visit_Name(self, node):
# Replace variable reference with dictionary access
if isinstance(node.ctx, (ast.Load, ast.Store)):
return ast.Subscript(
value=ast.Name(id='data', ctx=ast.Load()),
slice=ast.Index(value=ast.Str(s=node.id)),
ctx=node.ctx
)
return node


def process_formula(formula_str):
# Parse the formula to an AST
parsed = ast.parse(formula_str)

# Transform the AST
transformed = TransformVarToDict().visit(parsed)
ast.fix_missing_locations(transformed) # Fix line numbers

# Compile and execute the modified AST
code = compile(transformed, '<string>', 'exec')
exec(code, globals())


# Sample formulas
formulas = [
"A = B + C",
"D = E - F",
"G = H * I",
"J = K / L"
]


for formula in formulas:
process_formula(formula)


print(data)
```
xuegy
   24
xuegy  
OP
   2023 年 9 月 21 日
@hitmanx 不现实,我的任务是写一个类似于范文的东西,然后让一群学机械工程的人照着我这个范本写大量类似的东西出来。所以原则上来说,语法越像 MATLAB 那样简单直接越好。
hitmanx
   25
hitmanx  
   2023 年 9 月 21 日
@xuegy 你可能没明白这个意思。这个不需要你自己定义 DSL ,还是用 python 的 syntax 。

所以只要你的表达式本来就是 python 的 syntax ,直接一行不改应该就能用 python ast 把它 parse 成 AST 。然后只是遍历的时候把它替换成对应的 dict form 而已。

当然,所以依赖于 exec(string)的方法都会有 security 的问题,需要你的输入是 sanitized
MoYi123
   26
MoYi123  
   2023 年 9 月 21 日
json parse 到 class 里面, 然后加几个 method 去算不就好了? 用 ast 和 eval 真的有点搞了吧.
Maboroshii
   27
Maboroshii  
   2023 年 9 月 21 日 via Android
加减乘除 括号,自己写一个解析字符串的计算器吧。
NoOneNoBody
   28
NoOneNoBody  
   2023 年 9 月 21 日   ❤️ 1
@LandCruiser
@MoYi123
起初我也不明白,虽然繁琐,但无法避免逐条算式写,那先写简单的,然后在编辑器用正则替换就可以了,直到#24

对于不懂 python 的人来说,反而第二种可读性是高的
看 OP #24 所说,看样子恰好就是这样,要给不懂 python 的人写这堆算式,跟 dict 什么的无关,就是指代某个对象某个 item

最简便就是 SimpleNamaspace
In [18]: from types import SimpleNamespace

In [19]: d=SimpleNamespace(**{'A':1,'B':2,'C':3})

In [20]: d.A=d.B+d.C

In [21]: d
Out[21]: namespace(A=5, B=2, C=3)
victorc
   29
victorc  
   2023 年 9 月 21 日
“写这么多 dict[]可读性实在太差”--- 这样写反而清楚明了

写程序要朴素,别玩花了
ipwx
   30
ipwx  
   2023 年 9 月 21 日
为什么我觉得楼主这个场景里面,需要执行的代码是可信的(自己写的),只不过变量值是外部读入的?

那直接构造 locals() 用 exec/eval 不就行了。。。
pursuer
   31
pursuer  
   2023 年 9 月 21 日
直接修改 locals 真的可以吗? 我记得不行啊,只能改 globals 或者 exec ,我刚刚还特意去试了下 3.8 和 3.10 不行啊,是哪个版本支持了吗?
xuegy
   32
xuegy  
OP
   2023 年 9 月 21 日 via iPhone
@NoOneNoBody 这个和 easydict 的简化效果差不多。不过 easydict 更好一点,可以直接送进 python-docx-replace 替换
xuegy
   33
xuegy  
OP
   2023 年 9 月 21 日 via iPhone
@ipwx 原理上是这样的,主要是这个东西的可维护性和普通 python 代码的可维护性不是一回事。
我希望它是一种把前后都写好,中间就能像 MATLAB 一样的,这样机械工程的人可以直接往中间填公式。
ZX576
   34
ZX576  
   2023 年 9 月 21 日
用 Pydantic 序列化,然后 __init__ 里修改,最后再 dict 导出,既清晰,扩展性又强,还能随便帮你检查 dict 中的值
推荐学习书目
› Learn Python the Hard Way
Python Sites
› PyPI - Python Package Index
› http://diveintopython.org/toc/index.html
› Pocoo
值得关注的项目
› PyPy
› Celery
› Jinja2
› Read the Docs
› gevent
› pyenv
› virtualenv
› Stackless Python
› Beautiful Soup
› 结巴中文分词
› Green Unicorn
› Sentry
› Shovel
› Pyflakes
› pytest
Python 编程
› pep8 Checker
Styles
› PEP 8
› Google Python Style Guide
› Code Style from The Hitchhiker's Guide
© 2026 V2EX · 167ms · 3.9.8.5