使用C语言编写Python扩展1——Hello World

能够使用C语言编写扩展是Python一大卖点吧,这可以将一些关键的代码使用C来写以提升程序的性能。本文是参考了Python的官方文档整理而来的,同时结合了Python2跟Python3。按照惯例现在先从一个Hello World开始讲解一下写扩展的基本流程。

详细的内容可以参考官方文档:

https://docs.python.org/2.7/extending/index.html

https://docs.python.org/3/extending/index.html

https://docs.python.org/2.7/c-api/index.html

https://docs.python.org/3/c-api/index.html

同时本文中的示例代码可从 https://github.com/wusuopu/python-c-extension-sample 获取到。

首先介绍一下我当前的开发环境:

  • ArchLinux
  • gcc 4.8.2
  • glibc 2.19
  • Python 2.7.6
  • Python 3.3.5


    开始


    先创建一个新的C代码文件 lc_hello.c。为了能够正常使用python的api,需要导入Python.h这个头文件。


    #include <Python.h>

    然后再定义一个模块的初始化函数。


    PyMODINIT_FUNC initlc_hello_world(void)
    {
    Py_InitModule(“lc_hello_world”, lc_hello_world_methods);
    printf(“init lc_hello_world module\n”);
    }

    这个函数是用于模块初始化的,即是在第一次使用import语句导入模块时会执行。其函数名必须为initmodule_name这样的格式,在这里我们的模块名为lc_hello_world,所以函数名就是initlc_hello_world。

    在这个函数中又调用了Py_InitModule函数,它执行了模块初始化的操作。Py_InitModule函数传入了两个参数,第一个参数为字符串,表示模块的名称;第二个参数是一个PyMethodDef的结构体数组,表示该模块都具有哪些方法。与Py_InitModule相似的方法还有Py_InitModule3和Py_InitModule4。因此在initlc_hello_world方法之前还需要先定义 lc_hello_world_methods 数组。


    static PyMethodDef lc_hello_world_methods[] = {
    {“test”, (PyCFunction)test_function, METH_NOARGS, “lc_hello_world extending test”},
    {“add”, (PyCFunction)add_function, METH_VARARGS, NULL},
    {NULL, NULL, 0, NULL}
    };

    PyMethodDef结构体有四个字段。

    • 第一个是一个字符串,表示在Python中对应的方法的名称;
    • 第二个是对应的C代码的函数;
    • 第三个是一个标致位,表示该Python方法是否需要参数,METH_NOARGS表示不需要参数,METH_VARARGS表示需要参数;
    • 第四个是一个字符串,它是该方法的doc属性,这个不是必须的,可以为NULL。

      PyMethodDef结构体数组最后以 {NULL, NULL, 0, NULL}结尾。(感觉好像不是必须的,但是通常都这么做那我们也这么做吧)


      注意:以上的用法都是针对Python2的,在Python3中又有些不同。

      在Python3中模块的初始化函数的函数名变为了PyInit_module_name这样的形式了,因此这里就需要定义一个函数 PyMODINIT_FUNC PyInit_lc_hello_world。并且还需要返回一个 module 类型的变量。

      其次在Python3中创建module对象的函数也由 Py_InitModule 变为了 PyModule_Create。

      因此在Python3中模块的初始化函数应该定义如下:


      PyMODINIT_FUNC PyInit_lc_hello_world(void)
      {
      PyObject m;
      m = PyModule_Create(&lc_hello_world_module);
      if (m == NULL)
      return NULL;
      printf(“init lc_hello_world module\n”);
      return m;
      }

      PyModule_Create函数需要传入一个 PyModuleDef 类型的指针。

      因此在此之前还需要先定义 lc_hello_world_module 变量。


      static struct PyModuleDef lc_hello_world_module = {
      PyModuleDef_HEAD_INIT,
      “lc_hello_world”, /
      name of module /
      NULL, /
      module documentation, may be NULL /
      -1, /
      size of per-interpreter state of the module, or -1 if the module keeps state in global variables. /
      lc_hello_world_methods /
      A pointer to a table of module-level functions, described by PyMethodDef values. Can be NULL if no functions are present. /
      };

      在 lc_hello_world_methods 中我们为模块指定了两个方法,接下来我们需要实现这两个方法。


      static PyObject test_function(PyObject self)
      {
      PyObject_Print(self, stdout, 0);
      printf(“lc_hello_world test\n”);
      Py_INCREF(Py_True);
      return Py_True;
      }

      这段代码定义了Python的test方法所对应的C函数。在这个函数中就只执行了一条printf语句,然后就返回了Py_True。

      Py_True即是Python中的True,Py_INCREF函数执行的操作是对Python对象的计数引用值进行加1。与Py_INCREF对应的是Py_DECREF,它是对计数引用减1,并且计数引用为0时就销毁对象并回收内存。


      static PyObject add_function(PyObject self, PyObject args)
      {
      int num1, num2;
      PyObject result=NULL;
      if (!PyArg_ParseTuple(args, “nn”, &num1, &num2)) {
      printf(“传入参数错误!\n”);
      return NULL;
      }
      result = PyInt_FromLong(num1+num2);
      return result;
      }

      这须代码定义了Python的add方法所对应的C函数。该函数需要传入两个整数类型的参数。

      PyArg_ParseTuple是对传入的参数进行解析,关于这个函数的说明请查看Python手册。


      注意:在Python3中整数都是 long 类型的,因此这里的 PyInt_FromLong 需要改为 PyLong_FromLong,其作用是将C的int类型转为Python的int类型。


      编译


      扩展模块编写完成后,接下来就是对其进行编译了。先编写一个 setup.py 脚本。


      #!/usr/bin/env python
      #-
      - coding:utf-8 -*-

