标题可能描述的不够具体。大致操作是从 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)
求助各位高人有没有更合理的写法?
1
thinkershare 2023-09-21 10:40:35 +08:00 2
没有办法,python 不支持这种类似 js 的 with 临时作用域附加(js 的 with 大多数时候也被认为是一个不好的东西),因为 python 字典的 key 很可能不是一个合法的变量名称,而且它的类型也不一定是一个字符串。
不管使用 local()或者 global 都是一个糟糕的实现方式。 |
2
xuegy OP @thinkershare 有个前提是,我的 json 文件可以保证所有的 key 都是合法的变量名称
|
3
hitmanx 2023-09-21 10:51:18 +08:00
想到一个更 hack 的方法,把计算放到一个 function 里,定义一个类似 C/C++里面"preprocess"的 decorator 加在函数上。
在这个 decorator 的实现:通过 inspect.getsource(func)去拿到 source 。然后每一行里把 dict 里的存在的 token 替换成 dict[token],最后调用 exec()去执行替换完的字符串。 相当于你自己实现了一个 preprocessor |
4
Ricardoo 2023-09-21 10:52:31 +08:00 1
这种我一般会转成类
my_json = type('MyJson', (object,), my_json) my_json.A = my_json.B + my_json.C 只比 dict 方式强一丢丢 |
5
thinkershare 2023-09-21 10:53:59 +08:00 1
@xuegy 没有办法,因为变量在现代编程语言中都是很特殊的存在。任何尝试动态解构变量的做法都会导致性能的下降,因为编译器会尝试在今日函数前对函数做优化。确定要给整个函数分配的堆栈空间大小,如果搞动态变量注入到局部,那么函数将无法优化,函数的机器码也无法被缓存重用,这些都是实实在在的性能问题。javascript 当初对 with 的支持就是一个错误。python 已经有很多语法都是因为容易写但性能差而被滥用了,你看看现在 python 写的很多程序的性能为什么如此差就明白了,python 现在支持的语法已经非常容易让人写出性能极差,时间复杂度极高的代码了。可读性和性能有时候就是相互矛盾的。
我们目前一般是这么做的 d=EasyDict(json.load(txt), d.a+d.b 这样。 |
6
stein42 2023-09-21 10:54:01 +08:00
exec 函数了解一下
``` env = {'a': 1, 'b': 2} exec('c = a + b', None, env) print(env) ``` |
7
FYFX 2023-09-21 11:04:19 +08:00
你字符串转换成运行时的变量名, 不管怎么实现都是一种不安全的操作了
|
8
xuegy OP @thinkershare 这个 easydict 看起来是一个可以接受的折中方案
|
9
wuwukai007 2023-09-21 11:11:36 +08:00
|
10
BingoXuan 2023-09-21 11:14:05 +08:00
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) |
11
Tanix2 2023-09-21 11:14:10 +08:00
如果计算都是示例那样两元素的加减乘除,那么可以使用如下代码
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} |
13
Leviathann 2023-09-21 11:16:40 +08:00
你这个本质上是在造 dsl
|
14
mylifcc 2023-09-21 11:20:00 +08:00
我想说 如果是我会写成
dict[A]=dict.get("B", 0) + dict.get("C", 0) ...... |
15
liuhai233 2023-09-21 11:26:22 +08:00
看起来你这个 json 结构是固定的,为什么不考虑 parse 成 pydantic 对象呢,感觉很多人写 python 就不考虑创建 model 类了
|
16
Tanix2 2023-09-21 11:29:08 +08:00
@xuegy 如果你能找到 A 、B 、C 这样的名称的规律,可以用正则表达式把它们都找出来(只找等号右侧),然后再 eval ,不过这样写是没有代码提示的,也存在安全性问题。
|
17
iOCZ 2023-09-21 11:31:44 +08:00
没必要
|
18
LandCruiser 2023-09-21 11:32:55 +08:00
没有人说第二种可读性太差吗
|
19
xuegy OP @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 还能看吗? |
20
yesterdaysun 2023-09-21 11:57:39 +08:00
要不用解构的方式导出本地变量计算, 算完再导回 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} |
21
aloxaf 2023-09-21 12:00:44 +08:00
我感觉只能用 locals 了,如果你们 leader 觉得不够 pythonic ,你可以间接使用,就像这样
def calc(): return A + B env = {"A": 1, "B": 2} print(eval(calc.__code__, env)) |
22
hitmanx 2023-09-21 12:07:51 +08:00
>> 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 |
23
hitmanx 2023-09-21 12:11:09 +08:00
这是我让 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) ``` |
24
xuegy OP @hitmanx 不现实,我的任务是写一个类似于范文的东西,然后让一群学机械工程的人照着我这个范本写大量类似的东西出来。所以原则上来说,语法越像 MATLAB 那样简单直接越好。
|
25
hitmanx 2023-09-21 12:16:19 +08:00
@xuegy 你可能没明白这个意思。这个不需要你自己定义 DSL ,还是用 python 的 syntax 。
所以只要你的表达式本来就是 python 的 syntax ,直接一行不改应该就能用 python ast 把它 parse 成 AST 。然后只是遍历的时候把它替换成对应的 dict form 而已。 当然,所以依赖于 exec(string)的方法都会有 security 的问题,需要你的输入是 sanitized |
26
MoYi123 2023-09-21 12:29:22 +08:00
json parse 到 class 里面, 然后加几个 method 去算不就好了? 用 ast 和 eval 真的有点搞了吧.
|
27
Maboroshii 2023-09-21 13:53:51 +08:00 via Android
加减乘除 括号,自己写一个解析字符串的计算器吧。
|
28
NoOneNoBody 2023-09-21 14:14:09 +08:00 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) |
29
victorc 2023-09-21 14:30:41 +08:00
“写这么多 dict[]可读性实在太差”--- 这样写反而清楚明了
写程序要朴素,别玩花了 |
30
ipwx 2023-09-21 14:36:51 +08:00
为什么我觉得楼主这个场景里面,需要执行的代码是可信的(自己写的),只不过变量值是外部读入的?
那直接构造 locals() 用 exec/eval 不就行了。。。 |
31
pursuer 2023-09-21 14:40:40 +08:00
直接修改 locals 真的可以吗? 我记得不行啊,只能改 globals 或者 exec ,我刚刚还特意去试了下 3.8 和 3.10 不行啊,是哪个版本支持了吗?
|
32
xuegy OP @NoOneNoBody 这个和 easydict 的简化效果差不多。不过 easydict 更好一点,可以直接送进 python-docx-replace 替换
|
33
xuegy OP @ipwx 原理上是这样的,主要是这个东西的可维护性和普通 python 代码的可维护性不是一回事。
我希望它是一种把前后都写好,中间就能像 MATLAB 一样的,这样机械工程的人可以直接往中间填公式。 |
34
ZX576 2023-09-21 18:44:28 +08:00
用 Pydantic 序列化,然后 __init__ 里修改,最后再 dict 导出,既清晰,扩展性又强,还能随便帮你检查 dict 中的值
|