构建自引用元组

Noc*_*wer 14 python ctypes tuples creation self-reference

在多年前在一个论坛中看到一个从未解决过的对话之后,它让我想知道如何正确地创建一个引用自身的元组.从技术上讲,这是一个非常糟糕的主意,因为元组应该是不可变的.一个不可变对象怎么可能包含自己呢?但是,这个问题不是关于最佳实践,而是关于Python中可能的内容的查询.

import ctypes

def self_reference(array, index):
    if not isinstance(array, tuple):
        raise TypeError('array must be a tuple')
    if not isinstance(index, int):
        raise TypeError('index must be an int')
    if not 0 <= index < len(array):
        raise ValueError('index is out of range')
    address = id(array)
    obj_refcnt = ctypes.cast(address, ctypes.POINTER(ctypes.c_ssize_t))
    obj_refcnt.contents.value += 1
    if ctypes.cdll.python32.PyTuple_SetItem(ctypes.py_object(array),
                                            ctypes.c_ssize_t(index),
                                            ctypes.py_object(array)):
        raise RuntimeError('PyTuple_SetItem signaled an error')
Run Code Online (Sandbox Code Playgroud)

前一个函数旨在访问Python的C API,同时牢记内部结构和数据类型.但是,运行该函数时通常会生成以下错误.通过未知的过程,之前可以通过类似的技术创建自引用元组.

问题:如何self_reference修改功能以始终如一地工作?

>>> import string
>>> a = tuple(string.ascii_lowercase)
>>> self_reference(a, 2)
Traceback (most recent call last):
  File "<pyshell#56>", line 1, in <module>
    self_reference(a, 2)
  File "C:/Users/schappell/Downloads/srt.py", line 15, in self_reference
    ctypes.py_object(array)):
WindowsError: exception: access violation reading 0x0000003C
>>> 
Run Code Online (Sandbox Code Playgroud)

编辑:这是与解释器的两个不同的对话,有点令人困惑.如果我正确理解文档,上面的代码似乎是正确的.但是,下面的对话似乎彼此冲突,而且self_reference上面的功能也相互冲突.

会话1:

Python 3.2.3 (default, Apr 11 2012, 07:15:24) [MSC v.1500 32 bit (Intel)]
on win32
Type "copyright", "credits" or "license()" for more information.
>>> from ctypes import *
>>> array = tuple(range(10))
>>> cast(id(array), POINTER(c_ssize_t)).contents.value
1
>>> cast(id(array), POINTER(c_ssize_t)).contents.value += 1
>>> cast(id(array), POINTER(c_ssize_t)).contents.value
2
>>> array
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
>>> cdll.python32.PyTuple_SetItem(c_void_p(id(array)), 0,
                                  c_void_p(id(array)))
Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    cdll.python32.PyTuple_SetItem(c_void_p(id(array)), 0,
                                  c_void_p(id(array)))
WindowsError: exception: access violation reading 0x0000003C
>>> cdll.python32.PyTuple_SetItem(c_void_p(id(array)), 0,
                                  c_void_p(id(array)))
Traceback (most recent call last):
  File "<pyshell#7>", line 1, in <module>
    cdll.python32.PyTuple_SetItem(c_void_p(id(array)), 0,
                                  c_void_p(id(array)))
WindowsError: exception: access violation reading 0x0000003C
>>> array
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
>>> cdll.python32.PyTuple_SetItem(c_void_p(id(array)), 0,
                                  c_void_p(id(array)))
0
>>> array
((<NULL>, <code object __init__ at 0x02E68C50, file "C:\Python32\lib
kinter\simpledialog.py", line 121>, <code object destroy at 0x02E68CF0,
file "C:\Python32\lib   kinter\simpledialog.py", line 171>, <code object
body at 0x02E68D90, file "C:\Python32\lib      kinter\simpledialog.py",
line 179>, <code object buttonbox at 0x02E68E30, file "C:\Python32\lib
kinter\simpledialog.py", line 188>, <code object ok at 0x02E68ED0, file
"C:\Python32\lib        kinter\simpledialog.py", line 209>, <code object
cancel at 0x02E68F70, file "C:\Python32\lib    kinter\simpledialog.py",
line 223>, <code object validate at 0x02E6F070, file "C:\Python32\lib
kinter\simpledialog.py", line 233>, <code object apply at 0x02E6F110, file
"C:\Python32\lib     kinter\simpledialog.py", line 242>, None), 1, 2, 3, 4,
5, 6, 7, 8, 9)
>>>
Run Code Online (Sandbox Code Playgroud)

会话2:

Python 3.2.3 (default, Apr 11 2012, 07:15:24) [MSC v.1500 32 bit (Intel)]
on win32
Type "copyright", "credits" or "license()" for more information.
>>> from ctypes import *
>>> array = tuple(range(10))
>>> cdll.python32.PyTuple_SetItem(c_void_p(id(array)), c_ssize_t(1),
                                  c_void_p(id(array)))
