CodeQL 文档

特殊方法中引发非标准异常

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

点击查看 CodeQL 仓库中的查询

用户定义的类通过特殊方法(也称为“魔术方法”)与 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:从不引发异常,始终返回 TrueFalse

  • 排序比较方法,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来处理任何IOErrorKeyError__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)
                           

参考

  • ©GitHub, Inc.
  • 条款
  • 隐私