任務是構建一個文本操縱器:一個模擬一組文本操作命令的程式。給定一段輸入文本和一串命令,輸出變異的輸入文本和游標位置。
開始簡單:
命令
h: move cursor one character to the left
l: move cursor one character to the right
r<c>: replace character under cursor with <c>
重復命令
# All commands can be repeated N times by prefixing them with a number.
#
# [N]h: move cursor N characters to the left
# [N]l: move cursor N characters to the right
# [N]r<c>: replace N characters, starting from the cursor, with <c> and move the cursor
例子
# We'll use Hello World as our input text for all cases:
#
# Input: hhlhllhlhhll
# Output: Hello World
# _
# 2
#
# Input: rhllllllrw
# Output: hello world
# _
# 6
#
# Input: rh6l9l4hrw
# Output: hello world
# _
# 6
#
# Input: 9lrL7h2rL
# Output: HeLLo WorLd
# _
# 3
#
# Input: 999999999999999999999999999lr0
# Output: Hello Worl0
# _
# 10
#
# Input: 999rsom
# Output: sssssssssss
# _
# 10
我撰寫了以下代碼,但出現錯誤:
class Editor():
def __init__(self, text):
self.text = text
self.pos = 0
def f(self, step):
self.pos = int(step)
def b(self, step):
self.pos -= int(step)
def r(self, char):
s = list(self.text)
s[self.pos] = char
self.text = ''.join(s)
def run(self, command):
command = list(command)
# if command not in ('F','B', 'R'):
#
while command:
operation = command.pop(0).lower()
if operation not in ('f','b','r'):
raise ValueError('command not recognized.')
method = getattr(self, operation)
arg = command.pop(0)
method(arg)
def __str__(self):
return self.text
# Normal run
text = 'abcdefghijklmn'
command = 'F2B1F5Rw'
ed = Editor(text)
ed.run(command)
print(ed)
我在我的代碼中使用了“F”和“B”而不是“h”和“l”,但問題是我缺少一個允許我定義可選“N”的部分。我的代碼只有在操作后定義了一個數字時才有效。如何修復上面的代碼以滿足所有要求?
uj5u.com熱心網友回復:
這個問題的關鍵是弄清楚如何決議命令字串。根據您的描述,命令字串包含一個可選數字,后跟以下三種可能性之一:
hlr, 后跟一個字符
決議這個的正則運算式是(在線嘗試):
(\d*)(h|l|r.)
Explanation:
(\d*) Capture zero or more digits,
(h|l|r.) Capture either an h, or an l, or an r followed by any character
使用re.findall()此正則運算式,您可以獲得匹配串列,其中每個匹配都tuple包含捕獲的組。例如,"rh6l9l4hrw"給出結果
[('', 'rh'), ('6', 'l'), ('9', 'l'), ('4', 'h'), ('', 'rw')]
所以元組的第一個元素是一個字串表示N(如果不存在,則為空字串),元組的第二個元素是命令。如果命令是r,它將在其后包含替換字符。現在我們需要做的就是遍歷這個串列,并應用正確的命令。
我做了一些改變:
self.pos通過具有處理正確邊界檢查的設定器的屬性進行訪問- 在創建物件時將輸入文本分解為串列,因為無法像使用串列那樣就地修改字串。
__str__()將串列連接回字串。 self.text通過只讀屬性訪問,該屬性將self.__text串列連接成字串。
class Editor():
def __init__(self, text):
self.__text = [char for char in text]
self.__pos = 0
@property
def text(self):
return "".join(self.__text)
@property
def pos(self):
return self.__pos
@pos.setter
def pos(self, value):
self.__pos = max(0, min(len(self.text)-1, value))
def l(self, step):
self.pos = self.pos step
def h(self, step):
self.pos = self.pos - step
def r(self, char, count=1):
# If count causes the cursor to overshoot the text,
# modify count
count = min(count, len(self.__text) - self.pos)
self.__text[self.pos:self.pos count] = char * count
self.pos = self.pos count - 1 # Set position to last replaced character
def run(self, command):
commands = re.findall(r"(\d*)(h|l|r.)", command)
for cmd in commands:
self.validate(cmd)
count = int(cmd[0] or "1") # If cmd[0] is blank, use count = 1
if cmd[1] == "h":
self.h(count)
elif cmd[1] == "l":
self.l(count)
elif cmd[1][0] == "r":
self.r(cmd[1][1], count)
def validate(self, cmd):
cmd_s = ''.join(cmd)
if cmd[0] and not cmd[0].isnumeric():
raise ValueError(f"Invalid numeric input {cmd[0]} for command {cmd_s}")
elif cmd[1][0] not in "hlr":
raise ValueError(f"Invalid command {cmd_s}: Must be either h or l or r")
elif cmd[1] == 'r' and len(cmd) == 1:
raise ValueError(f"Invalid command {cmd_s}: r command needs an argument")
def __str__(self):
return self.text
使用給定的輸入運行它:
commands = ["hhlhllhlhhll", "rhllllllrw", "rh6l9l4hrw", "9lrL7h2rL", "999999999999999999999999999lr0", "999rsom"]
for cmd in commands:
e = Editor("Hello World")
e.run(cmd)
uline = " " " " * e.pos "^"
cline = "Cursor: " " " * e.pos str(e.pos)
print(f"Input: {cmd}\nOutput: {str(e)}\n{uline}\n{cline}\n")
Input: hhlhllhlhhll
Output: Hello World
^
Cursor: 2
Input: rhllllllrw
Output: hello world
^
Cursor: 6
Input: rh6l9l4hrw
Output: hello world
^
Cursor: 6
Input: 9lrL7h2rL
Output: HeLLo WorLd
^
Cursor: 3
Input: 999999999999999999999999999lr0
Output: Hello Worl0
^
Cursor: 10
Input: 999rsom
Output: sssssssssss
^
Cursor: 10
現在,如果你想在沒有正則運算式的情況下做同樣的事情,你只需要想辦法將輸入命令字串決議成那種元組串列,你可以使用與以前相同的邏輯來進行實際替換。
在這里,我將通過撰寫一個函式來實作這一點,該函式接受一個字串,并回傳一個遍歷其中所有命令的迭代器。產生的每個元素都是一個元組,看起來像回傳的串列中的一個元素re.findall()。這將允許我們re.findall()用我們的自定義決議器簡單地替換呼叫:
def iter_command(self, command: str):
cmd = [[], []]
# The command is made of two segments:
# 1. The number part
# 2. The letters "h|l|r." part of the regex
seg = 0 # Start with the first segment
for cpos, char in enumerate(command):
if seg == 0:
if "0" <= char <= "9":
# If the character is a number, append it to the first segment
cmd[seg].append(char)
elif char in "hlr":
# Else, if the character is h or l or r, move on to the next segment
seg = 1
if seg == 1:
if not cmd[seg] and char in "hlr":
# If this segment is empty and the character is h|l|r
cmd[seg] = [char]
if char != "r":
# Convert our list of lists of characters to a tuple of strings and yield it
yield tuple(''.join(l) for l in cmd)
# Then reset cmd and seg to process the next command
cmd = [[], []]
seg = 0
else: # char == r
pass # So do one more iteration
elif cmd[seg] and cmd[seg][-1] == "r": # Command is r, so listening for any character
cmd[seg].append(char)
# Same yield tasks as before
yield tuple(''.join(l) for l in cmd)
cmd = [[], []]
seg = 0
else: # This is a character we don't care about
# So do nothing with it
if any(cmd):
yield tuple(''.join(l) for l in cmd)
cmd = [[], []]
seg = 0
現在,讓我們針對之前的正則運算式進行測驗:
commands = ["hhlhllhlhhll", "rhllllllrw", "rh6l9l4hrw", "9lrL7h2rL", "999999999999999999999999999lr0", "999rsom"]
for cmd in commands:
e = Editor("Hello World")
commands_custom = list(e.iter_command(cmd))
commands_regex = re.findall(r"(\d*)(h|l|r.)", cmd)
print(commands_custom)
print(commands_regex)
print(cmd)
print(all(a == b for a, b in zip(commands_custom, commands_regex)))
print("")
[('', 'h'), ('', 'h'), ('', 'l'), ('', 'h'), ('', 'l'), ('', 'l'), ('', 'h'), ('', 'l'), ('', 'h'), ('', 'h'), ('', 'l'), ('', 'l')]
[('', 'h'), ('', 'h'), ('', 'l'), ('', 'h'), ('', 'l'), ('', 'l'), ('', 'h'), ('', 'l'), ('', 'h'), ('', 'h'), ('', 'l'), ('', 'l')]
hhlhllhlhhll
True
[('', 'rh'), ('', 'l'), ('', 'l'), ('', 'l'), ('', 'l'), ('', 'l'), ('', 'l'), ('', 'rw')]
[('', 'rh'), ('', 'l'), ('', 'l'), ('', 'l'), ('', 'l'), ('', 'l'), ('', 'l'), ('', 'rw')]
rhllllllrw
True
[('', 'rh'), ('6', 'l'), ('9', 'l'), ('4', 'h'), ('', 'rw')]
[('', 'rh'), ('6', 'l'), ('9', 'l'), ('4', 'h'), ('', 'rw')]
rh6l9l4hrw
True
[('9', 'l'), ('', 'rL'), ('7', 'h'), ('2', 'rL')]
[('9', 'l'), ('', 'rL'), ('7', 'h'), ('2', 'rL')]
9lrL7h2rL
True
[('999999999999999999999999999', 'l'), ('', 'r0')]
[('999999999999999999999999999', 'l'), ('', 'r0')]
999999999999999999999999999lr0
True
[('999', 'rs')]
[('999', 'rs')]
999rsom
True
而且,由于這些給出相同的結果,我們只需要將呼叫替換為re.findall():
def run(self, command):
- commands = re.findall(r"(\d*)(h|l|r.)", command)
commands = self.iter_command(command)
for cmd in commands:
uj5u.com熱心網友回復:
@paddy 給了你一個很好的建議,但是看看你需要決議的字串,在我看來,正則運算式可以很容易地完成這項作業。對于決議后的部分,命令模式非常適合。畢竟,您有一個必須在初始字串上執行的操作(命令)串列。
在您的情況下,我認為使用這種模式主要帶來 3 個優勢:
每個
Command代表應用于初始字串的操作。這也意味著,例如,如果您想為一系列操作添加快捷方式,則 finalCommand的數量保持不變,您只需調整決議步驟。另一個好處是您可以擁有命令歷史記錄,并且通常設計更加靈活。所有
Command的 s 共享一個公共介面:一個方法execute(),如果需要,一個方法unexecute()用于撤消該execute()方法應用的更改。Commands 將操作執行與決議問題分離。
至于實作,首先定義Commands,它不包含任何業務邏輯,除了對接收者方法的呼叫。
from __future__ import annotations
import functools
import re
import abc
from typing import Iterable
class ICommand(abc.ABC):
@abc.abstractmethod
def __init__(self, target: TextManipulator):
self._target = target
@abc.abstractmethod
def execute(self):
pass
class MoveCursorLeftCommand(ICommand):
def __init__(self, target: TextManipulator, counter):
super().__init__(target)
self._counter = counter
def execute(self):
self._target.move_cursor_left(self._counter)
class MoveCursorRightCommand(ICommand):
def __init__(self, target: TextManipulator, counter):
super().__init__(target)
self._counter = counter
def execute(self):
self._target.move_cursor_right(self._counter)
class ReplaceCommand(ICommand):
def __init__(self, target: TextManipulator, counter, replacement):
super().__init__(target)
self._replacement = replacement
self._counter = counter
def execute(self):
self._target.replace_char(self._counter, self._replacement)
然后你就有了命令的接收者,它TextManipulator包含了改變文本和游標位置的方法。
class TextManipulator:
"""
>>> def apply_commands(s, commands_str):
... return TextManipulator(s).run_commands(CommandParser.parse(commands_str))
>>> apply_commands('Hello World', 'hhlhllhlhhll')
('Hello World', 2)
>>> apply_commands('Hello World', 'rhllllllrw')
('hello world', 6)
>>> apply_commands('Hello World', 'rh6l9l4hrw')
('hello world', 6)
>>> apply_commands('Hello World', '9lrL7h2rL')
('HeLLo WorLd', 3)
>>> apply_commands('Hello World', '999999999999999999999999999lr0')
('Hello Worl0', 10)
>>> apply_commands('Hello World', '999rsom')
Traceback (most recent call last):
ValueError: command 'o' not recognized.
>>> apply_commands('Hello World', '7l5r1')
('Hello W1111', 10)
>>> apply_commands('Hello World', '7l4r1')
('Hello W1111', 10)
>>> apply_commands('Hello World', '7l3r1')
('Hello W111d', 9)
"""
def __init__(self, text):
self._text = text
self._cursor_pos = 0
def replace_char(self, counter, replacement):
assert len(replacement) == 1
assert counter >= 0
self._text = self._text[0:self._cursor_pos] \
replacement * min(counter, len(self._text) - self._cursor_pos) \
self._text[self._cursor_pos counter:]
self.move_cursor_right(counter - 1)
def move_cursor_left(self, counter):
assert counter >= 0
self._cursor_pos = max(0, self._cursor_pos - counter)
def move_cursor_right(self, counter):
assert counter >= 0
self._cursor_pos = min(len(self._text) - 1, self._cursor_pos counter)
def run_commands(self, commands: Iterable[ICommand]):
for cmd in map(lambda partial_cmd: partial_cmd(target=self), commands):
cmd.execute()
return (self._text, self._cursor_pos)
run_commands除了接受部分命令的可迭代的方法之外,沒有什么很難解釋這段代碼。這些部分命令是在沒有接收器物件的情況下啟動的命令,其型別應為TextManipulator. 你為什么要這樣做?這是一種將決議與命令執行分離的可能方法。我決定這樣做,functools.partial但你還有其他有效的選擇。
最終,決議部分:
class CommandParser:
@staticmethod
def parse(commands_str: str):
def invalid_command(match: re.Match):
raise ValueError(f"command '{match.group(2)}' not recognized.")
get_counter_from_match = lambda m: int(m.group(1) or 1)
commands_map = {
'h': lambda match: functools.partial(MoveCursorLeftCommand, \
counter=get_counter_from_match(match)),
'l': lambda match: functools.partial(MoveCursorRightCommand, \
counter=get_counter_from_match(match)),
'r': lambda match: functools.partial(ReplaceCommand, \
counter=get_counter_from_match(match), replacement=match.group(3))
}
parsed_commands_iter = re.finditer(r'(\d*)(h|l|r(\w)|.)', commands_str)
commands = map(lambda match: \
commands_map.get(match.group(2)[0], invalid_command)(match), parsed_commands_iter)
return commands
if __name__ == '__main__':
import doctest
doctest.testmod()
正如我在開始時所說,在您的情況下可以使用正則運算式進行決議,并且命令創建基于每個匹配項的第二個捕獲組的第一個字母。原因是對于 char 替換,第二個捕獲組也包含要替換的 char。使用as 鍵commands_map訪問match.group(2)[0]并回傳 partial Command。如果在 map 中找不到該操作,則會引發ValueError例外。每個引數都是從物件Command中推斷出來的。re.Match
只需將所有這些代碼片段放在一起,您就有了一個可行的解決方案(以及由 執行的檔案字串提供的一些測驗doctest)。
在某些情況下,這可能是一個過于復雜的設計,所以我并不是說這是正確的方法(例如,如果您正在撰寫一個簡單的工具,則可能不是)。您可以避免Commands 部分而只采用決議解決方案,但我發現這是該模式的一個有趣(替代)應用程式。
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/429038.html
標籤:Python python-3.x 算法 哎呀 数据结构
上一篇:回呼轉換并從另一個物件設定
