关于条件检查,异常,错误码的一些思考

2012-12-16 08:48:41 +08:00
pythonee  pythonee
因为之前的一篇文章,"我不会抛弃python而是用Go",这两天看了很多讨论和文章,有些想法


异常
1,符合“速错”的原则,在python鼓励是用Exception,而非错误码和条件检查
2,异常通常可以返回完整的调用堆栈,可以方便程序员定位错误和分别进行处理或统一处理
3,你可以选择catch异常,或是不catch,你可以定义自己的异常,提供更具体的通知
4,一般在系统边界用try-catch,比如读取文件,访问网络,访问数据库等
5,异常是要消耗一部分性能的,尤其是.net的应用程序

条件检查和错误码
1,如果你返回错误码,那么就是在告诉调用方,这个方法永远是有响应的,虽然有可能出错,但总能返回,你在这个函数黑盒会尽可能的进行输入检查等其他防御式的编程
2,错误码很方便代码review,这个我真不知道根据是什么
3,如果调用方忘了检查错误码,等到程序崩溃,那么鬼都不知道哪里出错了,异常优于错误码就在于不会悄悄的崩溃,而是visible failure
4,错误码的使用者认为错误码的方式是为了编写非常严格的软件产品,异常是懒程序员的做法


何时使用异常和错误码呢?
1,取决于我们在哪一层编写代码
如果是业务层,从在系统边界处捕捉异常的观点出发,我们应该尽可能的检查输入,如果是数据库访问,这里用太多未知错误,我们可能会不断往上抛异常

2,取决于我们用的编程语言
如果是C/GO,那就仔细检查我们的错误码吧,如果是python,那么就EAFP吧,尽量用try-catch,如果是Java,这货就比较麻烦,他的异常又分为检查异常和非检查异常,其实检查异常就是python中的异常,非检查异常就是错误了,从我见过的代码看,好像Java中还是条件检查和异常捕捉双管齐下的,等高人来说说吧

3,取决于带给程序员的负担
对于python这种动态语言来说,虽然语言鼓励你使用try-catch,但是你既然不知道要catch什么,难道大家写python代码的时候都去查文档的吗,对于C来说,显然代码是随着错误码的增加而增加的,而且要考虑非常多的情况,或许这就是C程序员值钱的地方????

4,取决于你在写的产品
如果你写一个服务器,你不会因为几个非法输入或类型错误就崩溃,然后不响应吧,所以说,对于这种持久响应的产品,我觉得错误码或许是一个比较好的方式,它强迫你必须慎重思考该如何处理

5,
You shouldn't throw exceptions for things that happen all the time. Then they'd be "ordinaries".
这句话真不懂

最后的问题,big problem
对于我们可预见的、并且可以handle的错误我们需要异常吗?
比如说访问了一个字典中不存在的key值,比如说两个对象相加,但是两者类型不一,我们需要先检查还是直接异常呢?
5336 次点击
所在节点   程序员  程序员
15 条回复
thedevil5032
thedevil5032
2012-12-16 09:19:32 +08:00
聊聊自己实践的经验:

1. try ... except, 如果只是偶尔发生异常(except), 速度上比 if ... else 快.
2. '''You shouldn't throw exceptions for things that happen all the time. Then they'd be "ordinaries".''' 如果某件事结果总是这样的(假设为A), 那么你就不需要抛出异常. 因为 A 就是常态.
3. 可预见, 可 handle 的当然可以利用异常解决啊, 这应该是设计 EAFP 的初衷之一吧?
比如, 一段代码读取文件, 但是可能传过来的文件名实际上是不存在的, 那么这时用户实际上需要创建一个新文件.

try :
readFile()
except IOError : # 有时确实需要查文档, 或者通过试错来找出可能的异常. 另外的时候 except Exception 也可以.
createFile()

针对不存在的 Key 或者两个对象相加, 我觉得首先应该从程序的逻辑上考虑能不能消除这样的可能性. 从代码上解决, 而不是留给后面的代码去判断(我的理解中异常也算是一种判断).
如果不能消除, 那么就根据正常情况和异常情况出现的概率决定用 try 或者 if 吧. 因为 try 在正常情况下比 if 快, if 在异常情况下比 try 快.

这时我的一些想法, 欢迎讨论.
reorx
reorx
2012-12-16 09:49:02 +08:00
"对于我们可预见的、并且可以handle的错误我们需要异常吗?"

我个人觉得是不需要的,或者说不应该使用异常的。

比如楼上的例子,文件读取,我们很容易就能考虑到文件不存在的情况,这对我们来说是可预见而且清晰的,那么就应该先做判断:

if os.path.exists(filepath):
with open(filepath, 'r') as f:
...

这样和 try except IOError 有什么区别呢?你虽然知道不存在的文件路径会引发 IOError,但你不能确定这是唯一引发 IOError 的情况,那么做明确的条件判断,以后某种你所未知的情况同样引发了 IOError,你就能及时了解到,并选择更恰当的方式进行处理。

当然有些异常处理和条件判断区别是完全一样的,比如 if key in dict 和 try dict[key] except KeyError,但就像楼主引用的那句英文所说,可遇见的错误,是 ordinary situation,还是应该尽量 if 之。至少可以避免 try except 的使用不当导致的某种本应出现的错误被屏蔽的情况,那样才真的是鬼都不知道哪里错了 XD
bhuztez
bhuztez
2012-12-16 10:52:06 +08:00
所以要用Erlang啊,模式匹配才是正解
pythonee
pythonee
2012-12-16 10:56:02 +08:00
@reorx
其实你的这个想法就是符合 返回错误 代码的这种形式的,但是对于python来说,这是非常不合理的想法,就拿你举的这个例子来说,
if key in dict
# do something

