编写干净、灵活且易于维护的用户输入提示

Posted

技术标签:

【中文标题】编写干净、灵活且易于维护的用户输入提示【英文标题】:Writing clean, flexible, and easy to maintain user input prompts 【发布时间】:2016-05-22 23:46:31 【问题描述】:

我经常负责询问用户的意见。我一直只是在我的主要执行脚本中“按需”编写提示。这有点难看,因为我经常要求跨多个脚本输入相同类型的输入,所以我的大量代码只是复制/粘贴提示循环。这是我过去所做的:

while True:
    username = input("Enter New Username: ")
    if ldap.search(username):
        print "   [!] Username already taken."
    if not validator.validate_username(username):
        print "   [!] Invalid Username."
    else:
        break

我想创建一些可以这样称呼的东西:

username = prompt(prompt="Enter New Username: ",
                  default=None,
                  rules=["user_does_not_exist",
                         "valid_username"])

那么提示函数是这样的:

def prompt(prompt, default, rules):
    while True:
        retval = input(prompt)
        if default and retval == "":
            break
            return default
        if not rule_match(retval, rules):
            continue
        break
        return retval

def rule_match(value, rules):
    if "user_does_not_exist" in rules:
        if not user.user_exists(value):
            return False

    if "valid_username" in rules:
        if not validator.username(value):
            return False

    if "y_n_or_yes_no" in rules:
        if "ignore_case" in rules:
            if value.lower() not in ["y", "yes", "n", "no"]:
                return False
        else:
            if value not in ["y", "yes", "n", "no"]:
                return False

    return True

我正在考虑的另一种方法是创建一个 Prompt 类,这将使结果具有更大的灵活性。例如,如果我想将“y”或“n”转换为 True 或 False,上述方法实际上并不起作用。

create_another = Prompt(prompt="Create another user? (y/n): ,"
                       default=False,
                       rules=["y_n_or_yes_no",
                              "ignore_case"]).prompt().convert_to_bool()

我正在考虑的另一种选择是制作个性化的提示并命名它们,每个提示都与我的原始代码相似。这实际上并没有改变任何东西。它只是将这些循环从我的主执行代码中取出,从而使主执行代码更易于浏览:

username = prompt("get_new_username")

def prompt(prompt_name):
    if prompt_name == "get_new_username":
        while True:
            username = input("Enter New Username: ")
            if ldap.search(username):
                print "   [!] Username already taken."
            if not validator.validate_username(username):
                print "   [!] Invalid Username."
            else:
                break
                return username

    if prompt_name == "y_n_yes_no_ignore_case":
        # do prompt
    if prompt_name == "y_n_yes_no":
        # do prompt
    if prompt_name == "y_n":
        # do prompt
    if prompt_name == "y_n_ignore_case":
        # do prompt
    if prompt_name == "yes_no":
        # do prompt 
    if prompt_name == "yes_no_ignore_case":
        # do prompt 

我意识到,为我的所有程序确定一种可接受的“y/n”格式可能只是一个好主意,我会的。这只是为了表明,在我需要一个非常相似但略有不同的提示的情况下,它会导致大量的复制/粘贴代码(这种方法根本没有灵活性)。

编写干净、灵活且易于维护的用户提示的好方法是什么?

(我看过这个:Asking the user for input until they give a valid response 和其他一些回复。我的问题不是关于如何获取输入和验证它,而是关于如何制作一个可以跨多个程序重复使用的灵活输入系统)。

【问题讨论】:

我一直发现我的用例需要定制到足以使编写通用解决方案变得困难。不过,我很高兴看到是否有人提出了实用的解决方案,这是个好问题。 设计一个灵活、可重用的提示系统有点超出了 Stack Overflow 的范围。 @chepner 好吧,这是我的问题!它是否必须真正深入“设计”(在这种情况下,我同意,我们不能在 SO 答案中发布整个程序),或者,这是我的问题,是否有一些解决方案或方法这是我没想到的干净简单的东西吗?根据我收集到的信息,答案是这确实是一个需要复杂解决方案的问题。即使在前者的情况下,我认为这个问题仍然具有价值,可以向其他有类似问题的人表明,在一篇文章中回答确实不够简单。 【参考方案1】:

