1

Following code:

def fL0(L0=[]):
    L0.append(5)
    return L0
def fL1(L1=[]):
    L1.append(5)
    return L1
def fL2(L2=[]):
    L2.append(5)
    return L2
print("{} {} {}".format(fL0(),fL0(),fL0()))
print(f'{fL1()} {fL1()} {fL1()}')
print(   fL2(),  fL2(),  fL2()  )

is giving following output:

[5, 5, 5] [5, 5, 5] [5, 5, 5]
[5] [5, 5] [5, 5, 5]
[5, 5, 5] [5, 5, 5] [5, 5, 5]

The question why isn't the output [5] [5] [5] is answered here .

My question is: How does it come that there is different output for the same printed values? And which of the outputs is the 'right one'?

Here how it looks like in REPL:

>>> def fL0(L0=[]):
...     L0.append(5)
...     return L0
... 
>>> print("{} {} {}".format(fL0(),fL0(),fL0()))
[5, 5, 5] [5, 5, 5] [5, 5, 5]
>>> print("{} {} {}".format(fL0(),fL0(),fL0()))
[5, 5, 5, 5, 5, 5] [5, 5, 5, 5, 5, 5] [5, 5, 5, 5, 5, 5]
>>> print("{} {} {}".format(fL0(),fL0(),fL0()))
[5, 5, 5, 5, 5, 5, 5, 5, 5] [5, 5, 5, 5, 5, 5, 5, 5, 5] [5, 5, 5, 5, 5, 5, 5, 5, 5]
>>> def fL1(L1=[]):
...     L1.append(5)
...     return L1
... 
>>> print(f'{fL1()} {fL1()} {fL1()}')
[5] [5, 5] [5, 5, 5]
>>> print(f'{fL1()} {fL1()} {fL1()}')
[5, 5, 5, 5] [5, 5, 5, 5, 5] [5, 5, 5, 5, 5, 5]
>>> print(f'{fL1()} {fL1()} {fL1()}')
[5, 5, 5, 5, 5, 5, 5] [5, 5, 5, 5, 5, 5, 5, 5] [5, 5, 5, 5, 5, 5, 5, 5, 5]

And here some evidence that in all three versions the arguments are evaluated from left to the right:

from time import perf_counter as T
sT=T(); print("{} {} {}".format(T()-sT,T()-sT,T()-sT))
sT=T(); print(f'{T()-sT} {T()-sT} {T()-sT}')
sT=T(); print(   T()-sT,  T()-sT,  T()-sT  )
L = \
[['627274.23131541', '627274.23131578','627274.23131589'], 
 ['627274.23133362', '627274.23133663','627274.23133841'],
 ['627274.23134319', '627274.23134330','627274.23134340']]
from pprint import pprint
pprint(list(zip(L[0],L[1],L[2])))
L_T = \
[('627274.23131541', '627274.23133362', '627274.23134319'),
 ('627274.23131578', '627274.23133663', '627274.23134330'),
 ('627274.23131589', '627274.23133841', '627274.23134340')]

The value of the evaluated parameter seems not to be used in the final printing, as the first of the evaluated parameter does NOT evaluate to [5, 5, 5] but [5]. But this value is not printed ... It does not matter when the parameter are evaluated. They have to be different as the function returns different values in subsequent calls, right or not right? How to know what the function returns on which call from the printed output???

Here another example of same behavior without growing list:

def fn1(L, incr):
   L[0] += incr
   return L
lst = [0]
print(f'{fn1(lst,2)} {fn1(lst,3)}')
print(f'{fn1(lst,2)} {fn1(lst,3)}')
def fn2(L, incr):
   L[0] += incr
   return L
lst = [0]
print( fn2(lst,2), fn2(lst,3) )
print( fn2(lst,2), fn2(lst,3) )

The output is:

[2] [5]
[7] [10]
[5] [5]
[10] [10]

It seems that f-string is copying the evaluated result for the output and print doesn't copy the evaluated result storing only the reference to it for later use in the output. If the value of the reference change with subsequent evaluation print() prints the wrong value as it didn't copy the one evaluated before.

In other words f-string does it right and copies the current value at the time of evaluation to the output and pure print does it not as expected as it does not copy the result of the evaluation for use in final output after all evaluation steps are finished. Can this behavior be considered a bug in print() fixed in the code of f-strings? Anyway, it is not consistent, but should be ...

Claudio
  • 7,474
  • 3
  • 18
  • 48

1 Answers1

1

When you call a function, all the expressions used as parameters are called before the function executes. So all 3 calls to fL2() happen before print is called.

It appears that evaluating a f-string doesn't have the same pattern, so you're seeing the result after each call to fL1().

Edit: based on your additions to the question, I think you understand what's going on but simply refuse to believe it's proper behavior. In Python, nothing gets copied unless you explicitly make a copy. All the different iterations of your function always return the same object, whether it was a default parameter or passed in. A mutable object can change even after it's been returned - what matters is when the value of that object is used.

It's easy to get print to work the same way as the f-string, you just need to explicitly copy the return value of each function call so it won't be updated by the next one.

>>> print(fL2().copy(), fL2().copy(), fL2().copy())
[5] [5, 5] [5, 5, 5]
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • As you see in the updated question the output depends on which version of the formatting syntax is used. So different syntax, different way of evaluating? Is it a bug or a feature? – Claudio Sep 13 '22 at 23:12
  • 4
    @Claudio calling `format` is like calling `print`, all the arguments to the function are evaluated before the function is called. In the `f-string` however, the arguments are evaluated as they are encountered in the string, hence the different results, both of which are correct given their context. – Nick Sep 13 '22 at 23:26
  • 2
    @Claudio evaluation of expression in `f-strings` is discussed in [PEP 498](https://peps.python.org/pep-0498/#evaluation-order-of-expressions) with a specific mention of side-effects – Nick Sep 13 '22 at 23:40
  • *When you call a function, all the expressions used as parameters are called before the function executes. So all 3 calls to fL2() happen before print is called.* OK, but all 3 calls deliver different results ... so why only the result of last one call to the function is printed at all of the parameter positions? – Claudio Sep 14 '22 at 00:03
  • 1
    @Claudio the way the function is structured, all 3 calls return the *same* object. Since the object is mutable, it can change even after it's been returned. – Mark Ransom Sep 14 '22 at 01:41
  • *It's easy to get print to work the same way as the f-string* - in general case it is not as easy as using `.copy()`. If the type of returned value is not known the parameter for the print must be assigned to a variable and recursively deep-copied before print is called. So the easiest way to make print to behave like an f-string is to use the f-string for printing. Hope you smile now, like I do ... Thanks for your answer and the comments. Maybe someone of f-string programmers explains here the reason for the choice making f-string behave not as .format(). – Claudio Sep 14 '22 at 19:39
  • @Claudio yes I smile too, you have a very pragmatic attitude. But I'm hoping you leave with a better understanding of the mechanics behind the scenes. Because f-strings aren't evaluated like a function call they don't follow the same rules, and Nick gave you a perfect link to explain it. The `copy` is assigning to a variable, an unnamed temporary that gets garbage collected once the `print` is finished. – Mark Ransom Sep 15 '22 at 02:37