异常与错误处理高级用法

举报
brucexiaogui 发表于 2022/09/25 00:11:41 2022/09/25
【摘要】 异常与错误处理高级用法 1.概述 这篇文章介绍如何优雅的使用异常处理好程序的错误、用更少的代码、清晰的代码,写出更健壮的程序。 2.异常高级用法 2.1.两种编程风格处理错误 使用Python处...

异常与错误处理高级用法

1.概述

这篇文章介绍如何优雅的使用异常处理好程序的错误、用更少的代码、清晰的代码,写出更健壮的程序。

2.异常高级用法

2.1.两种编程风格处理错误

使用Python处理错误有两种风格,下面介绍下这两种风格。

  • LBYL(look before you leap):LBYL常备翻译成“三思而后行”,通俗讲就是在执行一个可能会出错的操作时,先做一些关键条件判断,仅当条件满足时才进行操作。
    LBYL是一种本能的思考结果,他的逻辑就像“如果天气预报说今天会下雨,那么我就不出门”
  • EAFP(easier to ask for forgiveness than permission),可直译为“获取原谅比许可简单”。 是一种和三思而后行截然不同的编程风格,它不做任何事前检查,直接执行操作,但在外层用try来捕获异常。
    这种做法类似于“出门前不看天气预报,如果下雨了就回家吃感冒药”

在python社区更偏向于使用EAFP编程风格,它的代码通常更精简。因为它不需要开发者用分支覆盖各种可能出错的情况,只需要捕获可能发生的异常情况即可。
EAFP性能更好,它直奔主要代码,省去了各种条件判断。

1.三思而后行编程风格

def incr_by_one(value):
    '''
    输入的参数加1返回新值
    :param value: 
    :return: 
    '''
    if isinstance(value, int):
        return value + 1
    elif isinstance(value, str) and value.isdigit():
        return int(value) + 1
    else:
        print(f'Unable to perform incr for value: {value}')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

1.仅当类型是int类型时才执行加法操作
2.判断仅当类型时str,同时满足.isdigit() 方法时才进行操作。
这几行代码看似简单,其实代表了LBYL编程风格。

2.获取原谅比许可简单编程风格

def incr_by_try(value):
    '''
    输入的参数加1返回新值
    :param value:
    :return:
    '''
    try:
        return int(value) + 1
    except (KeyError, ValueError):
        print(f'Unable to perform incr for value: {value}')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2.2.把更精确的except语句放在前面

python内置异常类之间存在许多继承关系,举个简单的依赖关系。
BaseException —> Exception—> LookupError—> KeyError

如果一个try代码块里包含多条except,异常匹配会按照从上而下的顺序执行。假如把一个父类异常放在前面,就会导致子类异常永远不会执行。

这个示例中KeyError异常永远不会被执行,要修复这个问题就要调整except的顺序。

def incr_by_key(d, key):
    try:
        d[key] + 1
    except Exception as e:
        print(f'Unknown error: {e}')
    except KeyError:
        print(f'key {key} does not exists')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.3.try 搭配 else分支增强代码能力

用try捕获异常时,有时程序需要在一切操作没有异常后执行某个操作。为了做到这一点我们需要创建一个变量来做标记实现这个功能,如下示例。

只有当sync_profile()执行成功时,才继续调用send_notification()发送消息通知。为此我们定义一个额外变量syn_succeeded来作为标记。

def send_mess():
    syn_succeeded = False
    try:
        sync_profile(user.profile)
        syn_succeeded = True
    except Exception as e:
        print(f'Unknown error: {e}')

    if syn_succeeded:
        send_notification(user, 'profile sync succeeded')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果使用try搭配else分支,代码可以变得更简单。
异常捕获语句里的else表示:仅当try语句块里没有抛出任何异常时,才执行else分支下的内容,效果就像在try最后增加一个标记变量一样。

def sen_mess_try_else():
    try:
        sync_profile(user.profile)
        syn_succeeded = True
    except Exception as e:
        print(f'Unknown error: {e}')
    else:
        send_notification(user, 'profile sync succeeded')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.4.使用空raise语句

