Skip to main content

简单的对象序列化和版本控制框架

项目描述

versionedobj是一个用于创建复杂 python 对象的框架,这些对象可以在字符串、字典或 JSON 文件之间进行序列化/反序列化。

versionedobj还提供了一种版本控制机制,用于跟踪对象结构随时间的变化,并在不同对象版本之间迁移。

请参阅API 文档

<nav class="contents" id="table-of-contents" role="doc-toc">

目录

</nav>

安装

使用 pip安装versionedobj :

pip install versionedobj

入门

对象定义

通过创建一个继承自VersionedObject的新类来定义对象,并设置类属性来定义您的对象属性:

from versionedobj import VersionedObjbect

class UserConfig(VersionedObject):
    version = "v1.0.0"
    username = "john smith"
    friend_list = ["user1", "user2", "user3"]

您还可以通过简单地将另一个VersionedObject 类或实例对象分配给类属性来嵌套 VersionedObjects:

from versionedobj import VersionedObject

class DisplayConfig(VersionedObject):
    display_mode = "windowed"
    resolution = "1920x1080"
    volume = 0.66

# Populate class attributes to build your object
class UserConfig(VersionedObject):
    version = "v1.0.0"
    username = "john smith"
    friend_list = ["user1", "user2", "user3"]
    display_config = DisplayConfig() # VersionedObjects can be nested

    # Nested VersionedObjects can be a class object, or an instance of the
    # class, either way will behave the same

    # display_config = DisplayConfig

创建对象实例和访问对象属性

您在VersionedObject的类属性上设置的值用作该对象的默认值。当您创建VersionedObject类的实例时,将自动创建实例属性以匹配类属性,并且类属性的值将被复制到实例属性中:

obj = UserConfig()

print(obj.friend_list)
# Output looks like this: ["user1", "user2", "user3"]

print(obj.display_config.display_mode)
# Output looks like this: "windowed"

除了常规的点符号外,您还可以将对象实例视为 dict,并使用其完整的点名称作为键来访问各个属性:

print(obj['friend_list'])
# Output looks like this: ["user1", "user2", "user3"]

print(obj['display_config.display_mode'])
# Output looks like this: "windowed"

# Change the value of an instance attribute
obj['display_config.display_mode'] = "fullscreen"

print(obj['display_config.display_mode'])
# Output looks like this: "fullscreen"

您还可以使用object_attributes()方法遍历所有对象属性名称和值:

for attr_name, attr_value in obj.object_attributes():
    print(f"{attr_name}: {attr_value}")

# Output looks like this:
#
# version: v1.0.0
# username: john smith
# friend_list: ["user1", "user2", "user3"]
# display_config.display_mode: windowed
# display_config.resolution: 1920x1080
# display_config.volume: 0.66

序列化和反序列化

使用to_filefrom_file方法对 JSON 文件中的数据进行序列化/反序列化:

# Save object instance to JSON file
obj.to_file('user_config.json', indent=4)

# Load object instance from JSON file
obj.from_file('user_config.json')

您还可以将对象数据保存/加载为 JSON 字符串:

# Save object instance to JSON string
obj_as_json = obj.to_json(indent=4)

# Load object instance from JSON string
obj.from_json(obj_as_json)

或者,作为一个字典:

# Save object instance to dict
obj_as_dict = obj.to_dict()

# Load object instance from dict
obj.from_dict(obj_as_dict)

过滤序列化/反序列化输出

按字段名列入白名单

序列化时,如果只想输出某些字段,可以使用 'only' 参数指定应该输出哪些字段(实际上是按字段名的白名单):

cfg.to_file('user_config.json', only=['version', 'username', 'display_config.resolution'])

# Output looks like this:
#
# {
#     "version": "v1.0.0",
#     "username": "jane doe",
#     "display_config": {
#         "resolution": "1920x1080",
#     }
# }

相同的参数可用于反序列化:

cfg.from_file('user_config.json', only=['display_config.display_mode'])

