首页 > 解决方案 > 正则表达式匹配“|” 联合类型的分隔值

问题描述

我正在尝试匹配类型注释,例如int | str,并使用正则表达式替换将它们替换为字符串Union[int, str]

所需的替换(之前和之后):

到目前为止,我想出的正则表达式如下:

r"\w*(?:\[|,|^)[\t ]*((?'type'[a-zA-Z0-9_.\[\]]+)(?:[\t ]*\|[\t ]*(?&type))+)(?:\]|,|$)"

您可以在 Regex Demo 上试用。它并没有真正按照我想要的方式工作。到目前为止我注意到的问题:

附加信息

这主要是为了支持 Python 3.7+ 的PEP 604语法,这需要支持注释前向声明(例如声明为字符串),否则内置类型不支持该|运算符。

这是我想出的示例代码:

from __future__ import annotations

import datetime
from decimal import Decimal
from typing import Optional


class A:
    field_1: str|int|bool
    field_2: int  |  tuple[str|int]  |  bool
    field_3: Decimal|datetime.date|str
    field_4: str|Optional[int]
    field_5: Optional[int|str]
    field_6: dict[str | int, list[B | C | Optional[D]]]

class B: ...
class C: ...
class D: ...

对于 3.10 之前的 Python 版本,我使用__future__导入来避免以下错误:

TypeError: unsupported operand type(s) for |: 'type' and 'type'

这实际上将所有注释转换为字符串,如下所示:

>>> A.__annotations__
{'field_1': 'str | int | bool', 'field_2': 'int | tuple[str | int] | bool', 'field_3': 'Decimal | datetime.date | str', 'field_4': 'str | Optional[int]', 'field_5': 'Optional[int | str]', 'field_6': 'dict[str | int, list[B | C | Optional[D]]]'}

但是在代码中(比如在另一个模块中),我想评估 A 中的注释。这在 Python 3.10 中有效,但在 Python 3.7+ 中失败,即使__future__导入支持前向声明的注释。

>>> from typing import get_type_hints
>>> hints = get_type_hints(A)

Traceback (most recent call last):
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'type' and 'type'

似乎完成这项工作的最佳方法是将所有出现的int | str(例如)替换为Union[int, str],然后typing.Union包含在localns用于评估注释的附加内容中,然后应该可以评估 Python 3.7 的 PEP 604 样式注释+。

标签: pythonpython-3.xregexannotationspython-typing

解决方案


您可以安装 PyPiregex模块(因为re不支持递归)并使用

import regex
text = "str|int|bool\nOptional[int|tuple[str|int]]\ndict[str | int, list[B | C | Optional[D]]]"
rx = r"(\w+\[)(\w+(\[(?:[^][|]++|(?3))*])?(?:\s*\|\s*\w+(\[(?:[^][|]++|(?4))*])?)+)]"
n = 1
res = text
while n != 0:
    res, n = regex.subn(rx, lambda x: "{}Union[{}]]".format(x.group(1), regex.sub(r'\s*\|\s*', ',', x.group(2))), res) 

print( regex.sub(r'\w+(?:\s*\|\s*\w+)+', lambda z: "Union[{}]".format(regex.sub(r'\s*\|\s*', ',', z.group())), res) )

输出:

Union[str,int,bool]
Optional[Union[int,tuple[Union[str,int]]]]
dict[Union[str,int], list[Union[B,C,Optional[D]]]]

请参阅Python 演示

一个正则表达式查找WORD[...]包含管道字符和其他WORDs 或WORD[...]其中没有管道字符的所有类型。

\w+(?:\s*\|\s*\w+)+ 则表达式匹配用管道和可选空格分隔的 2 个或更多单词。

第一个图案细节:

  • (\w+\[)- 第 1 组(这将在替换开始时保持原样):一个或多个单词字符,然后是一个[字符
  • (\w+(\[(?:[^][|]++|(?3))*])?(?:\s*\|\s*\w+(\[(?:[^][|]++|(?4))*])?)+)- 第 2 组(它将被放入Union[...]其中,所有\s*\|\s*模式都替换为,):
    • \w+- 一个或多个单词字符
    • (\[(?:[^][|]++|(?3))*])?- 一个可选的第 3 组,匹配一个[字符,后跟零次或多次出现的一个或多个[字符]或整个第 3 组递归(因此,它匹配嵌套括号),然后是一个]字符
    • (?:\s*\|\s*\w+(\[(?:[^][|]++|(?4))*])?)+- 一次或多次出现(因此匹配包含至少一个要替换为,的管道字符):
      • \s*\|\s*- 包含零个或多个空格的管道字符
      • \w+- 一个或多个单词字符
      • (\[(?:[^][|]++|(?4))*])?- 可选的第 4 组(匹配与第 3 组相同的内容,注意(?4)子程序重复第 4 组模式)
  • ]- 一个]字符。

推荐阅读