Skip to content

Commit 6f3c138

Browse files
gh-108751: Add copy.replace() function (GH-108752)
It creates a modified copy of an object by calling the object's __replace__() method. It is a generalization of dataclasses.replace(), named tuple's _replace() method and replace() methods in various classes, and supports all these stdlib classes.
1 parent 9f0c0a4 commit 6f3c138

19 files changed

+314
-71
lines changed

Doc/library/collections.rst

+2
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,8 @@ field names, the method and attribute names start with an underscore.
979979
>>> for partnum, record in inventory.items():
980980
... inventory[partnum] = record._replace(price=newprices[partnum], timestamp=time.now())
981981

982+
Named tuples are also supported by generic function :func:`copy.replace`.
983+
982984
.. attribute:: somenamedtuple._fields
983985

984986
Tuple of strings listing the field names. Useful for introspection

Doc/library/copy.rst

+26-4
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,22 @@ operations (explained below).
1717

1818
Interface summary:
1919

20-
.. function:: copy(x)
20+
.. function:: copy(obj)
2121

22-
Return a shallow copy of *x*.
22+
Return a shallow copy of *obj*.
2323

2424

25-
.. function:: deepcopy(x[, memo])
25+
.. function:: deepcopy(obj[, memo])
2626

27-
Return a deep copy of *x*.
27+
Return a deep copy of *obj*.
28+
29+
30+
.. function:: replace(obj, /, **changes)
31+
32+
Creates a new object of the same type as *obj*, replacing fields with values
33+
from *changes*.
34+
35+
.. versionadded:: 3.13
2836

2937

3038
.. exception:: Error
@@ -89,6 +97,20 @@ with the component as first argument and the memo dictionary as second argument.
8997
The memo dictionary should be treated as an opaque object.
9098

9199

100+
.. index::
101+
single: __replace__() (replace protocol)
102+
103+
Function :func:`replace` is more limited than :func:`copy` and :func:`deepcopy`,
104+
and only supports named tuples created by :func:`~collections.namedtuple`,
105+
:mod:`dataclasses`, and other classes which define method :meth:`!__replace__`.
106+
107+
.. method:: __replace__(self, /, **changes)
108+
:noindex:
109+
110+
:meth:`!__replace__` should create a new object of the same type,
111+
replacing fields with values from *changes*.
112+
113+
92114
.. seealso::
93115

94116
Module :mod:`pickle`

Doc/library/dataclasses.rst

+2
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,8 @@ Module contents
456456
``replace()`` (or similarly named) method which handles instance
457457
copying.
458458

459+
Dataclass instances are also supported by generic function :func:`copy.replace`.
460+
459461
.. function:: is_dataclass(obj)
460462

461463
Return ``True`` if its parameter is a dataclass or an instance of one,

Doc/library/datetime.rst

+9
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,9 @@ Instance methods:
652652
>>> d.replace(day=26)
653653
datetime.date(2002, 12, 26)
654654

655+
:class:`date` objects are also supported by generic function
656+
:func:`copy.replace`.
657+
655658

656659
.. method:: date.timetuple()
657660

@@ -1251,6 +1254,9 @@ Instance methods:
12511254
``tzinfo=None`` can be specified to create a naive datetime from an aware
12521255
datetime with no conversion of date and time data.
12531256

1257+
:class:`datetime` objects are also supported by generic function
1258+
:func:`copy.replace`.
1259+
12541260
.. versionadded:: 3.6
12551261
Added the ``fold`` argument.
12561262

@@ -1827,6 +1833,9 @@ Instance methods:
18271833
``tzinfo=None`` can be specified to create a naive :class:`.time` from an
18281834
aware :class:`.time`, without conversion of the time data.
18291835

1836+
:class:`time` objects are also supported by generic function
1837+
:func:`copy.replace`.
1838+
18301839
.. versionadded:: 3.6
18311840
Added the ``fold`` argument.
18321841

Doc/library/inspect.rst

+8-3
Original file line numberDiff line numberDiff line change
@@ -689,8 +689,8 @@ function.
689689
The optional *return_annotation* argument, can be an arbitrary Python object,
690690
is the "return" annotation of the callable.
691691

692-
Signature objects are *immutable*. Use :meth:`Signature.replace` to make a
693-
modified copy.
692+
Signature objects are *immutable*. Use :meth:`Signature.replace` or
693+
:func:`copy.replace` to make a modified copy.
694694

695695
.. versionchanged:: 3.5
696696
Signature objects are picklable and :term:`hashable`.
@@ -746,6 +746,9 @@ function.
746746
>>> str(new_sig)
747747
"(a, b) -> 'new return anno'"
748748

749+
Signature objects are also supported by generic function
750+
:func:`copy.replace`.
751+
749752
.. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True, globalns=None, localns=None)
750753

751754
Return a :class:`Signature` (or its subclass) object for a given callable
@@ -769,7 +772,7 @@ function.
769772
.. class:: Parameter(name, kind, *, default=Parameter.empty, annotation=Parameter.empty)
770773

771774
Parameter objects are *immutable*. Instead of modifying a Parameter object,
772-
you can use :meth:`Parameter.replace` to create a modified copy.
775+
you can use :meth:`Parameter.replace` or :func:`copy.replace` to create a modified copy.
773776

774777
.. versionchanged:: 3.5
775778
Parameter objects are picklable and :term:`hashable`.
@@ -892,6 +895,8 @@ function.
892895
>>> str(param.replace(default=Parameter.empty, annotation='spam'))
893896
"foo:'spam'"
894897