在处理异常时,有时我们可能仅仅想记录下某个异常,然后把它重新抛出,交由上层处理。这是使用不带任何参数的raise语句可以派上用场。

当一个空raise出现在except时,他会原封不动的重新抛出当前异常,因此print语句不会执行。

def incr_by_raise(value):
    try:

        return value + 1
    except TypeError:
        print(f'key {value} does not exists')
        raise

incr_by_raise('dd')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.5.抛出异常而不是返回错误

python函数支持一次返回多个值,当我们表名函数执行出错时,可以让它同时返回结果与错误信息。
下面是create_item()函数就利用了这个特性,在这段代码里,create_item()函数的功能是创建新的Item对象。当调用create_item()函数,如果执行失败函数会把错误信息放到第二个结果中返回。而当函数执行成功时,为了保持返回值统一,函数同样返回错误原因,只是内容为空字符串。

这种做法看上去很自然,但在python世界里,返回错误并非解决此类问题的最佳办法。这是因为这种做法会增加调用方处理错误的成本

MAX_LENGTH_OF_NAME = 10
MAX_ITEMS_QUOTA = 5
def cerate_item(name):
    '''
    接收名称,创建item对象
    :param name: 
    :return: 返回 结果,错误信息。如果执行成功返回错误信息为空
    '''
    if len(name) > MAX_LENGTH_OF_NAME:
        return None, 'name of item is too long'
    if len(get_current_items()) > MAX_ITEMS_QUOTA:
        return None, 'item is full'
    # 当执行成功后返回结果和空错误信息
    return Item(name), ''

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

python有完善的异常处理机制,在某种程度上鼓励我们使用异常,所以用异常来处理错误才是更地道的做法

通过引入自定义异常类,上面的代码可以改成下面的样子

MAX_LENGTH_OF_NAME = 10
MAX_ITEMS_QUOTA = 5
Item = []

def get_current_items():
    return len(Item)

# 创建自定义异常类
class CreateItemError(Exception):
    pass


def create_item_except(name):
    if len(name) > MAX_LENGTH_OF_NAME:
    	# 向上抛出异常信息
        raise CreateItemError('name of item is too long')
    if len(get_current_items()) > MAX_ITEMS_QUOTA:
        raise CreateItemError('item is full')
    return Item(name), ''


def create_from_input():
    name = input()
    try:
        item = create_item_except(name)
    except CreateItemError as e:
        print(f'create item failed: {e}')
    else:
        print(f'item<{name}> created')

create_from_input()

#运行结果
create item failed: name of item is too long

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

用抛出异常代替返回错误后,整个代码结构看上去变化不大但细节上改变非常多。

  • 新函数拥有更稳定的返回值类型,他永远只会返回Item类型或是抛出异常。
  • 不同于返回值,异常在被捕获前会不断往调用栈上层汇报。因此create_item()的直接调用方可以完全不用处理CreateItemError,而交由更上层处理。异常的这个特点给了我们更多灵活性,但同时也带来了风险。假如程序缺少一个顶级的统一异常处理逻辑,那么某个被所有人忽略的异常可能会层层上报,最终弄垮整个程序。
  • 虽然我们鼓励使用异常,但异常总是不可避免的然人感到“惊讶”所以最好在函数文档里说明可能抛出的异常类型。

2.6.使用上下文管理器

1.什么是上下文管理器

异常处理时,第一个想到的就是try关键字,其实除了try以外还有一个关键字和异常处理非常密切,他能简化异常处理工作,这个关键字就是with。
with是一个神奇的关键字,他可以在代码中开辟一段有他管理的上下文,并控制程序在进入和退出时的行为
并非所有的对象都能和with配合使用,只有满足上下文管理器协议的对象才行。
上下文管理器:是一种定义了“进入”和“退出”动作的特殊对象,要创建一个上下文管理器,只要实现__enter__ 和 __exit__两个魔法方法即可。

创建一个简单的上下文管理器示例:

class DummyContext:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        # __enter__会在进入管理器时自动被调用,同时返回结果
        return f'{self.name} - {random.random()}'

    def __exit__(self, exc_type, exc_val, exc_tb):
        # __exit__会在退出管理器时被调用
        print('Exiting DummyContext')
        return False

with DummyContext('foo') as name:
    print(f'name: {name}')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2.上下文管理器作用

上下文管理器作用很多,在这里可以用来简化异常处理工作。

3.替代finally语句清理资源

在编写try语句时,finally关键字经常用来做一些清理工作,比如关闭资源对象。
比如关闭网络连接对象,下面是经典的写法。

conn = create_conn(host, port, timeout=None)
try:
	conn.send_text('Hello, world')
except Exception as e:
	print(f'Unable to use connection:{e}')
finally:
	conn.close()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

经典写法有些繁琐,使用上下文管理器可以变得简单。

# 创建上下文管理器
class create_conn_obj:
    def __init__(self, host, port, timeout=None):
        self.conn = create_conn(host, port, timeout=timeout)

    def __enter__(self):
        return self.conn
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.conn.close()
        return False

# 使用上下文管理器
with create_conn_obj(host, port, timeout=None) as conn:
    try:
        conn.send_text('Hellod world')
    except Exception as e:
        print(f'Unable to use connection:{e}')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

4.忽略异常

在执行操作时,有些程序会抛出一些不影响正常执行逻辑的异常。举个例子,当你关闭某个链接时,如果已经关闭了,解释器就会抛出Already-CloseError异常。为了程序正常运行下去,必须用try语句来捕获并忽略这个异常。

try:
	conn.close()
except AlreadClosedError:
	pass

  
 
  • 1
  • 2
  • 3
  • 4

虽然这样的代码很简单,当项目中有很多地方都要忽略这类异常时,这些语句就会分散在各个角落,看上去非常凌乱。
如果使用上下文管理器,可以非常方便的实现可复用的忽略异常功能。

class ignore_closed:
    '''
    忽略已经关闭的连接
    '''
    
    def __enter__(self):
        pass
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type == AlreadyClosedError:
            return True
        return False

# 当你想忽略AlreadyClosedError 异常时,只要把代码用with语句包裹起来即可。
# 程序的执行结果取决于__exit__方法的返回值,如果返回了True,那么这个异常就会被当前的with语句压制住,
# 不再继续抛出达到了忽略异常效果。如果返回了False,这个异常就会被正常抛出,交调用方处理。
with ignore_closed():
	close_conn(conn)

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

__exit__方法三个参数介绍

  • exc_type:异常的类型
  • exc_val:异常对象
  • exc_tb:错误的堆栈对象

如果在项目中忽略某类异常,可以直接调用标准库模块contextlib里的suppress函数,它提供了现场的忽略异常功能。

2.7.使用contextmanager装饰器

虽然上下文管理器很好用,但定义一个符合协议的管理器对象其实挺麻烦的,为了简化这部分工作,python提供了一个非常好用的工具。
@contextmanager 位于内置模块contextlib下,他可以把任何一个生成器函数直接转换为上下文管理器。

举个例子:上面实现自动关闭连接的create_conn_obj 上下文管理器,假如用函数来改写,可以简化成下面这样。

from contextlib import contextmanager
@contextmanager
def create_conn_obj(host, port, timeout=None):
    '''
    创建连接对象,并在退出上下文时自动关闭
    :param host: 
    :param port: 
    :param timeout: 
    :return: 
    '''
    
    conn = create_conn(host, port, timeout=timeout)
    try:
        yield conn
    finally:
        conn.close()

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 以yield关键字为界,yield前的逻辑会在进入管理器时执行(类似于__enter__),yield后的逻辑会在退出管理器时执行(类似__exit__)
  • 如果要在上下文管理器内处理异常,必须用try语句包裹yield语句
    在日常开发中,我们用到的大多数上下文管理器,可以直接通过“生成器函数+@contextmanager”方式来定义,比创建一个符合协议的类要简单。

3.编程建议

3.1.为什么要捕获异常

在代码中捕获异常,它的核心是编码者对处于程序主流程之外的,已知或未知情况的一种妥当处置。而妥当这个词正是异常处理的关键。
异常捕获不是在拿着捕虫网玩捕虫游戏,谁捕的多就获胜。弄一个庞大的try语句,把所有可能出错、不可能出错的代码,全部用except Exception 抱起来,显然是不妥当的。

  • 永远只捕获那些可能会抛出异常的语句块
  • 尽量只捕获精确的异常类型,而不是模糊的Exception
  • 如果出现预期外的异常,让程序早点崩溃也未必是件坏事

下面是精确捕获异常的例子
将保存文件函数代码拆分为两段更精确的异常捕获。

def save_website_title(url, filename):
    # 发送请求
    try:
        resp = requests.get(url)
    except RequestException as e:
        print(f'Unable to write to file:{e}')
        return False

    # 获取标题
    obj = re.search(r'<title>(.*)</title>, resp.text')
    if not obj:
        return False
    title = obj.group(1)
    
    # 保存文件
    try:
        with open(filename, 'w') as fp:
            fp.write(title)
    except IOError as e:
        print(f'save filed: unable to write to write to file{filename}:{e}')
        return False
    else:
        return True

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

3.2.异常要与当前抽象模块一致

1.为什么异常级别要与抽象模块一致

当异常抽象的级别与当前模块不一致时(异常抽象级别高于或低于当前模块),无法复用当前模块的函数

2.异常级别与抽象模块不一致示例

当我们在写后端程序API时,通常会创建一个自定义的统一的异常类,规范API错误码,为客户端处理错误提供方便。
例如我们在项目中定义了错误码异常类:APIErrorCode,然后写了很多继承该类的错误码异常。当需要返回错误信息给调用方,只需要做一次raise就能搞定。

raise error_codes.UNABLE_TO_UPVOTE
raise error_code.USER_HAS_BEEN_BANNED

  
 
  • 1
  • 2

毫无疑问,大家都喜欢用这种方式来返回错误码。因为用起来非常方便:无论当前调用栈有多深,只要你想给调用方返回错误码,直接调用raise error_code.USER_HAS_BEEN_BANNED就行。

APIErrorCode异常类是整个项目中最高层的抽象之一,随着产品的不断演进,项目规模变得越来越庞大,当准备复用一个底层处理图片函数时,由于当时出于方便,在该函数里抛出了高于当前模块级别的异常抽象,打破了process_image()函数的抽象一致性,导致无法复用它。

process_image()函数会尝试打开一个文件,假如该文件不是一个有效的图片格式,就抛出error_codes.INVALID_IMAGE_UPLOADED异常,最终给用户返回错误响应码。

下面来分析下为什么不能复用函数
最初编写process_image()时,调用这个函数就只有“处理用户上传图片的POST请求” 而已。所以为了偷懒,让改函数直接抛出APIErrorCode异常类完成错误处理工作。
当我需要编写一个在后台运行的图片批处理脚本,而它刚好可以复用process_image()函数所实现的功能。
但这时事情开始白给你的不对劲起来,如果我先复用该函数,那么:

  • 必须引入APIErrorCode异常类依赖来捕获异常——哪怕批处理脚本根本用不上这个异常。
  • 必须捕获INVALID_IMAGE_UPLOADED异常——哪怕图片根本就不是由用户上传。
def process_image():
    try:
        image = Image.open(fp)
    except Exception:
        raise error_codes.INVALID_IMAGE_UPLOADED

  
 
  • 1
  • 2
  • 3
  • 4
  • 5

3.异常与模块抽象不一致优化

这就是异常类与模块抽象级别不一致导致的结果。这类情况就是模块抛出了高于所属抽象级别的异常。避免这类错误需要注意以下两点:

  • 让模块只抛出与当前级别一致的异常
  • 在必要的地方进行异常包装与转换

