异常与错误处理高级用法
异常与错误处理高级用法
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
- 点赞
 - 收藏
 - 关注作者
 
            
           
评论(0)