from setuptools import setup, Extension

hello_world = Extension(‘lc_hello_world’, sources=[“lc_hello.c”])
setup(ext_modules=[hello_world])

然后再执行命令进行编译:

$ python setup.py build

执行成功后会在当前目录下的build目录中生成扩展模块文件。

测试

最后就是编写一个小程序来测试刚刚的模块是否可用。

import lc_hello_world

print(lc_hello_world.test.__doc__)
print(lc_hello_world.add.__doc__)
print(lc_hello_world.test())
print(lc_hello_world.add(1, 2))
print(lc_hello_world.add(1, '2'))    # 这个会报错

mongoengine教程(5)——信号

MongoEngine在进行数据操作时会发出一些信号,我们可以连接这些信号进行一些额外的操作。注意:要在MongoEngine中使用信号,需要安装 blinker 这个库。


$ pip install blinker

MongoEngine提供的信号如下:



  • pre_init: 在创建一个新的 Document 或者 EmbeddedDocument 实例对象之后,并且对象初始化之前调用。

  • post_init:在 Document 或者 EmbeddedDocument 实例对象初始化完成之后调用。

  • pre_save:在 save 方法执行之前调用。

  • pre_save_post_validation:在数据检验完成之后,数据保存之前调用。

  • post_save:在数据保存完成之后调用。

  • pre_delete:在 delete 方法执行之前调用。

  • post_delete:在记录成功删除之后调用。

  • pre_bulk_insert:在数据检验之后,数据插入之前调用。

  • post_bulk_insert:在数据成功插入之后调用。


事件连接


使用 signals 将信号与回调函数进行连接。


from mongoengine import 
from mongoengine import signals

class Author(Document):
name = StringField()

@classmethod
def pre_save(cls, sender, document, *kwargs):
print(“Pre Save: %s” % document.name)

@classmethod
def post_save(cls, sender, document,
kwargs):
print(“Post Save: %s” % document.name)
if ‘created’ in kwargs:
if kwargs[‘created’]:
print(“Created”)
else:
print(“Updated”)

signals.pre_save.connect(Author.pre_save, sender=Author)
signals.post_save.connect(Author.post_save, sender=Author)

注意:对于 RefereneField 的reverse_delete_rules参数不会触发信号。

mongoengine教程(4)——文件存储

MongoDB的GridFS支持直接在数据库中存储文件。要在MongoEngine中使用GridFS,只要使用 FileField 对象即可。以下是一个例子:

class Animal(Document):
    genus = StringField()
    family = StringField()
    photo = FileField()

marmot = Animal(genus='Marmota', family='Sciuridae')

marmot_photo = open('gtk.png', 'rb')
marmot.photo.put(marmot_photo, content_type = 'image/png')
marmot.save()

这个例子将 gtk.png 这个图片存入了数据库中。
文件的读取也很简单:

marmot = Animal.objects(genus='Marmota').first()
photo = marmot.photo.read()
content_type = marmot.photo.content_type

FileField不仅可以存储文件,还可以用来存储数据流。只是操作上略微不同。

要存储数据流,首先先创建一个新的文件,然后再往里面写入数据。

marmot.photo.new_file()
marmot.photo.write('some_image_data')
marmot.photo.write('some_more_image_data')
marmot.photo.close()
marmot.save()

如果要删除存储在数据库中的文件,只需要调用该文件对象的 delete 方法:

marmot.photo.delete()

注意:一条文档记录中的FileField字段只保存了对GridFS集合中该文件的ID引用。这意味着如果该文档被删除了,对用的文件不会被删除。因此在删除这类文档时需要小心,以免出现孤立文件。

对于已存储的文件可以进行替换修改:

another_marmot = open('python.png', 'rb')
marmot.photo.replace(another_marmot, content_type='image/png')

mongoengine教程(3)——数据查询

与Django一样,Document类都有一个 objects 属性。它用于将类与数据库关联起来。objects属性是一个QuerySetManager类型的对象,它的操作会返回一个QuerySet类型的对象。可以通过对QuerySet对象的迭代获取数据库中的数据。

class User(Document):
    name = StringField()
    country = StringField()

class Paper(Document):
    content = StringField()
    author = ReferenceField(User)

查询过滤

可以在查询是指定过滤条件以获取想要的结果。例如想要查询英国的用户:

uk_users = User.objects(country='uk')

与Django类似,要查询引用的对象只需要使用双下划线即可。例如想要查询英国用户的论文:

uk_papers = Paper.objects(author__country='uk')

查询操作

与Django类似,MongoEngine同样也提供了一些条件语句。

  • ne - 不相等
  • lt - 小于
  • lte - 小于等于
  • gt - 大于
  • gte - 大于等于
  • not - 取反
  • in - 值在列表中
  • nin - 值不在列表中
  • mod - 取模
  • all - 与列表的值相同
  • size - 数组的大小
  • exists - 字段的值存在

例如查询年龄小于等于18岁的用户:

young_users = Users.objects(age__lte=18)

对于不同类型的数据提供了不同的条件语句。

查询结果个数限制

跟传统的ORM一样,MongoEngine也可以限制查询结果的个数。一种方法是在QuerySet对象上调用limit和skip方法;另一种方法是使用数组的分片的语法。例如:

users = User.objects[10:15]
users = User.objects.skip(10).limit(5)

聚合操作

MongoEngine提供了一些数据库的聚合操作。

统计结果个数即可以使用QuerySet的count方法,也可以使用Python风格的方法:

num_users = len(User.objects)
num_users = User.objects.count()

其他的一些聚合操作。
求和:

yearly_expense = Employee.objects.sum('salary')

求平均数:

mean_age = User.objects.average('age')

高级查询

有时需要将多个条件进行组合,前面提到的方法就不能满足需求了。这时可以使用MongoEngine的Q类。它可以将多个查询条件进行 &(与) 和 |(或) 操作。

例如下面的语句是查询所有年龄大于等于18岁的英国用户,或者所有年龄大于等于20岁的用户。

User.objects((Q(country='uk') & Q(age__gte=18)) | Q(age__gte=20))

在服务器端执行javascript代码

通过MongoEngine QuerySet对象的 exec_js 方法可以将javascript代码作为字符串发送给服务器端执行,然后返回执行的结果。

例如查询该数据库都有那些集合:

User.objects.exec_js("db.getCollectionNames()")

mongoengine教程(2)——文档模式

在MongoDB中一个文档(document)与关系型数据库中的一行(row)相似;文档保存在集合(collection)中,行保存在表(table)中。


定义文档的模式


与django类似,要定义一个文档模式只需要创建一个类继承自 Document,并添加一些 Field 对象。


from mongoengine import *
import datetime

class Page(Document):
title = StringField(max_length=200, required=True)
date_modified = DateTimeField(default=datetime.datetime.now)

如上定义了一个文档模式具有 title和date_modified 两个字段。


同时MongoDB本身就是无模式的,因此我们还可以创建动态的文档模式。它可以在添加数据时为不同的数据设置不同的字段。


