Python中的奇特“else”

条件语句中的 Else

我们都写过条件语句,并且可能至少使用过一次完整的 if-elif-else 结构。
例如,在为所需的浏览器创建一个 web 驱动实例时:

browser = get_browser()
if browser == 'chrome':
    driver = Chrome()
elif browser == 'firefox':
    driver = Firefox()
else:
    raise ValueError('不支持的浏览器')

这个代码片段支持在 Chrome 和 Firefox 中进行测试,如果提供了不支持的浏览器,则会引发异常。

一个鲜为人知的事实是,Python 支持在循环和异常处理时使用 else 子句。

循环中的 Else

假设我们有一个单词列表,我们希望打印出所有以大写字母开头的单词。最后,我们想检查是否所有单词都已处理,如果是,则执行特定的逻辑。

我们可以使用一个标志变量 is_all_words_processed,如果遇到无效单词则将其设置为 False,然后在循环外检查该变量以执行逻辑。

seasons = ['冬季', '春季', '夏季', '秋季']
is_all_words_processed = True
for season in seasons:
    if not season.istitle():
        is_all_words_processed = False
        break
    print(season)

if is_all_words_processed:
    print('所有季节都已处理')

Python 允许我们通过将所有单词有效时的逻辑放入 else 子句中,从而避免额外的变量:

seasons = ['Winter', 'Spring', 'Summer', 'Autumn']

for season in seasons:
    if not season.istitle():
        break
    print(season)
else:
    print('所有季节均已处理')

else 块仅在循环自然完成且没有被 break 中断的情况下执行。如果循环被 break 中断,则 else 子句将不会运行。
以下是使用 while 循环重写的相同示例。在 while 循环中,else 子句的行为与之前相同:

seasons = ['Winter', 'Spring', 'Summer', 'Autumn']
index = 0
while index < len(seasons):
    if not seasons[index].istitle():
        break

print(seasons[index])
    index += 1
else:
    print('所有季节都已处理完毕')

异常处理中的 else

else 子句也可以用于异常处理。它必须位于所有 except 块之后。只有在 try 块中没有引发任何异常时,else 块中的代码才会执行。

例如,让我们读取一个包含两列数字的文件并打印它们的商。我们需要处理无效的文件名,而任何其他错误(例如,将值转换为数字或除以零)则应导致程序崩溃(我们将不处理这些错误)。

file_name = 'input.dat'
try:
    f = open(file_name, 'r')
except FileNotFoundError:
    print('文件名不正确')
else:
    for line in f:


a, b = map(int, line.split())
        print(a / b)
    f.close()

在这个例子中,try 块仅包含可能引发捕获异常的代码。

官方文档 建议使用 else 块来避免无意中捕获在 try 块外部代码引发的异常。然而,在异常处理中的 else 使用可能并不直观。

将 Else 与循环和异常处理结合

这是我在面试中提出的一个问题。
假设我们有一个 Driver 类,其中有一个方法 find_element。该 find_element 方法要么返回一个元素,要么引发一个 ElementNotFoundException 异常。在这个例子中,它被实现为随机返回一个元素或以相等的概率引发异常。

使用基本的 Python 语法,实现一个方法 smart_wait(self, locator: str, timeout: float, step: float),该方法每隔 step 秒检查一次给定定位符的元素。如果在 timeout 秒内找到该元素,则返回;否则,引发一个 ElementNotFoundException 异常。

from random import random
class Element:
    pass

class ElementNotFoundException(Exception):
    pass

class Driver:

    def __init__(self):
        pass

    def find_element(self, locator: str):
        print(f"查找元素: {locator}")
        if random() < 0.5:
            return Element()
        else:

raise ElementNotFoundException

def smart_wait(self, locator: str, timeout: float, step: float):
    raise NotImplementedError

这里有一种实现该方法的思路:

    • 在超时时间未到之前,尝试查找元素。

 

  • 如果找到该元素,则退出循环。
  • 如果未找到该元素,则等待step时间间隔。
  • 如果超时,则引发ElementNotFoundException异常。
    以下是一个简单的实现:

 