0
>>> array
(0, (...), 2, 3, 4, 5, 6, 7, 8, 9)
>>> array[1] is array
True
>>>
Run Code Online (Sandbox Code Playgroud)

nne*_*neo 6

AFAICT,你看到问题的原因是因为PyTuple_SetItem如果元组的引用计数不完全是一个则失败.这是为了防止在元组已经在别处使用过时使用该函数.我不确定为什么你会从中获得访问冲突,但可能是因为抛出的异常PyTuple_SetItem没有得到妥善处理.此外,数组似乎变异到其他对象的原因是因为PyTuple_SetItemDECREF是每个失败的元组; 在两次失败之后,refcount为零,因此释放了对象(并且其他一些对象显然最终位于相同的内存位置).

pythonapi在ctypes中使用对象是访问Python DLL的首选方法,因为它正确处理Python异常并保证使用正确的调用约定.

我没有Windows机器来测试它,但以下在Mac OS X(Python 2.7.3和3.2.2)上工作正常:

import ctypes

def self_reference(array, index):
    # Sanity check. We can't let PyTuple_SetItem fail, or it will Py_DECREF
    # the object and destroy it.
    if not isinstance(array, tuple):
        raise TypeError("array must be a tuple")

    if not 0 <= index < len(array):
        raise IndexError("tuple assignment index out of range")

    arrayobj = ctypes.py_object(array)

    # Need to drop the refcount to 1 in order to use PyTuple_SetItem.
    # Needless to say, this is incredibly dangerous.
    refcnt = ctypes.pythonapi.Py_DecRef(arrayobj)
    for i in range(refcnt-1):
        ctypes.pythonapi.Py_DecRef(arrayobj)

    try:
        ret = ctypes.pythonapi.PyTuple_SetItem(arrayobj, ctypes.c_ssize_t(index), arrayobj)
        if ret != 0:
            raise RuntimeError("PyTuple_SetItem failed")
    except:
        raise SystemError("FATAL: PyTuple_SetItem failed: tuple probably unusable")

    # Restore refcount and add one more for the new self-reference
    for i in range(refcnt+1):
        ctypes.pythonapi.Py_IncRef(arrayobj)
Run Code Online (Sandbox Code Playgroud)

结果:

>>> x = (1,2,3,4,5)
>>> self_reference(x, 1)
>>> import pprint
>>> pprint.pprint(x)
(1, <Recursion on tuple with id=4299516720>, 3, 4, 5)
Run Code Online (Sandbox Code Playgroud)


Noc*_*wer 6

感谢nneonneo的帮助,我决定采用以下self_reference方法实现.

import ctypes

ob_refcnt_p = ctypes.POINTER(ctypes.c_ssize_t)

class GIL:
    acquire = staticmethod(ctypes.pythonapi.PyGILState_Ensure)
    release = staticmethod(ctypes.pythonapi.PyGILState_Release)

class Ref:
    dec = staticmethod(ctypes.pythonapi.Py_DecRef)
    inc = staticmethod(ctypes.pythonapi.Py_IncRef)

class Tuple:
    setitem = staticmethod(ctypes.pythonapi.PyTuple_SetItem)
    @classmethod
    def self_reference(cls, array, index):
        if not isinstance(array, tuple):
            raise TypeError('array must be a tuple')
        if not isinstance(index, int):
            raise TypeError('index must be an int')
        if not 0 <= index < len(array):
            raise ValueError('index is out of range')
        GIL.acquire()
        try:
            obj = ctypes.py_object(array)
            ob_refcnt = ctypes.cast(id(array), ob_refcnt_p).contents.value
            for _ in range(ob_refcnt - 1):
                Ref.dec(obj)
            if cls.setitem(obj, ctypes.c_ssize_t(index), obj):
                raise SystemError('PyTuple_SetItem was not successful')
            for _ in range(ob_refcnt):
                Ref.inc(obj)
        finally:
            GIL.release()
Run Code Online (Sandbox Code Playgroud)

要使用此方法,请按照下面显示的示例创建自己的自引用元组.

>>> array = tuple(range(5))
>>> Tuple.self_reference(array, 1)
>>> array
(0, (...), 2, 3, 4)
>>> Tuple.self_reference(array, 3)
>>> array
(0, (...), 2, (...), 4)
>>> 
Run Code Online (Sandbox Code Playgroud)


wim*_*wim 6

更简单的解决方案:

import ctypes
tup = (0,)
ctypes.c_longlong.from_address(id(tup)+24).value = id(tup)
Run Code Online (Sandbox Code Playgroud)

结果:

>>> tup
((...),)
>>> type(tup)
tuple
>>> tup[0] is tup
True
Run Code Online (Sandbox Code Playgroud)