898+
Parameter objects are also supported by generic function :func:`copy.replace`.
899+
895900
.. versionchanged:: 3.4
896901
In Python 3.3 Parameter objects were allowed to have ``name`` set
897902
to ``None`` if their ``kind`` was set to ``POSITIONAL_ONLY``.

Doc/library/types.rst

+2
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ Standard names are defined for the following types:
200200

201201
Return a copy of the code object with new values for the specified fields.
202202

203+
Code objects are also supported by generic function :func:`copy.replace`.
204+
203205
.. versionadded:: 3.8
204206

205207
.. data:: CellType

Doc/whatsnew/3.13.rst

+12
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ array
115115
It can be used instead of ``'u'`` type code, which is deprecated.
116116
(Contributed by Inada Naoki in :gh:`80480`.)
117117

118+
copy
119+
----
120+
121+
* Add :func:`copy.replace` function which allows to create a modified copy of
122+
an object, which is especially usefule for immutable objects.
123+
It supports named tuples created with the factory function
124+
:func:`collections.namedtuple`, :class:`~dataclasses.dataclass` instances,
125+
various :mod:`datetime` objects, :class:`~inspect.Signature` objects,
126+
:class:`~inspect.Parameter` objects, :ref:`code object <code-objects>`, and
127+
any user classes which define the :meth:`!__replace__` method.
128+
(Contributed by Serhiy Storchaka in :gh:`108751`.)
129+
118130
dbm
119131
---
120132

Lib/_pydatetime.py

+6
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,8 @@ def replace(self, year=None, month=None, day=None):
11121112
day = self._day
11131113
return type(self)(year, month, day)
11141114

1115+
__replace__ = replace
1116+
11151117
# Comparisons of date objects with other.
11161118

11171119
def __eq__(self, other):
@@ -1637,6 +1639,8 @@ def replace(self, hour=None, minute=None, second=None, microsecond=None,
16371639
fold = self._fold
16381640
return type(self)(hour, minute, second, microsecond, tzinfo, fold=fold)
16391641

1642+
__replace__ = replace
1643+
16401644
# Pickle support.
16411645

16421646
def _getstate(self, protocol=3):
@@ -1983,6 +1987,8 @@ def replace(self, year=None, month=None, day=None, hour=None,
19831987
return type(self)(year, month, day, hour, minute, second,
19841988
microsecond, tzinfo, fold=fold)
19851989

1990+
__replace__ = replace
1991+
19861992
def _local_timezone(self):
19871993
if self.tzinfo is None:
19881994
ts = self._mktime()

Lib/collections/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ def __getnewargs__(self):
495495
'_field_defaults': field_defaults,
496496
'__new__': __new__,
497497
'_make': _make,
498+
'__replace__': _replace,
498499
'_replace': _replace,
499500
'__repr__': __repr__,
500501
'_asdict': _asdict,

Lib/copy.py

+13
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,16 @@ def _reconstruct(x, memo, func, args,
290290
return y
291291

292292
del types, weakref
293+
294+
295+
def replace(obj, /, **changes):
296+
"""Return a new object replacing specified fields with new values.
297+
298+
This is especially useful for immutable objects, like named tuples or
299+
frozen dataclasses.
300+
"""
301+
cls = obj.__class__
302+
func = getattr(cls, '__replace__', None)
303+
if func is None:
304+
raise TypeError(f"replace() does not support {cls.__name__} objects")
305+
return func(obj, **changes)

Lib/dataclasses.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -1073,6 +1073,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
10731073
globals,
10741074
slots,
10751075
))
1076+
_set_new_attribute(cls, '__replace__', _replace)
10761077

10771078
# Get the fields as a list, and include only real fields. This is
10781079
# used in all of the following methods.
@@ -1546,13 +1547,15 @@ class C:
15461547
c1 = replace(c, x=3)
15471548
assert c1.x == 3 and c1.y == 2
15481549
"""
1550+
if not _is_dataclass_instance(obj):
1551+
raise TypeError("replace() should be called on dataclass instances")
1552+
return _replace(obj, **changes)
1553+
15491554

1555+
def _replace(obj, /, **changes):
15501556
# We're going to mutate 'changes', but that's okay because it's a
15511557
# new dict, even if called with 'replace(obj, **my_changes)'.
15521558

1553-
if not _is_dataclass_instance(obj):
1554-
raise TypeError("replace() should be called on dataclass instances")
1555-
15561559
# It's an error to have init=False fields in 'changes'.
15571560
# If a field is not in 'changes', read its value from the provided obj.
15581561

Lib/inspect.py

+4
Original file line numberDiff line numberDiff line change
@@ -2870,6 +2870,8 @@ def __str__(self):
28702870

28712871
return formatted
28722872

2873+
__replace__ = replace
2874+
28732875
def __repr__(self):
28742876
return '<{} "{}">'.format(self.__class__.__name__, self)
28752877

@@ -3130,6 +3132,8 @@ def replace(self, *, parameters=_void, return_annotation=_void):
31303132
return type(self)(parameters,
31313133
return_annotation=return_annotation)
31323134

3135+
__replace__ = replace
3136+
31333137
def _hash_basis(self):
31343138
params = tuple(param for param in self.parameters.values()
31353139
if param.kind != _KEYWORD_ONLY)

0 commit comments

Comments
 (0)