自定义类型示例#

这是一个在PyMongo中使用自定义类型的示例。这里的例子展示了如何子类化 TypeCodec 编写用于填充 TypeRegistry . 然后,可以使用类型注册表创建一个自定义类型感知 Collection . 在文档保存到MongoDB或从MongoDB检索文档时,针对生成的集合对象发出的读写操作透明地操作文档。

设置#

我们将从获取一个干净的数据库开始,以供示例使用:

>>> from pymongo import MongoClient
>>> client = MongoClient()
>>> client.drop_database("custom_type_example")
>>> db = client.custom_type_example

由于该示例的目的是演示如何使用自定义类型,因此我们需要使用一个自定义数据类型。对于本例,我们将使用 Decimal 从Python的标准库中输入。因为BSON类库的 Decimal128 类型(实现ieee754 decimal128基于十进制的浮点编号格式)不同于Python的内置 Decimal 类型,尝试保存 Decimal 在PyMongo中,结果是 InvalidDocument 例外。

>>> from decimal import Decimal
>>> num = Decimal("45.321")
>>> db.test.insert_one({"num": num})
Traceback (most recent call last):
...
bson.errors.InvalidDocument: cannot encode object: Decimal('45.321'), of type: <class 'decimal.Decimal'>

这个 TypeCodec 等级#

在 3.8 版本加入.

为了对自定义类型进行编码,我们必须首先定义 类型编解码器 对于那种类型。类型编解码器描述自定义类型的实例可以是 转化 到和/或从其中一种类型 bson 已经明白了。根据所需的功能,用户在定义类型编解码器时必须从以下基类中进行选择:

  • TypeEncoder :子类化此项以定义将自定义Python类型编码为已知BSON类型的编解码器。用户必须实现 python_type 属性/属性和 transform_python 方法。

  • TypeDecoder :子类化此项以定义将指定的BSON类型解码为自定义Python类型的编解码器。用户必须实现 bson_type 属性/属性和 transform_bson 方法。

  • TypeCodec :子类化此项以定义既可以对自定义类型进行编码又可以对自定义类型进行解码的编解码器。用户必须实现 python_typebson_type 属性/属性,以及 transform_pythontransform_bson 方法。

自定义类型的类型编解码器只需定义 Decimal 实例可以转换为 Decimal128 实例,反之亦然。由于我们对自定义类型的编码和解码都感兴趣,所以我们使用 TypeCodec 定义编解码器的基类:

>>> from bson.decimal128 import Decimal128
>>> from bson.codec_options import TypeCodec
>>> class DecimalCodec(TypeCodec):
...     python_type = Decimal  # the Python type acted upon by this type codec
...     bson_type = Decimal128  # the BSON type acted upon by this type codec
...     def transform_python(self, value):
...         """Function that transforms a custom type value into a type
...         that BSON can encode."""
...         return Decimal128(value)
...     def transform_bson(self, value):
...         """Function that transforms a vanilla BSON type value into our
...         custom type."""
...         return value.to_decimal()
...
>>> decimal_codec = DecimalCodec()

这个 TypeRegistry 等级#

在 3.8 版本加入.

在开始对自定义类型对象进行编码和解码之前,必须首先将相应的编解码器通知PyMongo。通过创建一个 TypeRegistry 实例:

>>> from bson.codec_options import TypeRegistry
>>> type_registry = TypeRegistry([decimal_codec])

请注意,类型注册表可以用任意数量的类型编解码器实例化。一旦实例化,注册中心是不可变的,向注册中心添加编解码器的唯一方法就是创建一个新的。

把它放在一起#

最后,我们可以定义 CodecOptions 我们的实例 type_registry 用它来获得 Collection 对象可以理解 Decimal 数据类型:

>>> from bson.codec_options import CodecOptions
>>> codec_options = CodecOptions(type_registry=type_registry)
>>> collection = db.get_collection("test", codec_options=codec_options)

现在,我们可以无缝地对 Decimal

>>> collection.insert_one({"num": Decimal("45.321")})
InsertOneResult(ObjectId('...'), acknowledged=True)
>>> mydoc = collection.find_one()
>>> import pprint
>>> pprint.pprint(mydoc)
{'_id': ObjectId('...'), 'num': Decimal('45.321')}

