user1403483
user1403483

Reputation:

How can I copy an immutable object like tuple in Python?

copy.copy() and copy.deepcopy() just copy the reference for an immutable object like a tuple. How can I create a duplicate copy of the first immutable object at a different memory location?

Upvotes: 50

Views: 97756

Answers (7)

Steffen
Steffen

Reputation: 29

Python 12.2 (probably lower as well, but not tested)

In addition to Wiki Zhao answer (https://stackoverflow.com/a/15214661/24155535):

Instead of

tup = (1,2,3)
nt = tuple(list(tup))

it's also possible to replace list with iter which should be less overhead

tup = (1,2,3)
nt = tuple(iter(tup))

.

Both return False by the following check of the memory location

tup is nt

.

Caution:

Concatenation as discribet by Ivo (https://stackoverflow.com/a/15214570/24155535) with + tuple() or + () does not work anymore and return True!

Upvotes: 1

akhan
akhan

Reputation: 3039

Newer python versions will not get tricked easily. However, slicing combined with splat operator gives you a way out. Tested with Python v3.12.

>>> test = (558, 3, 4, 64)
>>> test2 = (test[0], *test[1:])
>>> test2
(558, 3, 4, 64)
>>> test == test2
True
>>> test is test2
False
>>> id(test), id(test2)
(4345565632, 4345564832)

Like many others, the following is still caught by python:

>>> test3 = (test[:])
>>> test3 == test
True
>>> test3 is test
True
>>> id(test3), id(test)
(4345565632, 4345565632)

Upvotes: 0

Kotnen
Kotnen

Reputation: 11

This answer:

  • depends on cPython implementation details (meaning that it's somewhat version-specific (although in practice, it hasn't been changed in a long time, and probably won't be changed any time soon))
  • invokes C undefined behaviour
  • depends on the size of pointers on your machine

This answer should NOT be used in production code in any way. Now with the warning/disclaimer out of the way, let's get into it.

A preface full of pointless pedantry

First, I'm going to be pedantic about your question. (I'm sorry, but the pedant inside me just can't help themselves). Let's start with the idea of immutability. In cPython, nothing is immutable. This is because cPython gives you access to a convenient lil' module called ctypes. This allows you to do all sorts of cool things, such as modify tuples in place:

>>> from ctypes import py_object as PyObject_p
>>> mutator = PyObject_p.from_address
>>> victim = (0, 1, 2, 3, 4)
>>> print(victim)
(0, 1, 2, 3, 4)
>>> mutator(id(victim) + 40).value = "not so immutable now, are ye'?"
>>> print(victim)
(0, 1, "not so immutable now, are ye'?", 3, 4)

Even without ctypes, one could always write an extension module in something like C and accomplish the same thing. And then there's always just the ability to write into the process memory directly (because /proc/self/mem exists), and you can do that using plain Python (without ctypes or extension modules or any of that stuff). All of this is to say, immutability isn't real. When something is "immutable" in Python, all that means is that it's going to be slightly inconvenient if you want to mutate it.

The Answer

As per usual, the way to deal with Python's silly ideas of immutability and safety is to ignore them.

from ctypes import (
    byref as _addrof,
    sizeof as _sizeof,
    create_string_buffer as _malloc,
    cast,
    memmove as memcpy,
    c_void_p as void_p,
    py_object as PyObject_p,
)


# get the base class, since it's not publicly exposed
_ctypes_base_cls = void_p
while (_next_base := _ctypes_base_cls.__bases__[0]) is not object:
    _ctypes_base_cls = _next_base



## some utility functions that work with PyObjects as well as ctypes objects

# sizeof
def sizeof(obj):
    if isinstance(obj, _ctypes_base_cls):
        return _sizeof(obj)
    else:
        return obj.__sizeof__()

# does the same thing as `&obj` in C
def addrof(obj):
    if not isinstance(obj, _ctypes_base_cls):
        return void_p(id(obj))
    return cast(_addrof(obj), void_p)

# allocate memory
def malloc(size):
    return cast(_malloc(size), void_p)

# treat something as a `PyObject *`
def as_pyobj_p(obj):
    if not isinstance(obj, _ctypes_base_cls):
        what = addrof(obj)
    return PyObject_p.from_address(addrof(obj).value)



## now for the magic...

# returns a (shallow) copy of the object
def copyobj(obj):
    copy_addr = malloc(sizeof(obj))
    memcpy(copy_addr, addrof(obj), sizeof(obj))
    return as_pyobj_p(copy_addr).value


orig = (0, 1, 2, 3, 4)
copy = copyobj(orig)


# Show the details of the objects:
print(f"""
original:
    {orig!r}
    {sizeof(orig)} bytes at address {hex(addrof(orig).value)}

copy:
    {copy!r}
    {sizeof(copy)} bytes at address {hex(addrof(copy).value)}


{orig is copy = }
""")

This is just a shallow copy, but one could construct a real deepcopy with this. (I say "real" because Python's deepcopy is not real: it doesn't copy so-called immutable objects, or the type information of objects (the ob_type member in the PyObject struct), and a few other things like that.)

Enjoy!

Upvotes: 0

Wiki Zhao
Wiki Zhao

Reputation: 227

Try this:

tup = (1,2,3)
nt = tuple(list(tup))

Upvotes: 8

EMILIO
EMILIO

Reputation: 455

If You wish duplicate a tuple try

tup = ('a',2,obj)
dup_tup = tuple(tup)

in this case we created a new tuple: we can change it without changing the first.

If we use sec_tup = tup in this case we have two tuple but changing one the change is done also in the other

Upvotes: -4

Makoto
Makoto

Reputation: 106470

You're looking for deepcopy.

from copy import deepcopy

tup = (1, 2, 3, 4, 5)
put = deepcopy(tup)

Admittedly, the ID of these two tuples will point to the same address. Because a tuple is immutable, there's really no rationale to create another copy of it that's the exact same. However, note that tuples can contain mutable elements to them, and deepcopy/id behaves as you anticipate it would:

from copy import deepcopy
tup = (1, 2, [])
put = deepcopy(tup)
tup[2].append('hello')
print tup # (1, 2, ['hello'])
print put # (1, 2, [])

Upvotes: 51

lvc
lvc

Reputation: 35089

Add the empty tuple to it:

>>> a = (1, 2, 3)
>>> a is a+tuple()  
False

Concatenating tuples always returns a new distinct tuple, even when the result turns out to be equal.

Upvotes: 26

Related Questions