大量操作 dict 内元素时有什么能省略 dict 名字的语法糖? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
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
xuegy
V2EX    Python

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

  •  
  •   xuegy 2023-09-21 10:11:23 +08:00 3004 次点击
    这是一个创建于 756 天前的主题,其中的信息可能已经有所发展或是发生改变。

    标题可能描述的不够具体。大致操作是从 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-09-21 10:40:35 +08:00   2
    没有办法,python 不支持这种类似 js 的 with 临时作用域附加(js 的 with 大多数时候也被认为是一个不好的东西),因为 python 字典的 key 很可能不是一个合法的变量名称,而且它的类型也不一定是一个字符串。
    不管使用 local()或者 global 都是一个糟糕的实现方式。
    xuegy
        2
    xuegy  
    OP
       2023-09-21 10:47:35 +08:00
    @thinkershare 有个前提是,我的 json 文件可以保证所有的 key 都是合法的变量名称
    hitmanx
        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
    Ricardoo
        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 方式强一丢丢
    thinkershare
        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 这样。
    stein42
        6
    stein42  
       2023-09-21 10:54:01 +08:00
    exec 函数了解一下
    ```
    env = {'a': 1, 'b': 2}
    exec('c = a + b', None, env)
    print(env)
    ```
    FYFX
        7
    FYFX  
       2023-09-21 11:04:19 +08:00
    你字符串转换成运行时的变量名, 不管怎么实现都是一种不安全的操作了
    xuegy
        8
    xuegy  
    OP
       2023-09-21 11:06:36 +08:00
    @thinkershare 这个 easydict 看起来是一个可以接受的折中方案
    wuwukai007
        9
    wuwukai007  
       2023-09-21 11:11:36 +08:00
    BingoXuan
        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)
    Tanix2
        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}
    xuegy
        12
    xuegy  
    OP
       2023-09-21 11:15:21 +08:00
    @Tanix2 是很长很长的数学公式,什么都有,甚至还要用一下 numpy
    Leviathann
        13
    Leviathann  
       2023-09-21 11:16:40 +08:00
    你这个本质上是在造 dsl
    mylifcc
        14
    mylifcc  
       2023-09-21 11:20:00 +08:00
    我想说 如果是我会写成
    dict[A]=dict.get("B", 0) + dict.get("C", 0)
    ......
    awsl2333
        15
    awsl2333  
       2023-09-21 11:26:22 +08:00
    看起来你这个 json 结构是固定的,为什么不考虑 parse 成 pydantic 对象呢,感觉很多人写 python 就不考虑创建 model 类了
    Tanix2
        16
    Tanix2  
       2023-09-21 11:29:08 +08:00
    @xuegy 如果你能找到 A 、B 、C 这样的名称的规律,可以用正则表达式把它们都找出来(只找等号右侧),然后再 eval ,不过这样写是没有代码提示的,也存在安全性问题。
    iOCZ
        17
    iOCZ  
       2023-09-21 11:31:44 +08:00
    没必要
    LandCruiser
        18
    LandCruiser  
       2023-09-21 11:32:55 +08:00
    没有人说第二种可读性太差吗
    xuegy
        19
    xuegy  
    OP
       2023-09-21 11:43:34 +08:00
    @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-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}
    aloxaf
        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))
    hitmanx
        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
    hitmanx
        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)
    ```
    xuegy
        24
    xuegy  
    OP
       2023-09-21 12:12:08 +08:00
    @hitmanx 不现实,我的任务是写一个类似于范文的东西,然后让一群学机械工程的人照着我这个范本写大量类似的东西出来。所以原则上来说,语法越像 MATLAB 那样简单直接越好。
    hitmanx
        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
    MoYi123
        26
    MoYi123  
       2023-09-21 12:29:22 +08:00
    json parse 到 class 里面, 然后加几个 method 去算不就好了? 用 ast 和 eval 真的有点搞了吧.
    Maboroshii
        27
    Maboroshii  
       2023-09-21 13:53:51 +08:00 via Android
    加减乘除 括号,自己写一个解析字符串的计算器吧。
    NoOneNoBody
        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)
    victorc
        29
    victorc  
       2023-09-21 14:30:41 +08:00
    /div>
    “写这么多 dict[]可读性实在太差”--- 这样写反而清楚明了

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

    那直接构造 locals() 用 exec/eval 不就行了。。。
    pursuer
        31
    pursuer  
       2023-09-21 14:40:40 +08:00
    直接修改 locals 真的可以吗? 我记得不行啊,只能改 globals 或者 exec ,我刚刚还特意去试了下 3.8 和 3.10 不行啊,是哪个版本支持了吗?
    xuegy
        32
    xuegy  
    OP
       2023-09-21 15:43:13 +08:00 via iPhone
    @NoOneNoBody 这个和 easydict 的简化效果差不多。不过 easydict 更好一点,可以直接送进 python-docx-replace 替换
    xuegy
        33
    xuegy  
    OP
       2023-09-21 15:48:54 +08:00 via iPhone
    @ipwx 原理上是这样的,主要是这个东西的可维护性和普通 python 代码的可维护性不是一回事。
    我希望它是一种把前后都写好,中间就能像 MATLAB 一样的,这样机械工程的人可以直接往中间填公式。
    ZX576
        34
    ZX576  
       2023-09-21 18:44:28 +08:00
    用 Pydantic 序列化,然后 __init__ 里修改,最后再 dict 导出,既清晰,扩展性又强,还能随便帮你检查 dict 中的值
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     968 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 22:43 PVG 06:43 LAX 15:43 JFK 18:43
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86