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.
Another advantage of the free function is the general object oriented design principle that free functions should be preferred over methods when possible.
I really like this idea. Is the proposal to keep this in dataclasses? I feel like it would be nice to have somewhere more generic, but I canāt think of anywhere more appropriate.
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.
If a __replace__ special method gets traction (and I very much like the idea), the replace call would (and could) naturally be a built-in function.
Although there are some methods-that-use dunders that arenāt - like copy.copy and deepcopy, and all the pickle protocol, so it is not a given.
Since it looks like the default behavior for .__replace__ is to create a new shallow copy with the requested replacements, maybe the copy module itself could be a coherent place for it.
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 think replace() should be its own function and not merged into copy(), but the copy module seems like a fine place for it ā you are, essentially, copying an obect, and then making a change to it.
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.
It might be more convincing if there were a type checking operator to convert the dataclass fields to a TypedDict. Then, the annotation for replace would be:
Iām not sure about the more general replace requested here. Are all non-method attributes replaceable? If so, maybe replace DataclassFields with NonMethodAttributes in the above?