特殊方法中引发非标准异常¶
ID: py/unexpected-raise-in-special-method
Kind: problem
Security severity:
Severity: recommendation
Precision: very-high
Tags:
- reliability
- maintainability
- convention
Query suites:
- python-security-and-quality.qls
用户定义的类通过特殊方法(也称为“魔术方法”)与 Python 虚拟机交互。例如,要使类支持加法运算,它必须实现 __add__
和 __radd__
特殊方法。当表达式 a + b
被计算时,Python 虚拟机将调用 type(a).__add__(a, b)
,如果该方法未实现,它将调用 type(b).__radd__(b, a)
。
由于虚拟机为常用表达式调用这些特殊方法,因此类的用户会期望这些操作引发标准异常。例如,用户会期望表达式 a.b
可能会引发 AttributeError
,如果对象 a
没有属性 b
。如果改为引发 KeyError
,那么这将是意外的,并且可能会破坏期望 AttributeError
但不期望 KeyError
的代码。
因此,如果方法无法执行预期操作,则其响应应符合下面描述的标准协议。
属性访问,
a.b
:引发AttributeError
算术运算,
a + b
:不引发异常,而是返回NotImplemented
。索引,
a[b]
:引发KeyError
。哈希,
hash(a)
:使用__hash__ = None
表示对象不可哈希。相等方法,
a != b
:从不引发异常,始终返回True
或False
。排序比较方法,
a < b
:如果对象无法排序,则引发TypeError
。大多数其他方法:理想情况下,根本不实现该方法,否则引发
TypeError
表示操作不受支持。
建议¶
如果该方法是抽象方法,则使用 @abstractmethod
装饰器声明它。否则,要么删除该方法,要么确保该方法引发正确类型的异常。
示例¶
此示例显示了两个不可哈希类。第一个类以非标准方式不可哈希,这可能会导致维护问题。第二个类是已更正的,它使用不可哈希类的标准习惯用法。
#Incorrect unhashable class
class MyMutableThing(object):
def __init__(self):
pass
def __hash__(self):
raise NotImplementedError("%r is unhashable" % self)
#Make class unhashable in the standard way
class MyCorrectMutableThing(object):
def __init__(self):
pass
__hash__ = None
在这个例子中,第一个类是隐式抽象类;__add__
方法未实现,可能是预期在子类中实现它。第二个类使用@abstractmethod
装饰器在未实现的__add__
方法上显式声明了这一点。
#Abstract base class, but don't declare it.
class ImplicitAbstractClass(object):
def __add__(self, other):
raise NotImplementedError()
#Make abstractness explicit.
class ExplicitAbstractClass:
__metaclass__ = ABCMeta
@abstractmethod
def __add__(self, other):
raise NotImplementedError()
在最后一个例子中,第一个类实现了由文件存储支持的集合。但是,如果在__getitem__
方法中抛出了IOError
,它将传播给调用者。第二个类通过重新抛出KeyError
来处理任何IOError
,KeyError
是__getitem__
方法的标准异常。
#Incorrect file-backed table
class FileBackedTable(object):
def __getitem__(self, key):
if key not in self.index:
raise IOError("Key '%s' not in table" % key)
else:
#May raise an IOError
return self.backing.get_row(key)
#Correct by transforming exception
class ObjectLikeFileBackedTable(object):
def get_from_key(self, key):
if key not in self.index:
raise IOError("Key '%s' not in table" % key)
else:
#May raise an IOError
return self.backing.get_row(key)
def __getitem__(self, key):
try:
return self.get_from_key(key)
except IOError:
raise KeyError(key)