想想如果在并发的时候,这个if刚刚检测完,另外一个线程把这个key remove掉了,怎么办
pythonee
2012-12-16 11:02:29 +08:00
@thedevil5032
其实正是这个”正常“情况比较难区分,文件不存在是”正常“的吗,数组越界是可预见的吗,文件不可读是预见的吗?.....

这些可预见并可handle的异常我想可以类比java中的checked exception,那IOException, IndexOutOfBoundException等都是可以预见的,但是我们能做的并不多。

另外java这种静态语言还能强迫程序员对Checked exception进行处理,而python可能要去看文档,还要想该捕捉哪些异常,想想对于大型程序库,如果对于一些普通操作,你都需要考虑每行代 码是否会抛出异常、是否有必要捕捉处理,这对于开发效率和程序员的时间来说都是非常严重的拖累
pythonee
2012-12-16 11:04:15 +08:00
@bhuztez

模式匹配为什么是正解?它在解决不可预知的错误和可预知的错误有特别好的方法吗
pythonee
2012-12-16 11:08:46 +08:00
@pythonee
说错了,IndexOutOfException应该不是检查异常
thedevil5032
2012-12-16 11:15:06 +08:00
@pythonee 举个例子, 比如我有一个容器, 容器是 Counter 类, 实际上就是 dict, 而它的 value 存的是计数值. 容器里面装的都是单词和他们在某些文本里出现的次数, 通过这个次数来决定单词出现的概率. 那么很有可能我输入的单词, 即使拼写完全正确, 并没有被容器所收录. 但显然不能因为这个单词没有被收录就认为这个单词不存在(概率 0), 所以此时我不应该使用 Error, 而是自定义 Counter.__missing__ 使它返回 1. 这可以算是一个"正常"的异常.
参考: http://blog.youxu.info/spell-correct.html 其中关于 "平滑化" 的那段.

可能并不是一个很好的例子, 但是可以说明某些情况下的 Error 是不用异常来或者判断处理的.
reorx
2012-12-16 11:15:43 +08:00
@pythonee 你说得有道理,在并发的情况下,要考虑得更多。可否这样归纳,一般情况下,多用 explicit if,并发多线程等环境下,多用 try except?
bhuztez
2012-12-16 12:00:51 +08:00
函数总是返回错误代码,由调用的一方去做模式匹配,不匹配说明是碰到了意外情况则抛错,抛错直接导致process crash掉。若无必要,绝不catch。

$ cat mymodule.erl
-module(mymodule).
-export([do_something/0]).

do_something() ->
{error, sorry}.

$ erl
Erlang R15B02 (erts-5.9.2) [source] [64-bit] [smp:4:4] [async-threads:0] [kernel-poll:false]

Eshell V5.9.2 (abort with ^G)
1> c(mymodule).
{ok,mymodule}
2> {ok, Result} = mymodule:do_something().
** exception error: no match of right hand side value {error,sorry}
3> case mymodule:do_something() of
3> {ok, Result} ->
3> success;
3> {error, Reason} ->
3> failure
3> end.
failure
4>

第一,抛错是一种代价很高的操作,当且仅当发生意外的时候抛错。而如果没有抛错机制,意外情况处理起来很麻烦。
第二,从模式匹配的角度看,if/else, try/except 都是poor man's pattern match,其实你已经在用模式匹配了,只不过每次都自己人肉把模式匹配的逻辑翻译成 if/else, try/except,而且很多时候还很难处理好

很多人对Erlang有偏见,非要认为Erlang是函数式语言,程序员会不习惯。事实上,Erlang是模式匹配语言,模式匹配就是实现正确的错误处理机制的关键。Erlang的函数式特征仅仅是巧合,而不是故意要设计成这样的。Erlang的消息机制就已经是副作用了,已经不能算是纯函数式了。

在没有发生意外的时候就抛错和发生意外了还不抛错,都是不对的。非要说一种错误比另一种错误更不坏是很荒唐的。明明知道模式匹配才是正确的做法,而故意不提那也太赖皮了。恩,我就是怀疑Go的粉丝们在耍赖。
pythonee
2012-12-16 12:53:07 +08:00
@reorx
好像不能这么简单的总结
速错对一个产品来说也不是很好的主意
haohaolee
2012-12-16 15:23:50 +08:00
“在正式发布的产品里,速错应该不是理想的解决办法” 这得看产品和方法论,要是用户不在乎是不是crash呢,人家就想快速定位错误呢?严不严肃还是看coder的水平和态度,没理由用异常的就比使用错误码的更容易崩溃,况且用异常也可以在边界try catch all

异常的问题(我的认识范围,主要是C++)在于它对程序员的要求高,特别是在判断什么是异常什么不是方面必须有足够的经验。所以在水平参差不齐的团队中使用异常更难控制代码的质量和复杂度
pythonee
2012-12-16 15:35:45 +08:00
@haohaolee 非常同意你的观点,区分什么是异常什么不是,真的是个big problem,所以也是本帖想要讨论的

但是如果从这个角度对比的话,貌似java的检查异常和非检查异常就很好呢,它提供了区分的标准
pythonee
2012-12-16 15:48:24 +08:00
@bhuztez 哦,纯不纯函数不要紧,lisp要不纯,haskell也是装纯呢
erlang的函数调用貌似都是模式匹配,对于你举的例子来说,类比成go就是go版的多返回值,然后可以检查err是否为nil了,你这个不就是包装后的"返回错误码"了吗,go的error类型也只是一个接口而已啊,也可以自己定义更加具体的错误消息的,相比c语言一个冰冷的数字,好像进化了一点
pythonee
2012-12-16 15:48:46 +08:00
lisp也不纯

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/55027

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX