返回OSError异常类子类实例的逻辑在哪里?

时间:2016-09-14 19:14:54

标签: python

我一直在寻找一些对某些人来说可能比较愚蠢的东西,但对我来说非常有趣! : - )

输入和输出错误已在Python 3.3中与OSError合并,因此异常类层次结构发生了变化。关于内置类OSError的一个有趣的特性是,它在传递errnostrerror时返回它的子类

>>> OSError(2, os.strerror(2))
FileNotFoundError(2, 'No such file or directory')

>>> OSError(2, os.strerror(2)).errno
2
>>> OSError(2, os.strerror(2)).strerror
'No such file or directory'

正如您所看到的,将errnostrerror传递给OSError的构造函数会返回FileNotFoundError实例,它是OSError的子类。

Python Doc:

  

构造函数实际上经常返回OSError的子类,如下面的OS exceptions中所述。特定的子类取决于最终的errno值。 此行为仅在直接或通过别名构造OSError时发生,并且在子类化时不会继承。

我想编写一个以这种方式运行的子类。这主要是好奇心,而不是现实世界的代码。我也试图知道,创建子类对象的逻辑在哪里,例如它是用__new__编码的?如果__new__包含用于创建子类实例的逻辑,那么继承自OSError通常会返回此行为,除非在__new__中进行某种类型检查:

>>> class A(OSError): pass 
>>> A(2, os.strerror(2))
A(2, 'No such file or directory')

必须进行类型检查:

# If passed OSError, returns subclass instance
>>> A.__new__(OSError, 2, os.strerror(2))         
FileNotFoundError(2, 'No such file or directory')

# Not OSError? Return instance of A
>>> A.__new__(A, 2, os.strerror(2)
A(2, 'No such file or directory')

我一直在挖掘C代码以找出这些代码的确切位置,因为我不是C的专家,我怀疑这是真正的逻辑和(我对这一点很坦诚) ):

exceptions.c

if (myerrno && PyLong_Check(myerrno) &&
    errnomap && (PyObject *) type == PyExc_OSError) {
    PyObject *newtype;
    newtype = PyDict_GetItem(errnomap, myerrno);
    if (newtype) {
        assert(PyType_Check(newtype));
        type = (PyTypeObject *) newtype;
    }
    else if (PyErr_Occurred())
        goto error;
}
}

现在我想知道在不使用C代码的情况下从Python本身扩展errnomap的可能性,以便OSErro可以创建用户定义类的实例,如果你问我为什么会这样做去做?我会说,只是为了好玩。

2 个答案:

答案 0 :(得分:6)

您确认errnomap是保存从errno值到OSError子类的映射的变量,但遗憾的是它不会导出exceptions.c来源之外文件,所以没有可移植的方法来修改它。

可以使用高度非便携式黑客访问它,我提出了一种可能的方法(使用调试器),纯粹是本着乐趣。这适用于任何x86-64 Linux系统。

>>> import os, sys
>>> os.system("""gdb -p %d \
-ex 'b PyDict_GetItem if (PyLong_AsLongLong($rsi) == -1 ? \
(PyErr_Clear(), 0) : PyLong_AsLongLong($rsi)) == 0xbaadf00d' \
-ex c \
-ex 'call PySys_SetObject("errnomap", $rdi)' --batch >/dev/null 2>&1 &""" % os.getpid()) 
0
>>> OSError(0xbaadf00d, '')
OSError(3131961357, '')
>>> sys.errnomap
{32: <class 'BrokenPipeError'>, 1: <class 'PermissionError'> [...]}
>>> class ImATeapotError(OSError):
    pass
>>> sys.errnomap[99] = ImATeapotError
>>> OSError(99, "I'm a teapot")
ImATeapotError(99, "I'm a teapot")

快速解释其工作原理:

gdb -p %d [...] --batch >/dev/null 2>&1 &

以无人参与模式(os.getpid())将调试器附加到当前Python进程(--batch),丢弃输出(>/dev/null 2>&1)并在后台(&) ,允许Python继续运行。

b PyDict_GetItem if (PyLong_AsLongLong($rsi) == -1 ? (PyErr_Clear(), 0) : PyLong_AsLongLong($rsi)) == 0xbaadf00d

当Python程序accesses any dictionary时,如果键is an int具有魔术值(稍后用作OSError(0xbaadf00d, '')),则中断;如果它不是int,我们刚刚提出TypeError,那么suppress it

call PySys_SetObject("errnomap", $rdi)

当发生这种情况时,我们知道正在查找的字典是errnomap; store it as an attribute on the sys module

答案 1 :(得分:0)

您无法从Python更改OSError的行为,因为它未在Python中实现。

对于在Python中实现的类,您可以编写__new__,这样只有在基类上调用它时才会返回子类。然后行为将不会被继承。

class MyClass(object):
    def __new__(cls, sub=0, _subtypes={}):
        if cls is MyClass:
             if sub not in _subtypes:
                 _subtypes[sub] = type("MyClass(%s)" % sub, (MyClass,), {})
             return _subtypes[sub](sub)
        return object.__new__(cls, sub)
    def __init__(self, sub):
        assert type(self).__name__ == "MyClass(%s)" % sub

class SubClass(MyClass):
     def __init__(self, sub=None):
         assert type(self).__name__ == "SubClass"

print(MyClass(1))    # <__main__.MyClass(1) object at 0x01EB1EB0>
print(SubClass())    # <__main__.SubClass object at 0x01EB1CD0>