from time import sleep, monotonic
    def smart_wait(self, locator: str, timeout: float, step: float):
        start_time = monotonic()
        current_time = start_time
        while current_time - start_time < timeout:
            try:

self.find_element(locator)
                break
            except ElementNotFoundException:
                sleep(step)
                current_time = monotonic()
        if current_time - start_time >= timeout:
            raise ElementNotFoundException

我们可以通过使用 return 来简化逻辑,而不是使用 break,但现在先保持原样。

实际上,这个方法是在 Selenium 的 WebDriverWait 类中实现的 – until 方法:

POLL_FREQUENCY: float = 0.5  # 每次调用方法之间的睡眠时间
IGNORED_EXCEPTIONS: Tuple[Type[Exception]] = (NoSuchElementException,)  # 默认被忽略。

class WebDriverWait(Generic[D]):
    def __init__(
        self,
        driver: D,
        timeout: float,
        poll_frequency: float = POLL_FREQUENCY,
        ignored_exceptions: Optional[WaitExcTypes] = None,
    ):

    # ...

def until(self, method: Callable[[D], Union[Literal[False], T]], message: str = "") -> T:
        """调用提供的方法,并将驱动程序作为参数,直到返回值不再评估为``False``。

        :param method: 可调用对象(WebDriver)
        :param message: :exc:`TimeoutException`的可选消息
        :returns: 最后一次调用`method`的结果
        :raises: :exc:`selenium.common.exceptions.TimeoutException` 如果发生超时
        """
        screen = None
        stacktrace = None

end_time = time.monotonic() + self._timeout
while True:
try:
value = method(self._driver)
if value:
return value
except self._ignored_exceptions as exc:
screen = getattr(exc, screen, None)


stacktrace = getattr(exc, "stacktrace", None)
if time.monotonic() > end_time:
    break
time.sleep(self._poll)
raise TimeoutException(message, screen, stacktrace)

在这段代码中,首先通过 `getattr` 函数从异常对象 `exc` 中获取 `stacktrace` 属性,如果该属性不存在,则返回 `None`。接着,代码检查当前的单调时间是否超过了 `end_time`,如果超过,则跳出循环。随后,程序会调用 `time.sleep` 方法,暂停执行一段时间,时间长度由 `self._poll` 决定。最后,如果在规定时间内没有完成操作,则会抛出一个 `TimeoutException`,并传递相关的 `message`、`screen` 和 `stacktrace` 信息。

现在,让我们使用 else 语句重写这个方法,以处理异常和循环:

  1. 异常只可能在行 self.find_element(locator) 中被引发。当异常未被引发时,应退出循环。因此,我们可以将 break 移动到 else 块中。
  2. 如果循环是因为不是 break 而退出的,我们的方法应该引发异常。因此,我们可以将异常引发移动到循环的 else 子句中。
  3. 如果依次执行转换 1 和 2,您会发现当前时间只能在循环条件中获取。

完成这些转换后,我们获得了一个同时使用 else 语句处理异常和循环的方法:

from time import sleep, monotonic

def smart_wait(self, locator: str, timeout: float, step: float):
        start_time = monotonic()
        while monotonic() - start_time < timeout:
            try:
                self.find_element(locator)
            except ElementNotFoundException:
                sleep(step)
            else:
                break
        else:

在这个代码片段中,定义了一个名为 `smart_wait` 的函数。该函数接受三个参数:`locator`(字符串类型),`timeout`(浮点数类型),和 `step`(浮点数类型)。函数的主要逻辑是使用一个循环来检查某个元素是否存在,直到超时为止。首先记录开始时间,然后在循环中尝试找到指定的元素。如果在超时时间内未找到元素,则会休眠一段时间(由 `step` 参数指定)。如果成功找到元素,则跳出循环。


raise ElementNotFoundException


我能说什么呢……这是Python一个鲜为人知的特性。由于使用频率不高,可能在每个场景中使用时不太直观——这可能会导致困惑。然而,了解它并在需要时有效应用无疑是值得的。

更多