冻结数据类 pathlib.Path-like 初始化

Frozen dataclass pathlib.Path-like initialization

我有这个数据类:

@dataclass(frozen=True)
class CacheSchema:
    id_key: str
    snapshot_key: str | None
    version: str = Info.Versions.schema_cache

我想像 pathlib.Path 一样初始化它,所以要么传递所需的参数,要么传递一个已经初始化的 CacheSchema 对象。根据我有限的理解,我认为我必须自定义 __new__(),所以我做了,但是我不能完全按照 Path 那样做,因为数据类被冻结,我不应该改变创建后的值。所以我想到了这个:

def __new__(cls, *args, **kwargs):
    if len(args) == 1 and type(args[0]) is cls:
        return cls.__new__(cls, args[0].id_key, args[0].snapshot_key, args[0].version)

    return super(CacheSchema, cls).__new__(cls)

我的逻辑是:如果传递了普通参数,则使用给定的参数调用 __init__(),否则解压缩现有对象并使用解压缩的值调用 __new__()

我的问题是 cls.__new__(cls, args[0].id_key, args[0].snapshot_key, args[0].version) 没有按照我的预期执行(递归调用 __new__ 但参数不同)。

运行这个

schema= CacheSchema('a', 'b', 'c')
schema2 = CacheSchema(schema)

加注

File "D:/x/y/z/main.py", line 10, in <module>
    schema2 = CacheSchema(schema)
TypeError: CacheSchema.__init__() missing 1 required positional argument: 'snapshot_key'

__init____new__ 之后被调用。

创作本质上是:

@classmethod
def __call__(cls, *args, **kwargs):
    self = cls.__new__(cls, *args, **kwargs)
    cls.__init__(self, *args, **kwargs)
    return self

因此,您不能重新打包 args
另见:Python: override __init__ args in __new__

pathlib.Path-like 实例化

与您的数据class不同,pathlib.Path 没有带有必需参数的 __init__ 方法。

它的 __new__ 方法调用另一个 class 方法 _from_parts1 执行 self = object.__new__(cls) 然后在 self直接。

仅使用 __new__ 方法对您的数据class 执行类似操作将是:

@dataclass(frozen=True)
class CacheSchema:
    id_key: str
    snapshot_key: str
    version: str = ""

    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_init'):
            setattr(cls, '_init', cls.__init__)
            delattr(cls, '__init__')

        self = object.__new__(cls)
    
        if len(args) == 1 and type(args[0]) is cls:
            cls._init(self, args[0].id_key, args[0].snapshot_key, args[0].version)
            return self

        cls._init(self, *args, **kwargs)
        return self

1 这还有其他问题:https://github.com/python/cpython/issues/85281

pathlib.Path-like 初始化

您需要包装 @dataclass 添加的 __init__ 函数。

你可以用另一个装饰器来做到这一点:

def init_accept_instance(cls):
    init = cls.__init__

    def __init__(self, *args, **kwargs):
        if len(args) == 1 and type(args[0]) is cls:
            args, kwargs = (), args[0].__dict__
        init(self, *args, **kwargs)

    cls.__init__ = __init__
    return cls

用法:

@init_accept_instance
@dataclass(frozen=True)
class CacheSchema:
    id_key: str
    snapshot_key: str
    version: str = Info.Versions.schema_cache

或者您可以将 __init__ 包装在 __new__ 中:

from functools import wraps


@dataclass(frozen=True)
class CacheSchema:
    id_key: str
    snapshot_key: str
    version: str = ""

    def __new__(cls, *_, **__):
        if not hasattr(cls.__init__, '__wrapped__'):
            @wraps(cls.__init__)
            def __init__(self, *args, **kwargs):
                if len(args) == 1 and type(args[0]) is cls:
                    args = (args[0].id_key, args[0].snapshot_key, args[0].version)
                cls.__init__.__wrapped__(self, *args, **kwargs)

            setattr(cls, '__init__', __init__)

        return super().__new__(cls)