Python — copy.copy replacement?

Started by
9 comments, last by taby 9 months, 2 weeks ago

As far as I can tell, in Python there is no way to pass a function parameter by value — it’s all by reference. Also, assigning a variable, like x = y, ends up having it so that x and y point to the same memory address. It’s completely annoying, and not at all like C++. It turns out that there is a copy module that you can import… Calling x = copy.copy(y) creates x at a new memory address, like I want it to. Syntactically, it’s ugly, but it works. Do you know of a more elegant solution?

Advertisement

Keep in mind that:

x = copy.copy(y)

Returns a SHALLOW copy. If you want a deep copy then you have to do:

x = copy.deepcopy(y)

There are some specifics to lists F.e.

I'm not sure whether there is more elegant solution.

My current blog on programming, linux and stuff - http://gameprogrammerdiary.blogspot.com

Thanks for the info!

What is the big difference between deep and shallow copying, in Python anyway?

Each class can define 2 separate calls:

__deepcopy__()

__copy__()

The difference is relevant only to compound objects though, where shallow copy constructs new compound object and then inserts references into it to the objects found in the original. While deep copy constructs a new compound object and then recursively inserts copies into it (of the ones found in original).

My current blog on programming, linux and stuff - http://gameprogrammerdiary.blogspot.com

taby said:
As far as I can tell, in Python there is no way to pass a function parameter by value

There are immutable objects (strings, integers, frozen sets) that you can pass by reference without problems, but yeah, everything is by reference. In C/C++ you have a choice there, in languages like lua, Java, and assumingly C#, the situation is the same as in Python.

taby said:
Also, assigning a variable, like x = y, ends up having it so that x and y point to the same memory address. It’s completely annoying, and not at all like C++

You should stop trying to have C++ conventions eventually. I can understand what you're going through though, I have done the same transition as what you're going through, back in Python 1.5 iirc.

I first discovered like you that assignment was different, then switched to making full (deep) copies of everything for a while, started to experiment with less copying, and eventually switched to the normal style of Pytohn / Java programming.