我们可以通过创建一个新的collection对象而不使用定制的codec选项并使用它来查询MongoDB,可以看到实际保存到数据库中的内容:

>>> vanilla_collection = db.get_collection("test")
>>> pprint.pprint(vanilla_collection.find_one())
{'_id': ObjectId('...'), 'num': Decimal128('45.321')}

编码子类型#

考虑一下除了编码之外 Decimal ,我们还需要对子类的类型进行编码 Decimal . PyMongo会自动对从Python类型继承的类型执行此操作,默认情况下这些类型是BSON可编码的,但是上面描述的类型编解码器系统没有提供相同的灵活性。

考虑这个子类型 Decimal 它有一个将其值返回为整数的方法:

>>> class DecimalInt(Decimal):
...     def my_method(self):
...         """Method implementing some custom logic."""
...         return int(self)
...

如果我们尝试保存此类型的实例而不首先为其注册类型编解码器,则会收到错误:

>>> collection.insert_one({"num": DecimalInt("45.321")})
Traceback (most recent call last):
...
bson.errors.InvalidDocument: cannot encode object: Decimal('45.321'), of type: <class 'decimal.Decimal'>

为了继续,我们必须为定义一个类型编解码器 DecimalInt . 这很简单,因为与用于 Decimal 足够编码 DecimalInt 也:

>>> class DecimalIntCodec(DecimalCodec):
...     @property
...     def python_type(self):
...         """The Python type acted upon by this type codec."""
...         return DecimalInt
...
>>> decimalint_codec = DecimalIntCodec()

备注

没有额外的信息解码是不可能的,因为没有额外的信息就不可能被解码 Decimal128 值需要解码为 Decimal 它需要被解码为 DecimalInt . 此示例只考虑用户希望 编码 包含这两种类型之一的文档。

在创建了一个新的codec options对象并使用它来获取一个collection对象之后,我们可以无缝地对 DecimalInt

>>> type_registry = TypeRegistry([decimal_codec, decimalint_codec])
>>> codec_options = CodecOptions(type_registry=type_registry)
>>> collection = db.get_collection("test", codec_options=codec_options)
>>> collection.drop()
>>> collection.insert_one({"num": DecimalInt("45.321")})
InsertOneResult(ObjectId('...'), acknowledged=True)
>>> mydoc = collection.find_one()
>>> pprint.pprint(mydoc)
{'_id': ObjectId('...'), 'num': Decimal('45.321')}

请注意 transform_bson 方法将这些值解码为 Decimal (而不是) DecimalInt

译码 Binary 类型#

解码处理 Binary 类型 subtype = 0bson 根据所使用的Python运行时的版本,模块略有不同。在编写 TypeDecoder 修改此数据类型的解码方式。

在python3.x上, Binary 数据 (subtype = 0 )被解码为 bytes 实例:

>>> # On Python 3.x.
>>> from bson.binary import Binary
>>> newcoll = db.get_collection("new")
>>> newcoll.insert_one({"_id": 1, "data": Binary(b"123", subtype=0)})
>>> doc = newcoll.find_one()
>>> type(doc["data"])
bytes

在Python2.7.x上,相同的数据被解码为 Binary 实例:

>>> # On Python 2.7.x
>>> newcoll = db.get_collection("new")
>>> doc = newcoll.find_one()
>>> type(doc["data"])
bson.binary.Binary

因此,用户必须设置 bson_type 他们的属性 TypeDecoder 类不同,这取决于所使用的python版本。

备注

对于需要与python2和python2和python3兼容的代码基,必须为这两种情况注册类型解码器 bson_type 价值观。

这个 fallback_encoder 可赎回的#

在 3.8 版本加入.

除了类型编解码器之外,用户还可以注册一个callable来对BSON无法识别且尚未注册类型编解码器的类型进行编码。这个电话是 回退编码器 就像 transform_python 方法时,它接受不可编码的值作为参数,并返回BSON可编码值。以下回退编码器对python的 Decimal 键入a Decimal128

