我有一个 Pydantic 模型,其字段类型为
AnyUrl
。
将模型导出到 YAML 时,AnyUrl
被序列化为单独的字段槽,而不是单个字符串 URL(可能是由于 AnyUrl.__repr__
方法的实现方式)。
例如:
from pydantic import BaseModel, AnyUrl
import yaml
class MyModel(BaseModel):
url: AnyUrl
data = {'url': 'https://www.example.com'}
model = MyModel.parse_obj(data)
y = yaml.dump(model.dict(), indent=4)
print(y)
产品:
url: !!python/object/new:pydantic.networks.AnyUrl
args:
- https://www.example.com
state: !!python/tuple
- null
- fragment: null
host: www.example.com
host_type: domain
password: null
path: null
port: null
query: null
scheme: https
tld: com
user: null
理想情况下,我希望序列化的 YAML 包含
https://www.example.com
而不是单个字段。
我尝试重写
__repr__
的 AnyUrl
方法来返回 AnyUrl
对象本身,因为它扩展了 str
类,但没有成功。
pyyaml
文档实在是太可怕了,因此像定制(反)序列化这样看似基本的事情很难正确理解。但本质上有两种方法可以解决这个问题。
YAMLObject
您对子类化
AnyUrl
的想法是正确的,但是 __repr__
方法与 YAML 序列化无关。为此,您需要做三件事:
YAMLObject
,yaml_tag
,并且to_yaml
类方法。然后
pyyaml
将根据您在AnyUrl
中定义的内容序列化此自定义类(继承自YAMLObject
和to_yaml
)。
to_yaml
方法总是接收两个参数:
yaml.Dumper
实例,具有序列化标准类型的内置功能(例如通过 represent_str
等方法)和 为了避免添加/覆盖其他方法,您可以利用
AnyUrl
继承自 string 的事实,并且底层 str.__new__
方法实际上在构造过程中接收 full URL。因此 str.__str__
方法将返回“原样”。
from pydantic import AnyUrl, BaseModel
from yaml import Dumper, ScalarNode, YAMLObject, dump, safe_load
class Url(AnyUrl, YAMLObject):
yaml_tag = "!Url"
@classmethod
def to_yaml(cls, dumper: Dumper, data: str) -> ScalarNode:
return dumper.represent_str(str.__str__(data))
class MyModel(BaseModel):
foo: int = 0
url: Url
obj = MyModel.parse_obj({"url": "https://www.example.com"})
print(obj)
serialized = dump(obj.dict()).strip()
print(serialized)
deserialized = MyModel.parse_obj(safe_load(serialized))
print(deserialized == obj and isinstance(deserialized.url, Url))
输出:
foo=0 url=Url('https://www.example.com', scheme='https', host='www.example.com', tld='com', host_type='domain')
foo: 0
url: https://www.example.com
True
AnyUrl
注册一个 representer
您可以避免定义自己的子类,而是通过使用 AnyUrl
函数
全局注册一个定义如何序列化
yaml.add_representer
实例的函数。
该函数需要两个强制参数:
表示器函数本质上必须具有与选项 A 中提供的
YAMLObject.to_yaml
类方法相同的签名,即它采用 Dumper
实例和要序列化的数据作为参数。
from pydantic import AnyUrl, BaseModel
from yaml import Dumper, ScalarNode, add_representer, dump, safe_load
def url_representer(dumper: Dumper, data: AnyUrl) -> ScalarNode:
return dumper.represent_str(str.__str__(data))
add_representer(AnyUrl, url_representer)
class MyModel(BaseModel):
foo: int = 0
url: AnyUrl
obj = MyModel.parse_obj({"url": "https://www.example.com"})
print(obj)
serialized = dump(obj.dict()).strip()
print(serialized)
deserialized = MyModel.parse_obj(safe_load(serialized))
print(deserialized == obj and isinstance(deserialized.url, AnyUrl))
输出与选项 A 中的代码相同。
这种方法的好处是它涉及较少的代码以及选项 A 中两个父类之间潜在的命名空间冲突。
一个潜在的缺点是,它会修改程序整个运行时的 global 设置,如果您的应用程序变得很大并且只是需要注意,以防您决定要序列化,这可能会变得不太透明
AnyUrl
在某些时候会有不同的对象。
由于其他一些用例,我遇到了这个线程。现在 pydantic 似乎已经通过使用
field_serializer
解决了这个问题。我自己还没有测试过,但它应该像这样工作:
from pydantic import BaseModel, field_serializer
class MyModel(BaseModel):
url: AnyUrl
@field_serializer("url")
def serialize_url(self, url: AnyUrl, _info):
return str(url)