我们已经准备好了,你呢?

2024我们与您携手共赢,为您的企业形象保驾护航!

python math.floor_python math.floor_python math.floor

剧照 | 《三国秘史:深渊潜龙》

2016 年,Linux 操作系统之父 Linus 参加了一档 TED 脱口秀节目[1],节目的前半部分,他主要讲述了自己如何赤膊在家编写 Linux,而没有涉及太多编程相关的东西。

不过在采访的最后,却突然出现了一个有趣的部分,主持人问 Linus:“你曾经说过,你更喜欢和有良好代码品味的人一起工作,那么在你看来,什么才是良好的代码品味呢?”

为了解释这一点,Linus 在大屏幕上展示了一段代码。我将其复制在下面。

remove_list_entry(entry) {    prev = NULL;    walk = head;
// 遍历链表 while (walk != entry) { prev = walk; walk = walk->next; }
// 关键:当要删除时,判断当前位置是否在链表头部进行不同的动作 if (!prev) head = entry->next; else prev->next = entry->next;}

该函数的主要功能是通过遍历链表来删除成员。但是,此代码中存在一个边缘情况[2]。

在编程中,“边缘情况”是指仅在极端情况下才会发生的情况。例如,在上面的代码中,当我们要查找的元素位于链表的头部时,它就是一种边缘情况。为了处理它,该函数在删除它之前会执行 if / else 检查。

Linus 认为这个 if 语句是整段代码“坏味道”的来源,写这个语句的人代码品味不好。那么,一个品味高一点的人该怎么写呢?很快,第二段代码出现在屏幕上。

remove_list_entry(entry) {    indirect = &head
// 遍历链表过程代码已省略
// 当要删除时,直接进行指针操作删除 *indirect = entry->next}

在新代码中,函数充分利用了 C 语言中指针的特性,彻底消除了之前的 if/else,无论删除目标是在链表的头部还是中间,函数都能平等地完成删除操作,之前的边界情况消失了。

看到这里你是不是有些疑惑:没有指针,那你告诉我这么多指针和非指针干嘛?虽然没有指针,但是我觉得这个例子给我们提供了一个非常有趣的话题,那就是在编码时如何充分利用语言特性来更好地处理边缘情况。

我认为好的代码在处理边缘情况时应该简洁且“沉默”。就像上面的例子一样,边缘情况可以融入代码的主流。在编写时,有很多编码技巧和惯例可以帮助我们做到这一点。让我们来看看。

第 1 课:使用分支还是异常?

今天是周末,你打算参加朋友组织的聚餐。临走时,你突然想起现在是雨季。于是你拿出手机,打开天气应用程序,看看今天会不会下雨。如果下雨,你就带上雨伞再出门。

如果把“今天下雨”比喻成编程中的边界情况,那么“查看天气预报+带伞”就是我们的边界处理代码。这种如果下雨就带伞的分支判断,基本是一种直觉的思维本能。因此,当我们在编程中发现边界情况时,我们的第一反应往往是:“搞个if分支来遮一下!”

例如以下代码:

def counter_ap(l):    """计算列表里面每个元素出现的数量"""    result = {}    for key in l:        # 主流程:累加计数器        if key in result:            result[key] += 1        # **边界情况:当元素第一次出现时,先初始化值为 1**        else:            result[key] = 1    return result
# 执行结果:print(counter_ap(['apple', 'banana', 'apple'])){'apple': 2, 'banana': 1}

上面的循环中,代码的主要流程是“将每个键的计数器加 1”。但是,当字典中没有键元素时,不能直接进行累加操作(会抛出)。

>>> result = {}>>> result['foo'] += 1Traceback (most recent call last):  File "", line 1, in KeyError: 'foo'

因此出现了一个边界情况:当一个元素第一次出现时,我们需要初始化该值。

因此,我编写了一个 if 语句来处理这种极端情况。代码很简单,不需要太多解释。但你可能不知道的是,实际上有一个术语可以描述这种编程风格:“(LBYL) Look You Leap”。

“LBYL”这个缩写不太好翻译,说白了就是在执行操作前对可能出现的边界情况进行条件判断,根据判断结果来决定是否处理边界情况或者继续执行主流程。

前面提到过,使用“LBYL”处理边缘情况几乎是直观的。“如果有边缘情况,就增加一个 if 分支”就像“如果天气预报说会下雨,我就带伞出门”,是一种基本不需要思考的操作。

除了LBYL之外,还有一种与之形成鲜明对比的风格:“EAFP(to Ask for than)”。

获得宽恕比获得许可更容易(EAFP)

“EAFP”通常被翻译为“获得原谅比获得许可更容易”。如果我们以雨为例,EAFP 的做法类似于“出门前不要查看天气预报。如果淋了雨,回家后立即洗澡并服用感冒药。”

采用EAFP风格的代码如下:

def counter_af(l):    result = {}    for key in l:        try:            # 总是直接执行主流程:累加计数器            result[key] += 1        except KeyError:            # 边界情况:当元素第一次出现时会报错 KeyError,此时进行初始化            result[key] = 1    return result

EAFP的编程风格相比LBYL更加简单粗暴,总是直奔主流程,在异常处理的try块中消化掉边界条件。

如果你问我,“这两种编程风格哪一种更好?”,我只能说社区明显偏爱“异常捕获-请求-宽恕”(EAFP)编程风格。这有很多原因。

首先,与很多其他编程语言不同,抛出异常是一个非常轻量级的操作,即使程序抛出并捕获了大量的异常,使用EAFP也不会给程序带来额外的负担。

其次,“请求原谅”通常在性能方面更好,因为程序总是直接进入主流程,只有在极少数情况下才需要处理边缘情况。以上面的例子为例,第二个代码通常比第一个代码更快,因为它不必在每次循环时都进行额外的成员检查。

提示:如果你想了解更多有关此内容的信息,建议阅读:写:使用 [3]

因此,每当您想要编写 if else 语句来处理边缘情况时,请考虑使用 try 来捕获异常是否更合适。毕竟,我们总是更喜欢“吃感冒药”而不是“查看天气预报”。

当容器内容不存在时

内置的容器类型有很多,比如字典、列表、集合等。在操作容器时,经常会出现一些边缘情况。其中,“访问的内容不存在”是最常见的一种:

•操作字典时,如果访问的键不存在,则会抛出异常。 •操作列表或元组时,如果访问的索引不存在,则会抛出异常。

对于这样的边缘情况,除了有针对性地捕获相应的异常之外,还有许多其他的方法来处理。

使用重写的示例

在前面的例子中,我们使用try语句来处理“key第一次出现”这种边缘情况。虽然我说使用try的代码比if更好,但这并不意味着它是惯用的代码。

为什么?因为如果你想计算列表的元素数量,你只需使用 . 即可:

from collections import defaultdict

def counter_by_collections(l): result = defaultdict(int) for key in l: result[key] += 1 return result

这样的代码不需要“获得许可”或者“请求原谅”,整个函数只有一个主流程,代码更加清晰自然。

为什么可以让这个边缘情况消失呢?因为之前的代码缺少“key 不存在”的默认处理逻辑。所以,当我们声明如何处理这个边缘情况时,原本需要人工判断的部分就消失了。

提示:对于上面的例子,使用 .[4] 也可以达到同样的目的。

使用值并修改

有时候,我们需要对字典中的某个值进行操作,但该值可能不存在。例如,在下面的例子中:

# 往字典的 values 键追加新值,假如不存在,先以列表初始化try:    d['values'].append(value)except KeyError:    d['values'] = [value]

这种情况下,我们可以使用d.(key, =None)方法来简化边界处理逻辑,直接替换上面的异常捕获语句:

# 如果 setdefault 指定的 key(此处为 "values")不存在,以 [] 初始化,否则返回已存在# 的值。d.setdefault('values', []).append(value)

提示:使用(list)也可以巧妙地解决这个问题。

使用 dict.pop 删除不存在的键

如果我们想从字典中删除一个键,通常使用 del 关键字。但是当该键不存在时,删除操作将引发异常。

因此,如果您想安全地删除一个键,您必须添加一些异常捕获逻辑。

try:    del d[key]except KeyError:    # 忽略 key 不存在的情况    pass

但假设你只想删除一个键,而不关心它是否存在。使用 dict.pop(key, ) 方法就足够了。

只要在调用dict.pop方法时传入一个默认值,那么即使键不存在也不会抛出异常。

# 使用 pop 方法,指定 default 值为 None,当 key 不存在时,不会报错d.pop(key, None)

提示:严格来说,pop方法的主要目的不是删除一个键,而是检索一个键对应的值。不过,我认为偶尔用它来做删除操作是无害的。

当列表切片超出范围时

大家都知道,当你的列表(或元组)只有 3 个元素,而你尝试访问第 4 个元素时,解释器会报错。我们通常将这种错误称为“数组越界”。

>>> l = [1, 2, 3]>>> l[2]3>>> l[3]Traceback (most recent call last):  File "", line 1, in IndexError: list index out of range

但你可能不知道的是,如果你请求的是某个范围的切片而不是单个元素,那么不管你指定的范围是否有效,程序都只会返回一个空列表 [] 而不会抛出任何错误:

>>> l = []>>> l[1000:1001][]

了解了这一点,您会发现像下面这样的边框处理代码是不必要的:

def sum_list(l, limit):    """对列表的前 limit 个元素求和    """    # 如果 limit 过大,设置为数组长度避免越界    if limit > len(l):        limit = len(l)    return sum(l[:limit])

由于切片不会引发任何错误,因此无需检查限制是否超出范围,您可以直接执行求和运算:

def sum_list(l, limit):    return sum(l[:limit])

利用这个特性,我们还可以简化一些特定的边界处理逻辑。例如,安全地删除列表的一个元素:

# 使用异常捕获安全删除列表的第 5 个元素try:    l.pop(5)except IndexError:    pass
# 删除从 5 开始的长度为 1 的切片,不需要捕获任何异常del l[5:6]

有用但危险的“或”运算符

or 是几乎所有编程语言中都存在的运算符。它通常与 and 一起使用来执行布尔逻辑运算。例如:

>>> False or TrueTrue

但是 or 也有一个有趣的特性,即短路求值。例如,在下面的例子中,1 / 0 永远不会被执行(这意味着它不会抛出):

>>> True or (1 / 0)True

在很多场景下,我们可以利用 or 的特性来简化一些边界处理逻辑。看下面的例子:

context = {}# 仅当 extra_context 不为 None 时,将其追加进 context 中if extra_context:    context.update(extra_context)

在这段代码中, 的值通常是一个字典,但有时也可能是 None。所以我添加了一个条件语句,只有当它的值不为 None 时才执行操作。

如果我们使用或运算符,我们可以使上述语句更加简洁:

context.update(extra_context or {})

因为像 a or b or c or ... 这样的表达式会返回这些变量中第一个 true 的布尔值,直到最后一个。所以 None or {} 实际上等于 {},所以当值为 None 时,我们的 or 表达式会把它变成一个空字典。前面的条件判断可以简化成 or 表达式。

用 a 或 b 来表达“当 a 为空时,用 b 替换 a”并不新鲜,在各种编程语言和框架源码中都能找到。但这种写法其实有一个陷阱。

因为或运算计算的是变量的布尔真或假值。因此,不仅是 None,所有 0、[]、{}、set() 以及所有其他会被判断为布尔假的值在或运算中都将被忽略。

# 所有的 0、空列表、空字符串等,都是布尔假值>>> bool(None), bool(0), bool([]), bool({}), bool(''), bool(set())(False, False, False, False, False, False)

如果你忘记了 or 的这个特性,你可能会遇到一些奇怪的问题。例如,以下代码:

timeout = config.timeout or 60

虽然上述代码的目的是确定当 . 为 None 时,使用 60 作为默认值,但是如果主动将 . 的值配置为 0 秒,则会由于上述 0 or 60 = 60 的操作而重新赋值为 60,因此会忽略正确的配置。

因此,有时使用以下方法进行精确的边界处理更为安全:

if config.timeout is None:    timeout = 60

不要手动执行数据验证

无数前辈的经验告诉我们:“不要相信任何用户输入。”这意味着所有存在用户输入的地方都要进行验证。那些无效的、危险的用户输入值就是我们需要处理的边界情况。

假设我正在编写一个小型命令行程序,需要要求用户输入一个介于 0 到 100 之间的数字。如果用户的输入无效,则要求他们重新输入。

该程序大概是这样的:

def input_a_number():    """要求用户输入一个 0-100 的数字,如果无效则重新输入    """    while True:        number = input('Please input a number (0-100): ')
# 此处往下的三条 if 语句都是输入值的边界校验代码 if not number: print('Input can not be empty!') continue if not number.isdigit(): print('Your input is not a valid number!') continue if not (0 <= int(number) <= 100): print('Please input a number between 0 and 100!') continue
number = int(number) break
print(f'Your number is {number}')

执行效果如下:

Please input a number (0-100):Input can not be empty!Please input a number (0-100): fooYour input is not a valid number!Please input a number (0-100): 65Your number is 65

这个函数有 14 行有效代码,其中有 3 个 if 块,共 9 行代码,全部都是边界值检查代码。你可能觉得这样的检查很正常,但想象一下,如果有多个输入需要检查,并且检查逻辑比这更复杂,那这些边界值检查代码就会变得冗长乏味。

如何改进这些代码?将它们提取出来,作为验证功能从核心逻辑中分离出来是一个好主意。但更重要的是,我们应该把“输入数据验证”视为一个独立的职责和领域,并使用更合适的模块来完成这项工作。

在数据验证中,[5]模块是一个不错的选择,如果用于验证,代码可以简化如下:

from pydantic import BaseModel, conint, ValidationError

class NumberInput(BaseModel): # 使用类型注解 conint 定义 number 属性的取值范围 number: conint(ge=0, le=100)

def input_a_number_with_pydantic(): while True: number = input('Please input a number (0-100): ')
# 实例化为 pydantic 模型,捕获校验错误异常 try: number_input = NumberInput(number=number) except ValidationError as e: print(e) continue
number = number_input.number break
print(f'Your number is {number}')

在日常编码中,我们应该尽量避免手动进行数据验证,而是使用(或实现)合适的第三方验证模块,将这部分边界处理工作抽象出来,简化主流程代码。

提示:如果你正在开发一个 Web 应用程序,数据验证部分通常相当简单。例如,框架有自己的表单模块,Flask 也可以使用它进行数据验证。

别忘了做数学题

很多年前,当我刚开始做网页开发的时候,我想学习如何实现一个简单的文本滚动动画。如果你不知道什么是“滚动动画”,我可以简单解释一下。“滚动动画”就是让一段文字从页面的左侧循环滚动到右侧。十几年前,这种动画在网站上特别流行。

我记得有一段逻辑是这样的:控制文本不断向右移动,当横坐标超出页面宽度时,重新设置坐标,继续。我当时写的代码翻译过来是这样的:

while True:    if element.position_x > page_width:        # 边界情况:当对象位置超过页面宽度时,重置位置到最左边        element.position_x -= page_width
# 元素向右边滚动一个单位宽度 element.position_x += width_unit

看起来不错,对吧?我第一次写的时候就这么想。但是后来有一天,我再次看它时发现了一些奇怪的东西。

在上面的代码中,我需要在主循环中确保“不会超出页面宽度”。所以我写了一个 if 语句来处理超出页面宽度的情况。

但是,如果您想确保某个累计数字()不超过另一个数字(),那么直接使用 % 进行模运算不是更好吗?

while True:    # 使用 % page_with 控制不要超过页面宽度    element.position_x = (element.position_x + width_unit) % page_with

这样写的话,代码中的边界情况就会随着 if 语句一起消失。

类似取模的运算还有很多,比如 abs(),math.floor() 等等,切记不要写 if value < 0: value = -value 这样的“边界判断代码”,直接用 abs(value) 就行,不要再去搞什么绝对值运算了。

总结

“边缘情况”是我们日常编码的老朋友了。但是它们并不那么受欢迎。毕竟我们都希望我们的代码始终只有一个主流程,没有太多的条件判断和异常捕获。

但边缘情况是不可避免的,只要有代码,它们就会存在。所以如果我们能更好地处理它们,我们的代码就会变得更清晰、更易读。

除了上面介绍的想法之外,还有许多其他方法可以帮助我们处理边缘情况,比如利用面向对象的多态性、使用空对象模式[6]等等。

最后我们来总结一下:

• 条件判断和异常捕获都可以用来处理边界情况。 • 在本文中,我们更倾向于使用基于异常捕获的 EAFP 风格。 • //pop 可以巧妙地处理键不存在时的边界情况。 • 对不存在范围的列表进行切片不会抛出异常。 • 使用 or 可以简化默认值边界处理逻辑,但要注意不要掉入陷阱。 • 不要手动进行数据验证,使用 or 其他数据验证模块。 • 使用取模、绝对值计算等,可以简化一些特定的边界处理逻辑。

阅读本文后,您还有什么意见吗?请留言或在条目[7]中告诉我。

附录

二维码
扫一扫在手机端查看

本文链接:https://by928.com/2195.html     转载请注明出处和本文链接!请遵守 《网站协议》
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。

项目经理在线

我们已经准备好了,你呢?

2020我们与您携手共赢,为您的企业形象保驾护航!

在线客服
联系方式

热线电话

13761152229

上班时间

周一到周五

公司电话

二维码
微信
线