为了满足这两点对代码进行优化

  • image.processer模块应该抛出自己封装的ImageOpenError异常
  • 在贴近高层抽象的地方,将图形处理模块的低级异常ImageOpenError包装为高级异常APIErrorCode
# 创建图形处理模块异常类
class ImagaOpenError(Exception):
    '''图形打开错误异常类
    :param exc: 原始异常
    '''

    def __init__(self, exc):
        self.exc = exc
        # 调用异常父类方法,初始化错误信息
        super().__init__(f'Image open error: {self.exc}')

# 抛出自己封装的ImageOpenError异常
def process_image():
    try:
        image = Image.open(fp)
    except Exception as e:
        raise ImagaOpenError(exc=e)

# 在贴近高层抽象的地方,将图形处理模块的低级异常ImageOpenError包装为高级异常APIErrorCode
def foo_view_function(request):
    try:
        process_image(fp)
    except ImagaOpenError:
        raise raise error_codes.INVALID_IMAGE_UPLOADED

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这样调整以后,就能在后台脚本复用process_image()函数

4.包装抽象级别低于当前模块的异常

除了应该避免抛出高于当前抽象级别的异常外,我们同样应该避免抛出低于当前抽象级别的异常。
如果你使用过第三方HTTP工具库requests,可能已经发现他在请求出错时抛出的异常,并不是他在底层所使用的urllib3模块的原始异常,而是经过requests.exceptions包装过的异常:

try:
    requests.get('https://www.baidu.com')
except Exception as e:
    print(type(e))

# 输出结果
<class 'requests.exceptions.ConnectionError'>

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这样做是为了保证异常类的抽象一致性
urllib3模块是requests依赖的底层实现细节,而这个细节在未来是有可能变动的。当某天requests真的要修改底层实现时,这些包装过的异常类,就可以避免对用户侧的错误处理逻辑产生不良影响。

3.3.不要随意忽略异常

假如send_sms_notification执行失败就会抛出RequestError异常,他会直接被except忽略,就好像从来没有发生过一样。
如果这个异常影响流程执行将会停止程序。
假如这个异常不会影响流程执行,我们将它输出到日志中也总比忽略好,在需要查看异常时可以通过日志查看。

try:
    send_sms_notification(user, message)
except RequestError:
    pass

  
 
  • 1
  • 2
  • 3
  • 4

当编码者决定让自己的代码抛出异常时,他肯定是有原因的。希望调用自己代码的人对这个异常做点什么,调用方可以做如下操作:

  • 在except语句捕获并处理它,继续执行后面的代码
  • 在except语句捕获它,将错误通知给终端用户,终端执行
  • 不捕获异常,让异常网堆栈上层走,最终可能导致程序崩溃

3.4.抛出可区分的异常

当开发者创建自定义异常类时,需要遵循下面几条原则

  • 要继承Exception而不是BaseException
  • 异常类名称最好以Erro或Exception结尾
  • 让调用方清晰区分各种异常

1.抛出可区分的异常实例

def create_from_input():
    name = input()
    try:
        item = create_item(name)
    except CreateItemError as e:
        print(f'create item failed: {e}')
    else:
        print(f'item<{name}> created')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

假如示例中,调用者想针对item已满,这里错误增加一些特殊逻辑,比如情况所有items,我们就的把代码改成下面这样。虽然这段代码通过对比错误字符串实现了需求,但这种做法非常脆弱。假如create_item()稍微调整下一场信息,代码逻辑就会崩溃。

def create_from_input():
    name = input()
    try:
        item = create_item(name)
    except CreateItemError as e:
        if str(e) == 'items is full':
            clear_all_items()
        print(f'create item failed: {e}')
    else:
        print(f'item<{name}> created')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2.设计可区分的更精准的异常类

为了解决上面的问题,我们可以利用异常间的继承关系,设计一些更精准的异常子类。
这样调用方在捕获异常后,也能根据异常对象的error_code来精确分辨异常类型。

# 创建create_item异常类
class CreateItemError(Exception):
    '''创建Item失败异常'''


