Separando dados da representação de string de objetos, com extras adicionados
Dada uma representação de string de dados, desejo extrair as informações em seu objeto correspondente.
Contudo,
Se a string tiver "|" separadores, então, eles devem ser considerados opções e precisam ser escolhidos aleatoriamente.
Se os dados da string têm números mostrados como um intervalo "1-10", um valor aleatório deve ser escolhido entre o intervalo. Deve também preservar o tipo de dados numéricos, ou seja, int ou float
Ie
"(1-3,1,1)" retorna (1, 1, 1), (2, 1, 1) ou (3, 1, 1)
"(0,2-0,4,1,1)" retorna (0,2, 1, 1), (0,3, 1, 1) ou (0,4, 1, 1)
"foo | bar | foobar" retorna "foo", "bar" ou "foobar"
"[1-2,1,2] | foo | bar | [1,8-10,99]" pode retornar:
"foo", "bar", [1, 1, 2], [2, 1, 2], [1, 8, 99], [1, 9, 99] ou [1, 10, 99]
Isso é o que eu tenho e funciona bem. Mas não posso deixar de pensar que isso poderia ser alcançado de uma forma mais concisa. Deixe-me saber o que eu poderia ter feito melhor.
import re
import random
import ast
def randomize_by_pipe(st_value):
"""
Used to split strings with the pipe character and randomly choose and option.
:param: st_value - (str)
"""
if not st_value is None:
st_arr = st_value.split("|")
random.shuffle(st_arr)
return st_arr[0]
else:
return st_value
def randomise_range(text):
if text is None:
return text
else:
matches = re.findall("\d*\.*\d*-{1}\d*\.*\d*",text)
for match in matches:
startingPos = 0
position = text.find(match, startingPos)
while True:
position = text.find(match, startingPos)
if position > -1:
txt = text[position:position+len(match)]
txt = rand_no_from_string(txt)
new_text = text[0:position+len(match)].replace(match,str(txt))
text = new_text + text[position+len(match):]
else:
break
try:
return ast.literal_eval(text)
except ValueError:
return text
def rand_no_from_string(txt):
is_int = False
txt_arr = txt.split("-")
num_arr = [float(x) for x in txt_arr]
if int(num_arr[0]) == num_arr[0]:
mul = 1
is_int = True
else:
#new section to deal with the decimals
mul = 10 ** len(str(num_arr[0]).split(".")[1])
num_arr = [x*mul for x in num_arr]
if num_arr[0] > num_arr[1]:
num_arr[1], num_arr[0] = num_arr[0], num_arr[1]
val = random.randint(num_arr[0],num_arr[1])/mul
return int(val) if is_int else val
Correr com:
text="(108-100,0.25-0.75,100)|Foo|Bar|[123,234,234-250]"
randomise_range(randomize_by_pipe(text))
Respostas
Sugestão de tipo
Em vez de ter helpdocs declarando os tipos de parâmetros de função, por que não ir com dicas de tipo?
Complexidade
Atualmente, seu código tem muitas partes móveis. Você define 2 funções diferentes para analisar os dados, e ambas precisam ser chamadas em cadeia. Isso deve ser feito por uma única função de análise.
Deixe o analisador obter o texto dos dados, então o analisador deve lidar primeiro com a análise sintática pipee, posteriormente, usando os intervalos numéricos.
Seleção de uma lista
Você randomize_by_pipeembaralha a lista e seleciona o 0º valor. Em vez disso, você pode deixar random.choicefazer o trabalho.
range análise
Acho que a análise de alcance pode ser melhorada um pouco. Que tal o seguinte fluxo:
- Remova
[e]do texto fornecido. - Separar de
,. - Para cada seção da divisão, tente analisar como
float(ouint, dependendo do seu conjunto de dados) - Em caso de erro de conversão float,
rand_no_from_stringobtenha um valor.
regex
Você tem uma regex, mas não está fazendo uso completo / elegante dela. Em vez de correspondências, você pode agrupar os resultados e operar nesses grupos. O próprio padrão também pode ser um pouco otimizado :
\d+(?:\.\d+)?-\d+(?:\.\d+)?
Uma reescrita, por exemplo:
from re import sub, Match
from random import choice, randint
def randomise_range(match: Match):
given_range = match.group(0).split("-")
low, high = map(float, given_range)
if low > high:
low, high = high, low
if low.is_integer():
return str(randint(int(low), int(high)))
multiplier = 10 ** len(given_range[0].split(".")[-1])
low = int(low * multiplier)
high = int(high * multiplier)
return str(randint(low, high) / multiplier)
def extract_range(text: str = None):
if not text:
return text
return sub(r"\d+(?:\.\d+)?-\d+(?:\.\d+)?", randomise_range, text)
def parse(text: str = None):
if not text:
return text
selection = choice(text.split("|"))
if selection[0] in ('[', '('):
return extract_range(selection)
return selection
if __name__ == "__main__":
examples = (
"(1-3,1,1)",
"(0.2-0.4,1,1)",
"foo|bar|foobar",
"(108-100,0.25-0.75,100)|Foo|Bar|[123,234,234-250]",
"[1-2,1,2]|foo|bar|[1,8-10,99]",
)
for text in examples:
print(parse(text))
Aqui está uma implementação cujo principal esforço, quando comparado com a sua implementação e também com a resposta aceita, é a separação da análise e da execução. Não está claro se isso é importante para você, mas geralmente é um bom design e é provavelmente mais rápido para reexecutar depois de analisado:
import re
from numbers import Real
from random import randint, choice
from typing import Union, Callable
class Pattern:
chunk_pat = re.compile(
r'([^|]+)' # group: within a chunk, at least one non-pipe character
r'(?:' # non-capturing group for termination character
r'\||$' # pipe, or end of string r')' # end of termination group ) option_pat = re.compile( r'([^,]+)' # at least one non-comma character in an option r'(?:' # non-capturing group for termination character r',|$' # comma, or end of string
r')' # end of termination group
)
range_pat = re.compile(
r'^' # start
r'('
r'[0-9.]+' # first number group
r')-('
r'[0-9.]+' # second number group
r')'
r'$' # end
)
def __init__(self, pattern: str):
chunk_strs = Pattern.chunk_pat.finditer(pattern)
self.tree = tuple(
self.parse_chunk(chunk[1])
for chunk in chunk_strs
)
@staticmethod
def choose_in_group(group: tuple) -> tuple:
for option in group:
if isinstance(option, Callable):
yield option()
else:
yield option
def choose(self) -> Union[str, tuple]:
group = choice(self.tree)
if isinstance(group, tuple):
return tuple(self.choose_in_group(group))
return group
@staticmethod
def precis_parse(as_str: str) -> (Real, int):
if '.' in as_str:
return float(as_str), len(as_str.rsplit('.', 1)[-1])
return int(as_str), 0
@classmethod
def make_choose(cls, start: Real, end: Real, precis: int):
if precis:
factor = 10**precis
start = int(start * factor)
end = int(end * factor)
def choose():
return randint(start, end) / factor
else:
def choose():
return randint(start, end)
return choose
@classmethod
def parse_options(cls, options: str):
for option in cls.option_pat.finditer(options):
range_match = cls.range_pat.match(option[1])
if range_match:
start_str, end_str = range_match.groups()
start, start_n = cls.precis_parse(start_str)
end, end_n = cls.precis_parse(end_str)
yield cls.make_choose(start, end, max(start_n, end_n))
else:
# Fall back to one raw string
yield option[1]
@classmethod
def parse_chunk(cls, chunk: str):
if (
chunk[0] == '(' and chunk[-1] == ')' or
chunk[0] == '[' and chunk[-1] == ']'
):
return tuple(cls.parse_options(chunk[1:-1]))
# Fall back to returning the raw string
return chunk
def test():
p = Pattern('foo|(bar,3-4,50,6.3-7,92-99)')
for _ in range(20):
print(p.choose())
if __name__ == '__main__':
test()