# Only the 'display_config.display_mode' field is loaded from the file

按字段名列入黑名单

序列化时,如果您不想输出某些字段,可以使用 'ignore' 参数指定应从输出中排除哪些字段(实际上是按字段名称的黑名单):

cfg.to_file('user_config.json', ignore=['friend_list', 'display_config.volume'])

# Output looks like this:
#
# {
#     "version": "v1.0.0",
#     "username": "jane doe",
#     "display_config": {
#         "display_mode": "windowed",
#         "resolution": "1920x1080"
#     }
# }

相同的参数可用于反序列化:

cfg.from_file('user_config.json', ignore=['friend_list'])

# Every field except for the 'friend_list' field is loaded from the file

迁移:使用版本号

VersionedObject 对象可以有一个版本属性,它可以是任何对象,尽管它通常是一个字符串(例如"v1.2.3")。如果您需要更改对象的格式,此版本属性可用于支持旧对象的迁移。

示例场景,第 1 部分:您创建了一个漂亮的版本化对象

让我们从前面的示例中采用相同的配置文件定义:

from versionedobj import VersionedObject

# Nested config object
class DisplayConfig(VersionedObject):
    display_mode = "windowed"
    resolution = "1920x1080"
    volume = 0.66

# Top-level config object with another nested config object
class UserConfig(VersionedObject):
    version = "v1.0.0"
    username = "john smith"
    friend_list = ["user1", "user2", "user3"]
    display_config = DisplayConfig()

想象一下,您已经将此代码发布到世界各地。人们已经在使用它,并且他们的计算机上有由您的UserConfig类生成的 JSON 文件。

示例场景,第 2 部分:您更新软件,修改版本化对象

现在,假设您正在制作软件的新版本,并且一些新功能要求您对版本化对象进行以下更改:

  • 完全删除DisplayConfig.resolution字段

  • 将DisplayConfig.volume的名称更改为DisplayConfig.volumes

  • 将DisplayConfig.volumes的值从浮点数更改为列表

from versionedobj import VersionedObject

# Nested config object
class DisplayConfig(VersionedObject):
    display_mode = "windowed"
    # 'resolution' field is deleted
    volumes = [0.66, 0.1] # 'volume' is now called 'volumes', and is a list

# Top-level config object with another nested config object
class UserConfig(VersionedObject):
    version = "v1.0.0"
    username = "john smith"
    friend_list = ["user1", "user2", "user3"]
    display_config = DisplayConfig()

呃,你有问题……

现在,如果您将此更新后的 UserConfig 类发送给现有用户,它将无法加载版本为v1.0.0的现有 JSON 文件,因为这些文件将包含我们在v1.0.1中删除的DisplayConfig.resolution字段和 DisplayConfig .volume同样会消失,被 DisplayConfig.volumes取代。这种情况就是迁移的目的。

解决方案——迁移!

解决方案是:

  1. 将版本号更改为新的,例如v1.0.0变为v1.0.1

  2. 编写迁移函数,将v1.0.0对象数据转换为v1.0.1对象数据

from versionedobj import VersionedObject

# Nested config object
class DisplayConfig(VersionedObject):
    display_mode = "windowed"
    # 'resolution' field is deleted
    volumes = [0.66, 0.1] # 'volume' is now called 'volumes', and is a list

# Top-level config object with another nested config object
class UserConfig(VersionedObject):
    version = "v1.0.1" # Version has been updated to 1.0.1
    username = "john smith"
    friend_list = ["user1", "user2", "user3"]
    display_config = DisplayConfig()

# Create the migration function for v1.0.0 to v1.0.1
def migrate_100_to_101(attrs):
    del attrs['display_config']['resolution']        # Delete resolution field
    del attrs['display_config']['volume']            # Delete volume field
    attrs['display_config']['volumes'] = [0.66, 0.1] # Add defaults for new volume values
    return attrs                                     # Return modified data (important!)