class Page(DynamicDocument):
title = StringField(max_length=200, required=True)

添加数据:


page = Page(title=’Using MongoEngine’)
page.tags = [‘mongodb’, ‘mongoengine’]
page.save()

文档字段


文档字段(Field)不是必需的,但是使用它来进行数据验证、设置默认值等操作会比较方便。


MongoEngine提供了如下这些类型的Field:



  • BinaryField

  • BooleanField

  • ComplexDateTimeField

  • DateTimeField

  • DecimalField

  • DictField

  • DynamicField

  • EmailField

  • EmbeddedDocumentField

  • FileField

  • FloatField

  • GenericEmbeddedDocumentField

  • GenericReferenceField

  • GeoPointField

  • ImageField

  • IntField

  • ListField

  • MapField

  • ObjectIdField

  • ReferenceField

  • SequenceField

  • SortedListField

  • StringField

  • URLField

  • UUIDField


文档之间引用关系


在关系型数据库中多个表可以使用外键进行关联。然而MongoDB是无模式的,因此想要达到这样的效果就这能在应用程序中自己手动的进行关联了。


不过还好,使用MongoEngine的ReferenceField可以很方便的实现。


class User(Document):
name = StringField()

class Page(Document):
content = StringField()
author = ReferenceField(User)

john = User(name=”John Smith”)
john.save()

post = Page(content=”Test Page”)
post.author = john
post.save()

一对多的关系


对于一对多的关系可以使用ListField来保存一个ReferenceField列表。在进行查询操作是需要传入一个实例对象。


class User(Document):
name = StringField()

class Page(Document):
content = StringField()
authors = ListField(ReferenceField(User))

bob = User(name=”Bob Jones”).save()
john = User(name=”John Smith”).save()

Page(content=”Test Page”, authors=[bob, john]).save()
Page(content=”Another Page”, authors=[john]).save()

# Find all pages Bob authored
Page.objects(authors__in=[bob])

引用对象的删除操作


MongoDB默认不会检查数据的完整性,因此在删除一个对象是就需要自己手动的处理引用了该对象的其他对象。


同样的MongoEngine也提供了一样的功能。ReferenceField有一个 reverse_delete_rule 参数可以进行设置。它的取值如下:



  • mongoengine.DO_NOTHING:默认就是这个值,它不会进行任何操作。

  • mongoengine.DENY:如果该对象还被其他对象引用,则拒绝删除。

  • mongoengine.NULLIFY:将其他对象对该对象的引用字段设为null。

  • mongoengine.CASCADE:将引用了该对象的其他对象也删除掉。

  • mongoengine.PULL:移除对该对象的引用。


索引


与django的Model相似,MongoEngine的Document也可以在meta属性中设置索引。


class Page(Document):
title = StringField()
rating = StringField()
meta = {
‘indexes’: [‘title’, (‘title’, ‘-rating’)]
}

meta中的indexes可以是一个列表,也可以是一个字典。

mongoengine教程(1)——概述

MongoEngine是MongoDB的一个ODM(Object-Document Mapper)框架,它提供了类似Django的语法来操作MongoDB数据库。


安装


安装 MongoEngine 需要先安装 PyMongo。


使用pip安装


$ [sudo] pip install mongoengine

通过源代码安装


先从 PyPi 或者 Github 下载源代码。然后再进行安装。


$ [sudo] python setup.py install

使用


首先启动 mongodb 服务器:


$ mongod

连接服务器


使用 connect 方法进行数据库链接,与pymongo的用法相似,其参数可以是多种型式的。


