It is common task to create a new object based on the existing object, but with some attributes changed. dataclasses.replace() provides this feature for dataclasses, named tuples have the _raplace() method, and some concrete classes (date, time, datetime, inspect.Signature, inspect.Parameter, code object) have the replace() method.
I propose to make dataclasses.replace() extensible and work with all these classes and user classes which support corresponding protocol. It should simple call the __replace__() method. All classes mentioned above should provide this method (as an alias of existing _replace() or replace() method), and user classes can implement it as well. Good candidates for adding the __replace__() method are SimpleNamespace and AttrDict (if we keep the latter).
Advantage of using replace(obj) over obj.replace() is that the method name does not conflict with attribute name (especially important for dataclasses, named tuples and SimpleNamespace).
Advantage of using replace(obj) over obj._replace() is that the latter looks like using non-public API.
Fair enough. I suppose it mostly applies to dataclass-like classes, in any case, as your internal fields-to-replace have to pretty closely map to your __init__ arguments for such a generic function to be useful.
My two cents are that if this were added, copy seems a reasonable location since the method promises to return a new object, so it’s basically a copy-with-replacing. Relatedly, are the implementations/protocol contract supposed to return a shallow or deep copy? Or should the API have an option for either?
I would expect it to be the same as calling __init__() with some identical values and some alternative values. If __init__ makes a (deep)copy, then __replace__ will make a (deep)copy. Any deviations from that I would want documented.
And if I wanted a deepcopy, regardless, I would deepcopy(replace(obj, **kwargs)) or replace(deepcopy(obj), **kwargs).
Yes, at first glance the copy module looks the best candidate. I thought about this. And it would be nice to support wider class of objects in replace() by falling back to copy() or the pickle protocol. But there are differences between copy() and replace() which makes this difficult.
copy() supports immutable objects. Setting attributes will fail later, but with wrong exception. Some of these objects could be supported using the pickle protocol, but __copy__() and global registry have priority.
copy() treats classes and functions as atomic objects and return the argument. Most of attributes of Python classes and functions are mutable. Changing them will affect the original object.
By default (when the pickle protocol is used) copy() bypasses __init__(). For replace() we usually want to call __init__() to set calculated attribute which depend on specified attributes.
By default copy() sets all attributes, including internal attributes which should not be specified by user and should not be shared between instances (in dataclasses they are defined as fields with init=False).
The behavior of copy(obj) and replace(obj) will be too different to merge them in one function, and perhaps too different to have them in the same module. It is possible to add support of more general objects in replace(), but it will either be limited to very narrow class of objects, or work incorrectly in many cases.
I also think a replace that worked on immutable objects via __init__ could be useful when trying to write in somewhat functional style. Imagine I had a list of namedtuples and I wanted to blank out a field. I could write [replace(t, big_secret="") for t in my_list] to map them.