# 继承CreateItemError异常类,创建子异常类
class CreateItemFullError(CreateItemError):
    '''当前Item已满异常'''


def create_item(name):
    if len(name) > MAX_LENGTH_OF_NAME:
        raise CreateItemError('name of item is too long')
    if len(get_current_items()) > MAX_ITEMS_QUOTA:
        raise CreateItemFullError('item is full')
    return Item(name=name)

# 调用create_item函数,抛出精确的异常类
def create_from_input():
    name = input()
    try:
        item = create_item_except(name)
    except CreateItemFullError as e:
        clear_all_items()
        print(f'create item failed:{e}')
    except CreateItemError as e:
        print(f'create item failed:{e}')
    else:
        print(f'item<{name}> created')

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

3.5.空对象模式

空对象模式就是本该返回None值或抛出异常值时,返回一个符合正常结果接口的特征“空类型对象” 来代替,以此免去调用方错误处理工作。

1.空对象示例

下面是一个统计得分合格的程序,正常得分记录格式是username points 格式,但是由于格式可能不符合要求,所有make_userpoint()方法在解析数据时会抛出异常来通知调用方。

QUALIFIED_POINTS = 80

# 创建异常类
class CreateUserPointError(Exception):
    '''创建用户得分记录失败时抛出异常'''

# 计算得分是否合格
class UserPoint:

    def __init__(self, username, points):
        self.username = username
        self.points = points

    def is_qualified(self):
        return self.points > QUALIFIED_POINTS
    
def make_userpoint(point_string):
    '''
    从字符串中初始化一条得分记录
    :param point_string:
    :return:
    '''
    try:
        username, points = point_string.split()
        points = int(points)
    except ValueError:
        raise CreateUserPointError(f'input must follow pattern "{username} {points}"')
    if points < 0:
        raise  CreateUserPointError('points can not be nagative')
    return UserPoint(username=username, points=points)

# 计算得分合格的人数
def count_qualified(points_data):
    '''
    计算得分合格的总人数
    :param points_data: 
    :return: 
    '''
    
    result = 0
    for point_string in points_data:
        try:
            point_obj = make_userpoint(point_string=point_string)
        except CreateUserPointError:
            pass
        else:
            result += point_obj.is_qualified()
        return result

# 测试得分合格的人数
data = [
    'zhangsan 39',
    'lisi 50',
    'wangwu 90',
    'zhaoliu 88',
    'tom'
]
print(count_qualified(points_data=data))

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

假如引入空对象模式,上面的异常处理逻辑可以完全消失。

QUALIFIED_POINTS = 80

class UserPoint:
    def __init__(self, usname, points):
        self.name = usname
        self.points = points
    
    def is_qualified(self):
        return self.points >= QUALIFIED_POINTS

class NullUserPoint:
    '''
    一个空的用户得分记录
    '''
    username = ''
    points = 0
    
    def is_qualified(self):
        return False

def make_userpoint(point_string):
    '''
    从字符串初始化一条得分记录
    :param point_string: 
    :return: 
    '''
    
    try:
        username, points = point_string.split()
        points = int(points)
    except ValueError:
        return NullUserPoint()
    
    if points < 0:
        return NullUserPoint()
    return UserPoint(username=username, points=points)

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

在新版本里,定义了一个代表“空得分记录” 的新类型:NullUserPoint,每当make_userpoint()接收到无效的输入,执行失败时,就会返回一个NullUserPoint对象。因为返回的是空对象而不是异常,所以调用者count_qualified()就不在需要处理任何异常了。

def count_qualified(points_data):
	# 如果没有make_userpoint执行没有异常就会调用UserPoint对象的is_qualified,如果遇到异常就会调用NullUserPoint对象的is_qualified返回false
	return sum(make_userpoint(s).is_qualified() for s in points_data)

  
 
  • 1
  • 2
  • 3

文章来源: brucelong.blog.csdn.net,作者:Bruce小鬼,版权归原作者所有,如需转载,请联系作者。

原文链接:brucelong.blog.csdn.net/article/details/126669104

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。