from mongoengine import connect
connect(‘project1’)
connect(‘project1’, host=’mongodb://localhost:27017/test_database’)

从 MongoEngine 0.6 开始增加了多数据库的支持, connect 的第二个参数可以为每个链接设置一个别名。


定义数据模型


mongoengine的 Document 与django的 Model 相似。


class User(mongoengine.Document):
name = mongoengine.StringField()

meta = {“db_alias”: “default”}

数据操作


数据的添加过程也与django相似:


User.objects.create(name=”test1”)
User.objects.create(name=”test2”)
User(name=”test3”).save()

查询数据:


User.objects.filter(name=”test2”)

删除数据:


User.objects.filter(name=”test2”).delete()

MongoEngine虽然提供了ODM,但是我们同样还是可以直接对数据库进行操作。

获取 pymongo 的 collection 对象:


User.objects._collection

然后就可以使用原生的pymongo操作了。

pymongo教程(3)——自定义数据类型

pymongo提供一些常用的数据类型,如:数据、字符串、日期等。如果感觉还不能满足需求,那么还可以自定义数据类型。

首先定义一个类:

class Custom(object):
    def __init__(self, x):
        self.__x = x

    def x(self):
        return self.__x

要将自定义类型的数据存入数据库中需要先进行编码;将数据从数据库读取出来后又需要再解码。

手动编码/解码

我们可以定义两个方法,在插入和查询数据时进行手动的编码、解码。

def encode_custom(custom):
    return {"_type": "custom", "x": custom.x()}

def decode_custom(document):
    assert document["_type"] == "custom"
    return Custom(document["x"])

print(db.test.insert({"custom": encode_custom(Custom(5))}))
print(db.test.find_one()['custom'])

自动编码/解码

手动地进行编码虽然可行,但是还是不太方便。我们还可以使用 SONManipulator 进行自动编码。

from pymongo.son_manipulator import SONManipulator
class Transform(SONManipulator):
    def transform_incoming(self, son, collection):
        for (key, value) in son.items():
            if isinstance(value, Custom):
                son[key] = encode_custom(value)
            elif isinstance(value, dict): # Make sure we recurse into sub-docs
                son[key] = self.transform_incoming(value, collection)
        return son

    def transform_outgoing(self, son, collection):
        for (key, value) in son.items():
            if isinstance(value, dict):
                if "_type" in value and value["_type"] == "custom":
                    son[key] = decode_custom(value)
                else: # Again, make sure to recurse into sub-docs
                    son[key] = self.transform_outgoing(value, collection)
        return son

db.add_son_manipulator(Transform())
print(db.test.insert({"custom": Custom(5)}))
print(db.test.find_one())

二进制编码

我们也可以将其编码成二进制进行存储。

from bson.binary import Binary
def to_binary(custom):
    return Binary(str(custom.x()), 128)

def from_binary(binary):
    return Custom(int(binary))

class TransformToBinary(SONManipulator):
    def transform_incoming(self, son, collection):
        for (key, value) in son.items():
            if isinstance(value, Custom):
                son[key] = to_binary(value)
            elif isinstance(value, dict):
                son[key] = self.transform_incoming(value, collection)
        return son

    def transform_outgoing(self, son, collection):
        for (key, value) in son.items():
            if isinstance(value, Binary) and value.subtype == 128:
                son[key] = from_binary(value)
            elif isinstance(value, dict):
                son[key] = self.transform_outgoing(value, collection)
        return son

db.add_son_manipulator(TransformToBinary())
print(db.test.insert({"custom": Custom(5)}))
print(db.test.find_one())

pymongo教程(2)——聚合操作

在MongoDB中常用的聚合操作有 aggregation、map/reduce和group 。


首先先添加一些测试数据:


db.things.insert({“x”: 1, “tags”: [“dog”, “cat”]})
db.things.insert({“x”: 2, “tags”: [“cat”]})
db.things.insert({“x”: 2, “tags”: [“mouse”, “cat”, “dog”]})
db.things.insert({“x”: 3, “tags”: []})

aggregation


以下例子是统计 tags 字段内的各个值的出现的次数。


from bson.son import SON
db.things.aggregate([
{“$unwind”: “$tags”},
{“$group”: {“_id”: “$tags”, “count”: {“$sum”: 1}}},
{“$sort”: SON([(“count”, -1), (“_id”, -1)])}
])

{‘ok’: 1.0, ‘result’: [{‘count’: 3, ‘_id’: ‘cat’}, {‘count’: 2, ‘_id’: ‘dog’}, {‘count’: 1, ‘_id’: ‘mouse’}]}

注意:aggregate操作要求服务器程序为 2.1.0 以上的版本。PyMongo 驱动程序为 2.3 以上的版本。


Map/Reduce


上面的操作同样也可以使用 Map/Reduce 完成。


from bson.code import Code
mapper = Code(“””
function () {
this.tags.forEach(function(z) {
emit(z, 1);
});
}
“””)

reducer = Code(“””
function (key, values) {
var total = 0;
for (var i = 0; i < values.length; i++) {
total += values[i];
}
return total;
}
“””)

result = db.things.map_reduce(mapper, reducer, “myresults”)
for doc in result.find():
print(doc)

{u’_id’: u’cat’, u’value’: 3.0}
{u’_id’: u’dog’, u’value’: 2.0}
{u’_id’: u’mouse’, u’value’: 1.0}

map和reduce都是一个javascript的函数; map_reduce 方法会将统计结果保存到一个临时的数据集合中。


Group


group 操作与SQL的 GROUP BY 相似,同时比 Map/Reduce 要简单。


reducer = Code(“””
function(obj, prev){
prev.count++;
}
“””)

results = db.things.group(key={“x”:1}, condition={}, initial={“count”: 0}, reduce=reducer)
for doc in results:
print(doc)

{‘x’: 1.0, ‘count’: 1.0}
{‘x’: 2.0, ‘count’: 2.0}
{‘x’: 3.0, ‘count’: 1.0}

注意:在MongoDB的集群环境中不支持 group 操作,可以使用 aggregation 或者 map/reduce 代替。


完整的MongoDB聚合文档: http://docs.mongodb.org/manual/aggregation/

pymongo教程(1)——概述

MongoDB是使用C++开发的一款文档型数据库,PyMongo是MongoDB的Python驱动。

安装

使用pip安装

$ [sudo] pip install pymongo

如果要安装特定的版本则:

$ [sudo] pip install pymongo==2.6.3

通过源代码安装

$ git clone git://github.com/mongodb/mongo-python-driver.git pymongo
$ cd pymongo/
$ [sudo] python setup.py install

注意:使用C的扩展会对性能提升会有帮助。但是在uwsgi中会出现警告,则可以选择只安装python驱动,而不安装C扩展。

$ [sudo] python setup.py --no_ext install

注意: 如果你使用的是Python3的话,PyMongo只支持 Python 3.1以上的版本。

使用

首先启动 mongodb 服务器:

$ mongod

连接服务器

然后执行python程序连接服务器:

from pymongo import MongoClient
client = MongoClient()

以上会连接到默认的主机和端口(localhost:27017),也可以指定主机名和端口:

client = MongoClient('localhost', 27017)

或者:

client = MongoClient('mongodb://localhost:27017/')

访问数据库

db = client.test_database

如果数据库的名称不能直接使用属性名的风格访问,那么就需要使用字典的风格:

db = client['test-database']

访问数据集合

与访问数据库相似:

collection = db.test_collection
collection = db['test-collection']

插入数据

在MongoDB中数据是以类似JSON格式进行保存的,在PyMongo中则是使用字典风格。然后可以数据集合对象的 insert() 方法进行插入数据。

import datetime
post = {"author": "Mike",
        "text": "My first blog post!",
        "tags": ["mongodb", "python", "pymongo"],
        "date": datetime.datetime.utcnow()}
post_id = db.posts.insert(post)

查询数据

可以数据集合对象的 find() 方法进行查询数据。

db.posts.find({"author": "Mike"})
db.posts.find_one({"author": "Mike"})

自动生成.gitignore文件

.gitignore文件是用于对git进行设置,让其忽略对某些文件的跟踪。

最近发现每创建一个新的仓库都要把.gitignore文件重新写一遍,甚是麻烦。于是就想能否自动生成.gitignore文件,这样的话就比较方便。最后找到了 gitignore.io 这个网站,它可以根据需求生成相应的.gitignore文件。比较你是用vim编辑器编写python代码,则输入vim python就会生成对应vim和python的gitignore文件了。

为了方便使用我编写了一个shell脚本。从 https://gist.github.com/wusuopu/9408486 下载代码,保存为mkgitignore,并加上执行权限。然后执行如下命令生成.gitignore文件。

$ mkgitignore vim,python
$ mkgitignore vim,python .gitignore

第一条命令是直接将结果输出到终端,第二条命令是将结果输出到.gitignore文件中。

最后补充一个git的小知识:
如果想要在所有的项目中都忽略掉某些文件的话,那么可以设置一个全局的gitignore。执行如下命令:

$ git config --global core.excludesfile ~/.gitignore