我曾经为类似的东西写过一个函数。解释在文档字符串中:

def xory(question = "", setx = ["yes"], sety = ["no"], setz = [], strict = False):
    """xory([question][, setx][, sety][, setz][, strict]) -> string

    Asks question.  If the answer is equal to one of the elements in setx,
    returns True.  If the answer is equal to one of the elements in sety,
    returns False.  If the answer is equal to one of the elements in setz,
    returns the element in setz that answer is equal to.  If the answer is
    not in any of the sets, reasks the question.  Strict controls whether
    the answer is case-sensitive.  If show is True, an indication of the
    acceptable answers will be displayed next to the prompt."""

    if isinstance(setx, str):
        setx = [setx]
    if isinstance(sety, str):
        sety = [sety]
    if isinstance(setz, str):
        setz = [setz]
    if (setx[0])[0] != (sety[0])[0]:
        setx = [(setx[0])[0]] + setx
        sety = [(sety[0])[0]] + sety
    question = question.strip(" ") + " "
    while True:
        if show:
            shows = "[%s/%s] " % (setx[0], sety[0])
        else:
            shows = ""
        user_input = raw_input(question + shows)
        for y in [setx, sety, setz]:
            for x in y:
                if (user_input == x) or ((not strict) and (user_input.lower() == x.lower())):
                    if y is setx:
                        return True
                    elif y is sety:
                        return False
                    else:
                        return x
        question = ""
        show = True

例子:

>>> response = xory("1 or 0?", ["1", "one", "uno"], ["0", "zero", "null"], ["quit", "exit"])
1 or 0? x
[1/0] eante
[1/0] uno
>>> print(response)
True
>>> response = xory("Is that so?")
Is that so? Who knows?
[y/n] no
>>> print(response)
False
>>> response = xory("Will you do it?", setz=["quit", "exit", "restart"])
Will you do it? hm
[y/n] quit
>>> print(response)
quit

【讨论】:

【参考方案2】:

我建议编写一个库,其中包含许多定义非常明确的构建块,每个构建块都尽可能小且重量轻,不要对如何将各个部分组合在一起进行太多假设.

也就是说,我将包含一个执行循环的函数,但不是传入一组规则,而是允许传入一个函数,该函数要么返回一个值(如果有效输入是给定并在以任何必要的方式对其进行转换之后),或者如果输入不可用,则引发ValueError。其他构建块将实现某些检查或转换(例如将 'y''n' 解析为布尔值)。

这样,您将完全由用户以适合用例的方式组装这些东西。

# library:

def prompt(prompt, default, postprocess):
    value = input(' (): '.format(prompt, default)) or default
    try:
        return postprocess(value)
    except ValueError:
        continue

def check_lower(value):
    if not value.islower():
        raise ValueError()

def to_bool(value):
    return value in 'yes'

# using the library:

def postprocess(value):
    check_lower(value)
    return to_bool(value)

prompt('Really?', 'n', postprocess)

【讨论】:

【参考方案3】:

我会这样创建一个提示函数:

def prompt(prompt, default=None, rules=[]):
    while True:
        response = input(prompt)
        if response:
            valid = [rule(response) for rule in rules]
            if not(False in valid):
                return response
            else:
                print('Invalid input')
        else:
            return default

然后您可以创建不同的验证函数,例如

def filterValidEmail(string):
    if '@' in string:
        if '.' in string.split('@')[1]:
            return True
        else:
            return False
    else:
        return False

然后像这样调用这些函数:

prompt('What is your email? ', rules=[filterValidEmail])

您也可以对此进行调整,以便告诉用户他们失败的验证或不允许空白输入。

【讨论】:

以上是关于编写干净、灵活且易于维护的用户输入提示的主要内容,如果未能解决你的问题,请参考以下文章

第 7 章 用户输入和while 循环

如何以干净且可维护的方式编写非常复杂的 SQL? [关闭]

用C语言编写程序,如何输入两个整数,并求和。

Apache Shiro 1.3.2简介

07_编写脚本:提示用户输入用户名和密码,脚本自动创建相应的账户及配置密码。如果用户不输入账户名,则提示必须输入账户名并退出脚本;如果用户不输入密码,则统一使用默认的 123456 作为默认密码。

编写干净代码的 9 条必须知道的规则