Skip to content

Commit d59fe61

Browse files
committed
15.1完成
1 parent 5fd15f9 commit d59fe61

File tree

1 file changed

+171
-161
lines changed

1 file changed

+171
-161
lines changed

source/c15/p01_access_ccode_using_ctypes.rst

Lines changed: 171 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,6 @@
101101
distance.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point))
102102
distance.restype = ctypes.c_double
103103
104-
If all goes well, you should be able to load the module and use the resulting C functions.
105-
For example:
106104
如果一切正常,你就可以加载并使用里面定义的C函数了。例如:
107105

108106
::
@@ -154,163 +152,175 @@ For example:
154152
一旦你知道了C函数库的位置,那么就可以像下面这样使用 ``ctypes.cdll.LoadLibrary()`` 来加载它,
155153
其中 ``_path`` 是标准库的全路径:
156154

155+
.. code-block:: python
156+
157+
_mod = ctypes.cdll.LoadLibrary(_path)
158+
159+
函数库被加载后,你需要编写几个语句来提取特定的符号并指定它们的类型。
160+
就像下面这个代码片段一样:
161+
162+
.. code-block:: python
163+
164+
# int in_mandel(double, double, int)
165+
in_mandel = _mod.in_mandel
166+
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
167+
in_mandel.restype = ctypes.c_int
168+
169+
在这段代码中,``.argtypes`` 属性是一个元组,包含了某个函数的输入按时,
170+
而 ``.restype`` 就是相应的返回类型。
171+
``ctypes`` 定义了大量的类型对象(比如c_double, c_int, c_short, c_float等),
172+
代表了对应的C数据类型。如果你想让Python能够传递正确的参数类型并且正确的转换数据的话,
173+
那么这些类型签名的绑定是很重要的一步。如果你没有这么做,不但代码不能正常运行,
174+
还可能会导致整个解释器进程挂掉。
175+
使用ctypes有一个麻烦点的地方是原生的C代码使用的术语可能跟Python不能明确的对应上来。
176+
``divide()`` 函数是一个很好的例子,它通过一个参数除以另一个参数返回一个结果值。
177+
尽管这是一个很常见的C技术,但是在Python中却不知道怎样清晰的表达出来。
178+
例如,你不能像下面这样简单的做:
179+
180+
::
181+
182+
>>> divide = _mod.divide
183+
>>> divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
184+
>>> x = 0
185+
>>> divide(10, 3, x)
186+
Traceback (most recent call last):
187+
File "<stdin>", line 1, in <module>
188+
ctypes.ArgumentError: argument 3: <class 'TypeError'>: expected LP_c_int
189+
instance instead of int
190+
>>>
191+
192+
就算这个能正确的工作,它会违反Python对于整数的不可更改原则,并且可能会导致整个解释器陷入一个黑洞中。
193+
对于涉及到指针的参数,你通常需要先构建一个相应的ctypes对象并像下面这样传进去:
194+
195+
::
196+
197+
>>> x = ctypes.c_int()
198+
>>> divide(10, 3, x)
199+
3
200+
>>> x.value
201+
1
202+
>>>
203+
204+
在这里,一个 ``ctypes.c_int`` 实例被创建并作为一个指针被传进去。
205+
跟普通Python整形不同的是,一个 ``c_int`` 对象是可以被修改的。
206+
``.value`` 属性可被用来获取或更改这个值。
207+
208+
对于那些不像Python的C调用,通常可以写一个小的包装函数。
209+
这里,我们让 ``divide()`` 函数通过元组来返回两个结果:
210+
211+
.. code-block:: python
212+
213+
# int divide(int, int, int *)
214+
_divide = _mod.divide
215+
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
216+
_divide.restype = ctypes.c_int
217+
218+
def divide(x, y):
219+
rem = ctypes.c_int()
220+
quot = _divide(x,y,rem)
221+
return quot, rem.value
222+
223+
``avg()`` 函数又是一个新的挑战。C代码期望接受到一个指针和一个数组的长度值。
224+
但是,在Python中,我们必须考虑这个问题:数组是啥?它是一个列表?一个元组?
225+
还是 ``array`` 模块中的一个数组?还是一个 ``numpy`` 数组?还是说所有都是?
226+
实际上,一个Python“数组”有多种形式,你可能想要支持多种可能性。
227+
228+
``DoubleArrayType`` 演示了怎样处理这种情况。
229+
在这个类中定义了一个单个方法 ``from_param()`` 。
230+
这个方法的角色是接受一个单个参数然后将其向下转换为一个合适的ctypes对象
231+
(本例中是一个 ``ctypes.c_double`` 的指针)。
232+
在 ``from_param()`` 中,你可以做任何你想做的事。
233+
参数的类型名被提取出来并被用于分发到一个更具体的方法中去。
234+
例如,如果一个列表被传递过来,那么 ``typename`` 就是 ``list`` ,
235+
然后 ``from_list`` 方法被调用。
236+
237+
对于列表和元组,``from_list`` 方法将其转换为一个 ``ctypes`` 的数组对象。
238+
这个看上去有点奇怪,下面我们使用一个交互式例子来将一个列表转换为一个 ``ctypes`` 数组:
239+
240+
::
241+
242+
>>> nums = [1, 2, 3]
243+
>>> a = (ctypes.c_double * len(nums))(*nums)
244+
>>> a
245+
<__main__.c_double_Array_3 object at 0x10069cd40>
246+
>>> a[0]
247+
1.0
248+
>>> a[1]
249+
2.0
250+
>>> a[2]
251+
3.0
252+
>>>
253+
254+
对于数组对象,``from_array()`` 提取底层的内存指针并将其转换为一个 ``ctypes`` 指针对象。例如:
255+
256+
::
257+
258+
>>> import array
259+
>>> a = array.array('d',[1,2,3])
260+
>>> a
261+
array('d', [1.0, 2.0, 3.0])
262+
>>> ptr_ = a.buffer_info()
263+
>>> ptr
264+
4298687200
265+
>>> ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
266+
<__main__.LP_c_double object at 0x10069cd40>
267+
>>>
268+
269+
``from_ndarray()`` 演示了对于 ``numpy`` 数组的转换操作。
270+
通过定义 ``DoubleArrayType`` 类并在 ``avg()`` 类型签名中使用它,
271+
那么这个函数就能接受多个不同的类数组输入了:
272+
273+
::
274+
275+
>>> import sample
276+
>>> sample.avg([1,2,3])
277+
2.0
278+
>>> sample.avg((1,2,3))
279+
2.0
280+
>>> import array
281+
>>> sample.avg(array.array('d',[1,2,3]))
282+
2.0
283+
>>> import numpy
284+
>>> sample.avg(numpy.array([1.0,2.0,3.0]))
285+
2.0
286+
>>>
287+
288+
本节最后一部分向你演示了怎样处理一个简单的C结构。
289+
对于结构体,你只需要像下面这样简单的定义一个类,包含相应的字段和类型即可:
290+
291+
.. code-block:: python
292+
293+
class Point(ctypes.Structure):
294+
_fields_ = [('x', ctypes.c_double),
295+
('y', ctypes.c_double)]
296+
297+
一旦类被定义后,你就可以在类型签名中或者是需要实例化结构体的代码中使用它。例如:
298+
299+
::
300+
301+
>>> p1 = sample.Point(1,2)
302+
>>> p2 = sample.Point(4,5)
303+
>>> p1.x
304+
1.0
305+
>>> p1.y
306+
2.0
307+
>>> sample.distance(p1,p2)
308+
4.242640687119285
309+
>>>
310+
311+
最后一些小的提示:如果你想在Python中访问一些小的C函数,那么 ``ctypes`` 是一个很有用的函数库。
312+
尽管如此,如果你想要去访问一个很大的库,那么可能就需要其他的方法了,比如 ``Swig`` (15.9节会讲到) 或
313+
Cython(15.10节)。
314+
315+
对于大型库的访问有个主要问题,由于ctypes并不是完全自动化,
316+
那么你就必须花费大量时间来编写所有的类型签名,就像例子中那样。
317+
如果函数库够复杂,你还得去编写很多小的包装函数和支持类。
318+
另外,除非你已经完全精通了所有底层的C接口细节,包括内存分配和错误处理机制,
319+
通常一个很小的代码缺陷、访问越界或其他类似错误就能让Python程序奔溃。
320+
321+
作为 ``ctypes`` 的一个替代,你还可以考虑下CFFI。CFFI提供了很多类似的功能,
322+
但是使用C语法并支持更多高级的C代码类型。
323+
到写这本书为止,CFFI还是一个相对较新的工程,
324+
但是它的流行度正在快速上升。
325+
甚至还有在讨论在Python将来的版本中将它包含进去。因此,这个真的值得一看。
157326

