Python strings are immutable, but only sometimes
web.eecs.utk.edu
web.eecs.utk.edu
This is an undocumented optimization, so you should assume you're allocating a new sting every single time like the internet says.
I've been coding Python since 1.5.2 days and so I'll continue to use lists as intermediate string builders and then join afterwards because I know this works in past, present, a likely future versions of Python.
While I do get your point, Python likes making optimizations. They rarely, if ever, make patterns slower by choice. There's nothing in any spec that says it _has_ to be slow: that's as much as an implementation detail as "joining lists are fast".
In [18]: sys.version
Out[18]: '3.9.1 (default, Feb 3 2021, 07:38:02) \n[Clang 12.0.0 (clang-1200.0.32.29)]'
In [6]: strings = ["".join(random.choice(string.hexdigits) for _ in range(10)) for _ in range(10)]
In [9]: def one():
...: "".join(s for s in strings)
...:
In [10]: def two():
...: "".join([s for s in strings])
...:
In [11]: def three():
...: x = []
...: for s in strings:
...: x.append(s)
...: "".join(x)
...:
In [12]: def four():
...: x = ""
...: for s in strings:
...: x += s
...:
In [13]: %timeit one()
753 ns ± 9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [14]: %timeit two()
521 ns ± 5.63 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [15]: %timeit three()
696 ns ± 3.94 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [16]: %timeit four()
620 ns ± 4.82 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)However, other python implementations might not optimize the concatenation. IIRC, pypy doesn't.
If you are targetting CPython, try to be "pythonic"[0] and then drop to C/++ (through numba, cython, nuitka, pure C or whatever way you like best) if your performance requirements need it.
I'm not familiar with Jython and IronPython innards to know what's the best way to handle performance for them. Most probably it's very different than CPython and PyPy.
So like you said, keep things reasonable and don't depend on implementation details--unless you are explicit in your target/supported interpreter and you absolutely need to squeeze the performance out of the code.
a) that surprises me, because in my short experience with it, I found that PyPy is more about simpler idioms if you are aiming for performance, a bit C-ish in style
b) it's been something like... 3 years (I think) since I last used PyPy for some serious performance testing of code. And even then it was a horrible proof of concept that barely ran, but helped me point out that "hey, we optimized it to 3.5x times faster with CPython and PyPy is getting to 4.5x over the original code, but it's throwing a thousand errors and warnings on screen and we can't be sure it'll work in production"[0].
Probably things have changed a bit on the PyPy side, though I don't think RPython has changed much in concept.
Also I see I never got to explain the pythonic note on my previous comment: basically, whatever "pythonic" means to the team. In general I try to go for the definition that is "it's easy to read and understand what's going on with the code, and provides easy-to-use interfaces for programmers". Not an easy to do thing, but at least trying to do it helps a lot.
[0] the job was that there was a large program that was bottlenecking a process in a lab, and somebody threw the idea of "oh we should use pypy because it'll surely make things run faster". And then it was my responsibility to pick that skeleton up and take care of it.
Oh and the code was pretty inefficient to begin with, which is why we got to speed it up so much without even going into C. A quick test with Cython put it in the 7-10x range of speedups, but the client wasn't willing to handle that, so they just kept it in CPython.
You probably can but should not rely on it:
* it was a very official part of the Python 2.4 release notes
* it's unlikely the devs would remove it as they know how implementation details tend to leak into the software (see: dict ordering)
* but it is an optimisation so there's no guarantee, a simple trace function could break it
* and obviously there's no guarantee (and likely no way) other implementations can implement it
If you do want to take advantage of this, maybe you can go a level deeper: dig into Python's behaviour when allocating memory for strings, and figure out in what circumstances you can be guaranteed to get the same id back. E.g. maybe if you create a 28-character string, there will always be room to append 4 more.
Because, yeah, the fact that str += "?" is faster isn't a big deal; that's the natural, ergonomic way to deal with it, because you're appending all of one string. Likewise foo + " " + bar + "?" is probably easier to write that way, than to drop them into a list and join (but even if not, I'd be curious if it's actually any faster; this article doesn't measure). By the time you get to joining large amounts together, concatenating a CSV or something, you're going to use join, naturally, and join is going to be more performant.
So to my mind it's kind of an open question, at what points is the non-ergonomic thing going to be the faster thing? That's what "taking advantage of (an optimization)" feels like; otherwise you're just writing code and letting the performance fall where it may.
I’m guessing all of them. Because what it does is what join will do internally anyway, but without the overhead of the list itself, and with operations which have lower overhead than the list’s.
Yes, you are. I'm saying this is a more interesting question, and one that isn't answered. Because the reality is that if you're appending just a handful of things together, the idiomatic approach is to concatenate them, and this optimization comes into play. If you're concatenating a lot of items together...you probably already have a list, and the idiomatic approach to join them is probably faster than a for loop to concat them over and over. So the question then, is is there a point where idiomatically it makes more sense to join things (putting them in a list if they aren't already, but they might be; depends on the example), but this will be more efficient?
The important takeaway is that semantic immutability does not require or depend on implementation immutability. It's a 'levels of abstraction' thing. Once you let the users peek behind the curtain, however, it becomes much more difficult to be sure they cannot shoot themselves in the foot by so doing.
Congrats, by trying to be smart you just shot yourself in the foot.
No. The strings having the same address doesn't mean they're the same strings. This is an interesting optimization but one Python string is being replaced by another string at the same address. Even without this optimization the address will be reused eventually.
I assume my array sort won't be n^3, but how do we draw the line on what to assume and what not to assume?
[1] https://en.wikipedia.org/wiki/Timsort
[2] https://svn.python.org/projects/python/trunk/Objects/listsor...
I mean a guarantee that it meets some baseline efficiency that you can depend on across time and different implementations. Does that exist?
To draw the line on what you can assume re performance, you need a combination of background knowledge, folk wisdom and common sense. (You could say "or read the code" but that could change in the next minor version: you still need common sense to know if that's likely). This is unlike, e.g. C++ where there is a specification and standards-compliant compilers must implement certain functions with the specified runtime complexity.
[0] https://docs.python.org/3/reference/introduction.html#altern...
If builtin, then you'll need to check the code for cpython or whatever you're using.
Mutable state is not bad; shared mutable state is bad — or, at least, takes a lot of care to handle. So CPython mutates strings which are not observed by anyone else but the mutating code.
This is exactly the idea behind uniqueness types [1] and move semantics in C++ and Rust: mutation is safe as long as it's not shared.
When Armin Rigo's brilliant optimization was added, the rationale was mitigation-of-harm: it could sometimes save users who weren't following the advice. Also it sometimes helped with the then common technique of building a short string with series of concatenations done with "+":
advice = 'Dear ' + customer + ', please buy ' + product + '.'
Nowadays, that would be done efficiently with an f-string: advice = f'Dear {customer}, please buy {product}.'
One other personal thought as a code reviewer and teacher: If you teach someone to build strings with "+" or "+=" and the optimization prevents a negative consequence, then don't be surprised if they try this with other sequence types where no such optimization exists.Really, just learn str.join() and itertools.chain(). Both of those scale nicely.
x + "s" + y + "t" + z -->
new java.lang.StringBuilder().append(x).append("s").append(y).append("t").append(z).toString()
and not naively like this: java.lang.String.valueOf(x).concat("s").concat(String.valueOf(y)).concat("t").concat(String.valueOf(z))
> just learn str.join()Or Python io.StringIO .
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False
>>> a = 257; b = 257
>>> a is b
True
Sometimes I think of Python as the Nash Equilibrium[a] of programming languages:It's never the absolute best language for anything, but it's hard to improve it on any front (e.g., execution speed) without hindering it on other fronts (e.g., ad-hoc interactivity), and it's never the absolute worst language for anything. Often enough, it's good enough.
Python might well be "the least-worst language for everything."
--
FYI: What you're describing is not a Nash equilibrium, but a Pareto optimal point [1]. They are similar in that you couldn't do any better, but Nash equilibria is in terms of whether this would cause other players to change their strategies, while Pareto optimality is only about trading off different features/dimensions.
It is, at a first glance, a bit weird. But the way you should look at it is that Python the language doesn't say the two integers have the same identity, and you shouldn't assume they will. But it also doesn't say they can't be the same object. Since Python integers are immutable, and thus having the two variables actually reference the same object can't create side effects unless you're directly playing with identities and making assumptions in your code that you shouldn't make, the implementation can have the two variables reference the same object as an optimization without breaking anything.
I used to test for None by doing what seemed to work:
if my_variable:
do something
until I discovered it doesn't work if my_variable = 0 or some other falsy value besides None. missing = object()
def func(a=missing):
if a is missing:
raise ValueError('you must pass a')
This "numbers less than 256 are the same objects" is a fairly common on the list of "wtf python" but I've never understood it. You don't use `is` like that and you would never use it in code because the operator you're using is not the right one for the thing you're trying to do.Plus if this is the biggest wtf then that's pretty good going.
BTW, that's not the "biggest" WTF feature; it's just my favorite. There's a long list of WTF features here:
https://github.com/satwikkansal/wtfpython
Otherwise, I agree, it's a pretty good going :-)
This belief seems common, but I always wonder if anyone with familiarity with dynamic programming languages that were implemented by people who knew what they are doing (as implementers) thinks so. Self, Smalltalk and Common Lisp, for example, are doing much better on the ad-hoc interactivity front in non-trivial ways whilst offering implementations with vastly better performance preceding (C)Python by many years. The fact that python has terrible execution speed is most due to lack of relevant skills in the community not some conscious engineering trade-off.
Having said that, I don't think you are wrong on python being "the least worst language for everything" -- very few other languages have an eco system of remotely comparable expansiveness and quality (the top minds in several disciplines mostly use python for their work) which alone kills of huge swathes of would-be-competitors.
Yes, I agree. The ecosystem is part of what makes the language "the least worst language for everything."
In [2]: (1, 2) is (1, 2)
Out[2]: True
In [3]: a, b = (1, 2), (1, 2)
In [4]: a is b
Out[4]: True
In [7]: a = (1, 2)
In [8]: b = (1, 2)
In [9]: a is b
Out[9]: FalseQuick Google gives: https://stackoverflow.com/a/1515811
I think you can say that about almost any language. Each feature has it's advantages and disadvantages and even the most hated features of some languages have some reasoning behind them - so changing it would hurt some use case.
Language design is sometimes more about reasonable compromises than genius ideas.
>>> a=257
>>> b=a
>>> a is b
True
What you want is double equals.Many languages do the same, ditto for strings (as in TFA).
Also, cutting long strings with .strip() can use the same optimization. Allocation isn't fast.
IDK if it's used, but it's entirely plausible to implement.
Though it's probably not the case for strings, it should be noted that for most objects allocations are byte-perfect, because the average Python object is a bunch of pointers and and a refcount: two gc pointers, a refcount, a weakref pointer, a class pointer and a dict pointer, for a total of 48 bytes (as of 3.9, ignoring all the pointees).
That's not the case for `__slots__` though: they drop the instance dict and the weakref pointer, but then each field will add a pointer to the total (then again that's space which won't be allocated as part of the instance dict).
s = "asd"
import ctypes
ctypes.memmove(id(s) + ord('1'), b'X', 1)
print(s) # prints aXd
(Python 3.3+, 64-bit; this is a joke so please do not use this in anything real)Can you please explain to a rookie what the ord('1') is for? Is it just a way of writing a magic number of value 49, or is there some significance to it?
----
I'm personally kind of shocked at how big strings are - the minimum size is 6 native words in size, corresponding to the following fields: refcount, type pointer, string length, string hash (precomputed), flags, and a wstr pointer which just seems to be NULL for most strings. It seems like they could have at least merged the string length and flags together for short strings - a possible subject for a future PEP...
Documentation is here for the curious: https://docs.python.org/3/library/functions.html#ord
This, crucially, is not guaranteed. Instead, id() is only guaranteed to give a unique integer value for every object valid for the lifetime of that object. So if Python deallocated your first string object and allocated a new string object, Python could give the same id() for the new string object and this would be fine; this does not necessarily mean that the two string objects are the “same” object! In fact, since the objects do not share lifetimes, they cannot be said to be the same object!
Get back to me if you can make several references to the same string and make them step on each others’ toes by only assigning a new value to one of them.
Python strings have to be indexable by integers. But, in fact, most indexing is iteration. So a UTF-8 representation could work. You can index forwards and backwards through UTF-8 with opaque cursors. The search, match, and regular expression functions can all work on UTF-8. Returned indexes would be opaque cursors. Addition and subtraction of small integers to a cursor can be handled by indexing forwards and backwards through UTF-8. If a program does does random access indexing, or uses an opaque cursor in a context where it really has to be convereted to an integer, the implementation has to build an index of the entire string. Index building costs O(n), but should be rare.
All of this is just making the physics of electronics and stateful low level APIs provide reasonable performance for the reasoning benefits of immutable data.
Of course if you break that fourth wall you’ll find it’s more complicated than that. And that’s interesting for understanding how it works, but it’s important not to frame that as some kind of exception or trap door. Anyone busting through that abstraction either knows they are or has more problems coming.
Rather than statically verifying that the types are linear, the runtime throws an exception at runtime when they be used in a non-linear way.
There are various functional programming languages that utilize linear types to implement mutation while otherwise retaining a pure interface.
import java.lang.reflect.Field;
public class IntegerCacheFun {
public static void main(String[] args)
throws Exception {
Class cls=Class
.forName("java.lang.Integer$IntegerCache");
Field fld = cls.getDeclaredField("cache");
fld.setAccessible(true);
Integer[] cache = (Integer[]) fld.get(cls);
cache[4 + 128] = 5;
Integer result = 2 + 2;
System.out.print("2 + 2 = ");
System.out.println(result);
}
}
% java IntegerCacheFun
2 + 2 = 5
Ok, technically it's not mutability, but still a nice trick.[1] https://codeexplainer.wordpress.com/2018/02/18/some-are-more...
You're messing around with the internals of the system that was never intended to be touched.
> Two objects with non-overlapping lifetimes may have the same id() value.