>>> def fallback_encoder(value):
...     if isinstance(value, Decimal):
...         return Decimal128(value)
...     return value
...

声明回调后,必须使用此回退编码器创建类型注册表和编解码器选项,然后才能将其用于初始化集合:

>>> type_registry = TypeRegistry(fallback_encoder=fallback_encoder)
>>> codec_options = CodecOptions(type_registry=type_registry)
>>> collection = db.get_collection("test", codec_options=codec_options)
>>> collection.drop()

我们现在可以无缝地编码 Decimal

>>> collection.insert_one({"num": Decimal("45.321")})
InsertOneResult(ObjectId('...'), acknowledged=True)
>>> mydoc = collection.find_one()
>>> pprint.pprint(mydoc)
{'_id': ObjectId('...'), 'num': Decimal128('45.321')}

备注

将调用回退编码器 之后 尝试使用标准BSON编码器和任何配置的类型编码器对给定值进行编码失败。因此,在配置了类型编码器和回退编码器的类型注册表中,类型编码器和回退编码器都以相同的自定义类型为目标,则类型编码器中指定的行为将优先。

因为回退编码器不需要事先声明它们编码的类型,所以它们可以用来支持那些不能由提供服务的有趣的用例 TypeEncoder . 下一节将描述一个这样的用例。

编码未知类型#

在这个例子中,我们演示了如何使用回退编码器将任意对象保存到数据库中。我们将使用标准库的 pickle 对于未知的类型,picklable方法很自然地只适用于未知类型。

我们首先定义一些任意的自定义类型:

class MyStringType(object):
    def __init__(self, value):
        self.__value = value

    def __repr__(self):
        return "MyStringType('%s')" % (self.__value,)


class MyNumberType(object):
    def __init__(self, value):
        self.__value = value

    def __repr__(self):
        return "MyNumberType(%s)" % (self.__value,)

我们还定义了一个回退编码器,它将接收到的任何对象进行pickle并将其返回为 Binary 具有自定义子类型的实例。反过来,定制子类型允许我们编写一个TypeDecoder,在检索时识别pickled工件,并透明地将它们解码回Python对象:

import pickle
from bson.binary import Binary, USER_DEFINED_SUBTYPE


def fallback_pickle_encoder(value):
    return Binary(pickle.dumps(value), USER_DEFINED_SUBTYPE)


class PickledBinaryDecoder(TypeDecoder):
    bson_type = Binary

    def transform_bson(self, value):
        if value.subtype == USER_DEFINED_SUBTYPE:
            return pickle.loads(value)
        return value

备注

上面的例子是假设使用python3编写的。如果您使用的是python2, bson_type 必须设置为 Binary . 见 译码 Binary 类型 详细解释。

最后,我们创建一个 CodecOptions 实例:

codec_options = CodecOptions(
    type_registry=TypeRegistry(
        [PickledBinaryDecoder()], fallback_encoder=fallback_pickle_encoder
    )
)

我们现在可以将自定义对象往返到MongoDB:

collection = db.get_collection("test_fe", codec_options=codec_options)
collection.insert_one(
    {"_id": 1, "str": MyStringType("hello world"), "num": MyNumberType(2)}
)
mydoc = collection.find_one()
assert isinstance(mydoc["str"], MyStringType)
assert isinstance(mydoc["num"], MyNumberType)

局限性#

PyMongo的类型编解码器和回退编码器功能具有以下限制:

  1. 用户不能自定义PyMongo已经理解的Python类型的编码行为 intstr (“内置类型”)。尝试使用一个或多个作用于内置类型的编解码器实例化类型注册表将导致 TypeError . 此限制扩展到标准类型的所有子类型。

  2. 不支持链接类型编码器。自定义类型值,一旦由编解码器的 transform_python 方法, must 导致一个默认情况下是BSON可编码的类型,或者可以由回退编码器转换成BSON可编码的类型——它 不能 通过不同类型的编解码器进行第二次转换。

  3. 这个 command() 方法在解码命令响应文档时不应用用户的TypeDecoders。

  4. gridfs 不将自定义类型编码或解码应用于从用户接收或返回给用户的任何文档。