158-
_mod = ctypes.cdll.LoadLibrary(_path)
159-
160-
Once a library has been loaded, you need to write statements that extract specific sym‐
161-
bols and put type signatures on them. This is what’s happening in code fragments such
162-
as this:
163-
164-
# int in_mandel(double, double, int)
165-
in_mandel = _mod.in_mandel
166-
in_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)
167-
in_mandel.restype = ctypes.c_int
168-
169-
In this code, the .argtypes attribute is a tuple containing the input arguments to a
170-
function, and .restype is the return type. ctypes defines a variety of type objects (e.g.,
171-
c_double, c_int, c_short, c_float, etc.) that represent common C data types. Attach‐
172-
ing the type signatures is critical if you want to make Python pass the right kinds of
173-
arguments and convert data correctly (if you don’t do this, not only will the code not
174-
work, but you might cause the entire interpreter process to crash).
175-
A somewhat tricky part of using ctypes is that the original C code may use idioms that
176-
don’t map cleanly to Python. The divide() function is a good example because it returns
177-
a value through one of its arguments. Although that’s a common C technique, it’s often
178-
not clear how it’s supposed to work in Python. For example, you can’t do anything
179-
straightforward like this:
180-
181-
>>> divide = _mod.divide
182-
>>> divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
183-
>>> x = 0
184-
>>> divide(10, 3, x)
185-
Traceback (most recent call last):
186-
File "<stdin>", line 1, in <module>
187-
ctypes.ArgumentError: argument 3: <class 'TypeError'>: expected LP_c_int
188-
instance instead of int
189-
>>>
190-
191-
Even if this did work, it would violate Python’s immutability of integers and probably
192-
cause the entire interpreter to be sucked into a black hole. For arguments involving
193-
pointers, you usually have to construct a compatible ctypes object and pass it in like
194-
this:
195-
196-
>>> x = ctypes.c_int()
197-
>>> divide(10, 3, x)
198-
3
199-
>>> x.value
200-
1
201-
>>>
202-
203-
Here an instance of a ctypes.c_int is created and passed in as the pointer object. Unlike
204-
a normal Python integer, a c_int object can be mutated. The .value attribute can be
205-
used to either retrieve or change the value as desired.
206-
207-
For cases where the C calling convention is “un-Pythonic,” it is common to write a small
208-
wrapper function. In the solution, this code makes the divide() function return the
209-
two results using a tuple instead:
210-
# int divide(int, int, int *)
211-
_divide = _mod.divide
212-
_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))
213-
_divide.restype = ctypes.c_int
214-
215-
def divide(x, y):
216-
rem = ctypes.c_int()
217-
quot = _divide(x,y,rem)
218-
return quot, rem.value
219-
220-
The avg() function presents a new kind of challenge. The underlying C code expects
221-
to receive a pointer and a length representing an array. However, from the Python side,
222-
we must consider the following questions: What is an array? Is it a list? A tuple? An
223-
array from the array module? A numpy array? Is it all of these? In practice, a Python
224-
“array” could take many different forms, and maybe you would like to support multiple
225-
possibilities.
226-
The DoubleArrayType class shows how to handle this situation. In this class, a single
227-
method from_param() is defined. The role of this method is to take a single parameter
228-
and narrow it down to a compatible ctypes object (a pointer to a ctypes.c_double, in
229-
the example). Within from_param(), you are free to do anything that you wish. In the
230-
solution, the typename of the parameter is extracted and used to dispatch to a more
231-
specialized method. For example, if a list is passed, the typename is list and a method
232-
from_list() is invoked.
233-
For lists and tuples, the from_list() method performs a conversion to a ctypes array
234-
object. This looks a little weird, but here is an interactive example of converting a list to
235-
a ctypes array:
236-
237-
>>> nums = [1, 2, 3]
238-
>>> a = (ctypes.c_double * len(nums))(*nums)
239-
>>> a
240-
<__main__.c_double_Array_3 object at 0x10069cd40>
241-
>>> a[0]
242-
1.0
243-
>>> a[1]
244-
2.0
245-
>>> a[2]
246-
3.0
247-
>>>
248-
249-
For array objects, the from_array() method extracts the underlying memory pointer
250-
and casts it to a ctypes pointer object. For example:
251-
252-
>>> import array
253-
>>> a = array.array('d',[1,2,3])
254-
>>> a
255-
array('d', [1.0, 2.0, 3.0])
256-
>>> ptr_ = a.buffer_info()
257-
>>> ptr
258-
4298687200
259-
>>> ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
260-
<__main__.LP_c_double object at 0x10069cd40>
261-
>>>
262-
263-
The from_ndarray() shows comparable conversion code for numpy arrays.
264-
By defining the DoubleArrayType class and using it in the type signature of avg(), as
265-
shown, the function can accept a variety of different array-like inputs:
266-
267-
>>> import sample
268-
>>> sample.avg([1,2,3])
269-
2.0
270-
>>> sample.avg((1,2,3))
271-
2.0
272-
>>> import array
273-
>>> sample.avg(array.array('d',[1,2,3]))
274-
2.0
275-
>>> import numpy
276-
>>> sample.avg(numpy.array([1.0,2.0,3.0]))
277-
2.0
278-
>>>
279-
280-
The last part of this recipe shows how to work with a simple C structure. For structures,
281-
you simply define a class that contains the appropriate fields and types like this:
282-
283-
class Point(ctypes.Structure):
284-
_fields_ = [('x', ctypes.c_double),
285-
('y', ctypes.c_double)]
286-
287-
Once defined, you can use the class in type signatures as well as in code that needs to
288-
instantiate and work with the structures. For example:
289-
290-
>>> p1 = sample.Point(1,2)
291-
>>> p2 = sample.Point(4,5)
292-
>>> p1.x
293-
1.0
294-
>>> p1.y
295-
2.0
296-
>>> sample.distance(p1,p2)
297-
4.242640687119285
298-
>>>
299-
300-
A few final comments: ctypes is a useful library to know about if all you’re doing is
301-
accessing a few C functions from Python. However, if you’re trying to access a large
302-
library, you might want to look at alternative approaches, such as Swig (described in
303-
Recipe 15.9) or Cython (described in Recipe 15.10).
304-
305-
The main problem with a large library is that since ctypes isn’t entirely automatic, you’ll
306-
have to spend a fair bit of time writing out all of the type signatures, as shown in the
307-
example. Depending on the complexity of the library, you might also have to write a
308-
large number of small wrapper functions and supporting classes. Also, unless you fully
309-
understand all of the low-level details of the C interface, including memory management
310-
and error handling, it is often quite easy to make Python catastrophically crash with a
311-
segmentation fault, access violation, or some similar error.
312-
As an alternative to ctypes, you might also look at CFFI. CFFI provides much of the
313-
same functionality, but uses C syntax and supports more advanced kinds of C code. As
314-
of this writing, CFFI is still a relatively new project, but its use has been growing rapidly.
315-
There has even been some discussion of including it in the Python standard library in
316-
some future release. Thus, it’s definitely something to keep an eye on.

0 commit comments

Comments
 (0)