# Add the migration function for v1.0.0 to v1.0.1
UserConfig.add_migration("v1.0.0", "v1.0.1", migrate_100_to_101)

添加迁移功能并将版本更新为v1.0.1后,加载并包含版本v1.0.0的 JSON 文件将使用您添加的迁移功能自动迁移到版本 v1.0.1

这种方法的缺点是,您必须手动更新版本号,并编写一个新的迁移函数,只要您的配置数据结构发生变化。

当然,好处是您可以相对轻松地支持将任何旧版本的配置文件迁移到当前版本。

如果您不需要版本控制/迁移功能,请永远不要更改您的版本号,或者不要在您的VersionedObject类上创建版本属性。

迁移:迁移未版本化的对象

您可能会遇到发布未版本化对象的情况,但随后您需要进行更改,并将未版本化对象迁移到版本化对象。

对于“from_version”参数,这可以通过将“None”传递给“add_migration()”方法来简单地处理。例如:

from versionedobj import VersionedObj

class UserConfig(VersionedObject):
    version = "v1.1.0"
    username = ""
    friend_list = []

def migrate_none_to_100(attrs);
    attrs['friend_list'] = [] # Add new 'friend_list' field
    return attrs

UserConfig.add_migration(None, "v1.0.0", migrate_none_to_100)

Migrations:迁移功能的装饰器

如果您愿意,可以在迁移函数上使用 versionedobj.migration装饰器,而不是调用add_migration()类方法:

from versionedobj import VersionedObj, migration

class UserConfig(VersionedObject):
    version = "v1.0.1"
    username = "john smith"
    friend_list = []

@migration(UserConfig, "1.0.0", "1.1.0")
def migrate_100_to_101(attrs);
    attrs['friend_list'] = [] # Add new 'friend_list' field
    return attrs

在不反序列化的情况下验证输入数据

您可能想要验证一些序列化的对象数据,而无需实际反序列化和加载对象值。您可以为此使用validate_dict方法。

from versionedobj import VersionedObject

class Recipe(VersionedObject):
    ingredient_1 = "onions"
    ingredient_2 = "tomatoes"
    ingredient_3 = "garlic"

rcp = Recipe()

rcp.validate_dict({"ingredient_1": "celery", "ingredient_2": "carrots"})
# Raises versionedobj.exceptions.InputValidationError because 'ingredient_3' is missing

rcp.validate_dict({"ingredient_1": "celery", "ingredient_2": "carrots", "ingredient_12": "cumin"})
# Raises versionedobj.exceptions.InputValidationError because 'ingredient_12' is not a valid attribute

性能/压力测试可视化

下图由tests/performance_tests/big_class_performance_test.py脚本生成,该脚本创建了多个不断增加的版本对象。

将每个对象序列化为 dict 以及从 dict 反序列化对象数据以及创建对象实例所需的时间是针对图中的每个数据点进行测量的(请注意,测量from/to_jsonfrom/to_file方法不是很有用,因为我们只是用额外的 JSON 解析器或文件 I/O 开销来测量to/from_dict )。

测试在带有 Intel Core-i7 的系统上执行,该系统运行 Debian GNU/Linux 10(buster)和 Linux debian 4.19.0-21-amd64。

https://github.com/eriknyquist/versionedobj/raw/master/images/performance_graph.png

贡献

欢迎投稿,请在https://github.com/eriknyquist/versionedobj打开拉取请求并确保:

  1. 所有现有的单元测试都通过(通过python setup.py test运行测试)

  2. 添加了新的单元测试以涵盖任何修改/新功能

如果您对贡献或单元测试有任何疑问/需要帮助,请通过eknyquist @ gmail联系 Erik com

项目详情


下载文件

下载适用于您平台的文件。如果您不确定要选择哪个,请了解有关安装包的更多信息。

内置分布

versionedobj-0.3.0-py3-none-any.whl (14.2 kB 查看哈希

已上传 py3