The first step to make here is to treat parameter values as owned by the caller (as you say, parameters by value don't exist, although immutable objects can be treated like it).

The second step to make is to not worry about allocating objects. Unlike C++, these languages are optimized for creating loads of new objects with a short life-time. Python is a high-level programming language, memory is something you don't worry about, in general.

What I did after trying to deepcopy-ing everything (and finding it gets very slow for large data structures) is what I call “functional programming style”. Arguably a bad name to some extent but it works as follows: You treat everything as “create once, then it's read-only”. So you never modify anything that exists already,. All data instantly becomes completely stable, you can pass it around, walk through it, etc without ever worrying about modifications.

The costs are then that you cannot modify existing data. So instead of modifying data, you always make new objects. The good news here is that all existing data is stable, so you can use any part of that to construct the new object. That eliminates the need for deep-copy everything all the time, it also reduces the amount of garbage objects that you create in the process.

Where this style goes wrong is that in large data structures, if you need to change something in eg a tree 5 levels down, you must rebuild those 5 levels. Eventually I started to relax the rules, basically by allowing “my” data structures to be changed as I like.

So the eventual steps to add is to extend the function interface contract by defining what the caller is allowed to do with provided argument (ie does the caller want it untouched or can you modify it (and if yes, then how may it be modified?)

Alberth said:
There are immutable objects (strings, integers, frozen sets) that you can pass by reference without problems, but yeah, everything is by reference. In C/C++ you have a choice there

That's not entirely correct statement. In C/C++ the way how arguments are passed is dependent on target's ABI (Application Binary Interface). C implementations doesn't give you a choice, it in general is almost always pass-by-value (when you pass a pointer-to-memory, the pointer itself is passed by value). C++ does give you a choice with special syntax to pass-by-reference.

Most of the ABIs today (either ones using __vectorcall or System V AMD64 ABI) pass as much parameters as possible through processor's registers (this can contain quite a lot of data due to (X,Y)MM registers).

Neither Python, nor C# (and neither other languages) are dependent on target's ABI, as they run in interpreter/VM (I won't dig into optimizations with JIT - the forums post would get quite big).

Alberth said:
The second step to make is to not worry about allocating objects. Unlike C++, these languages are optimized for creating loads of new objects with a short life-time.

C++ can be quite confusing in this - as it is also possible of creating massive amounts of objects fast, the main problem is:

// 1
Foo x = Foo();

// 2
Foo* x = new Foo();

// 3
Foo* x = new Foo[10000];
for (int i = 0; i < 10000; i++)
{
    x[i] = Foo();
}

For short description:

  1. Allocates on stack (which is generally just 1 increment) and calls constructor on the stack memory space (fast)
  2. Performs system memory allocation (generally some sort/variant of HeapAlloc in Windows - which is quite heavy function), and calls constructor on the allocated memory space on heap
  3. As in 2., performs single call to HeapAlloc - then 10000 allocations within the allocated memory space on heap

Calling HeapAlloc has its impacts (and interpreted languages generally avoid that, as they often use different way to manage memory).

This makes a BIG performance difference when comparing the code that “looks” the same, but doesn't really do the same.

Your statement:

Alberth said:
You should stop trying to have C++ conventions eventually

Hits the head of the nail perfectly!

My current blog on programming, linux and stuff - http://gameprogrammerdiary.blogspot.com

Vilem Otte said:
C++ can be quite confusing in this - as it is also possible of creating massive amounts of objects fast, the main problem is:

Somewhat off-topic, I am not really interested in performance of compiler output, and I don;t know how much research you have done in this, but if you're quoting the official C++ language definition, please don't take what it says literally. I work in designing languages, and building compilers and simulators, focusing mostly on the parts before optimizing output. Correctness is way more relevant than performance in my field, hence my lack of interest in the latter.

I know the C++ language defines how a language construct should be translated with many function calls, object creation steps, etc, but that is only half the story. The reason they write what they write is because they need an unambiguous way to describe what a compiler implementation should do. In that way you have a much better chance that compiler A written by X and compiler B written by Y will both generate code that behaves the same for the same input source.

The other half of the story is that as a compiler implementer you may generate any code that you want, as long as the externally observable result is not distinguishable from what the language definition describes. The trivial way to implement the language correctly is thus by doing literally what the definition says, but it's usually not the fastest solution. If a compiler implementation can deduce that some part of the steps are not externally observable, it can decide to replace it by an equivalent part that faster/cheaper/etc. In other words, what the language definition promises and what a real implementation is actually doing can be very different for all parts that are not externally observable.

An ABI isn't part of the C++ language standard afaik, but being compatible at binary level with other systems is obviously relevant for a language implementation, so these give further restrictions (more "externally visible behavior to comply with).

Alberth said:
Somewhat off-topic, I am not really interested in performance of compiler output, and I don;t know how much research you have done in this, but if you're quoting the official C++ language definition, please don't take what it says literally. I work in designing languages, and building compilers and simulators, focusing mostly on the parts before optimizing output. Correctness is way more relevant than performance in my field, hence my lack of interest in the latter.

The example (difference) I wrote isn't really related to compilers that much as to the OS specifics/VM specifics. I often see people stating that calling the following code:

C++:
Foo* x = new Foo();

Java:
Foo x = new Foo();

To be equivalent. And then comparing it in performance (which is stupid tbh). Overall this couldn't be further from truth (it was mainly in reaction to not tie to C++ conventions in other languages). These 2 statements are doing very different things internally.

Alberth said:
An ABI isn't part of the C++ language standard afaik

You're correct, it can't be. ABI is different for different architectures.

I might be wrong with next statement (my main experience in compiler creation was writing a tiny C compiler (satisfying the language definition, but very far from anything production-wise) - outputting intermediate assembly, disassembler to custom code for custom VM … you could possibly disassemble to x86 code though and link it into elf binary) - although, if I'm not mistaken, some specifics of the ABI can be solved further down after compilation to intermediate assembly.

Alberth said:
If a compiler implementation can deduce that some part of the steps are not externally observable, it can decide to replace it by an equivalent part that faster/cheaper/etc.

Is this done on compiler level nowadays? I did know about “window optimizations” in assembly (not sure if that translation is correct in English), where you look through the instructions with “a window” (taking just a sequence of N instructions) and if there is a replacement on given architecture doing the exactly same thing, you use it. Typical example would be in case of consequent instructions MULPS and ADDPS, which you can replace with VFMADDxxxPS.

My current blog on programming, linux and stuff - http://gameprogrammerdiary.blogspot.com

Vilem Otte said:
I often see people stating that calling the following code: [[ C++ “new” versus Java “new” ]] To be equivalent.

Haha, sounds like fun :p Not sure what one tries to prove with it, but people do all kind of weird stuff to defend their favorite language ?

I once ran into a Java to C++ convertor claiming it could do translations so I tried the Java variant like your example and sure enough it returned the C++ variant. A “small” detail, it didn't include code to handle the allocated memory afterwards :p

Vilem Otte said:
… my main experience in compiler creation was writing a tiny C compiler (satisfying the language definition, but very far from anything production-wise) - outputting intermediate assembly ….

Sounds like fun. I work in specification language (a much higher abstraction level than a regular programming language), where you can declarativilly specify how a controlled system should behave, in terms of a collection of parallel executing processes, in an abstract form of time. I usually end in an imperative programming language (Java, C++, or even PLC code)..

So I don't have much practical experience in optimizing in compilers yet, but that topic is coming up somewhere next year so I have been reading “Engineering a compiler” which is about all the analysis and code generation and code change steps (like your instruction replacement) that can happen. Basically the message of the book seems to be that deciding what code to generate is an unsolved problem. It's horribly difficult, even after splitting the problem in phases, each phase in itself is still so difficult that you have no hope to find the best solution at all, so people try every technique and every idea to improve the process.

So yeah, once you passed scanning and parsing, and built an AST, everything after it is analysis, storing found results for a next analysis or acting on the found results by rewriting. Your example is of rewriting assembly language sequences looks like it belong somewhere near the end of the compilation (it doesn't introduce functionally different behavior, it improves performance by (relatively safe) rewriting sequences to better ones).

Thanks for all of the elaboration y’all.

This topic is closed to new replies.

Advertisement