0
available_items = {"health potion": 10, "cake of the cure": 5, "green elixir": 20, "strength sandwich": 25, "stamina grains": 15, "power stew": 30}

health_points = 20

health_points += available_items.pop("stamina grains")

When I run this code the value of key "stamina grains" is added and stamina grains is removed from the dictionary available_items.

But Python evaluates expressions left to right so it should remove the key "stamina grains" first therefore there should be nothing added to the health_points.

I am confused on how Python evaluates expressions. Could someone clarify it and also give me some resources about how Python evaluates expressions?

Roberto Caboni
  • 7,252
  • 10
  • 25
  • 39
Muslim
  • 45
  • 5
  • 3
    The `.pop()` method of dictionaries explicitly returns the value corresponding to the key it removed - *that's the whole point of this method*. The return value in this case is simply the integer `15`, it doesn't require the continued existence of the key in order to exist itself. – jasonharper Apr 25 '20 at 07:07
  • The `pop`'s mission is to return the value associated to the key and remove the key from the dict. The very last thing that happens is the affectation of the result (computed on the right hand side) to the variable on the left hand side. See [the doc on evaluation order](https://docs.python.org/3.3/reference/expressions.html#evaluation-order) – Thierry Lathuille Apr 25 '20 at 07:10
  • 1
    btw, your code would be more robust with `health_points += available_items.pop("stamina grains",0)`. that updates with 0, rather than throwing a KeyError if you dont have the item for whatever reason – JL Peyret Apr 25 '20 at 07:25

3 Answers3

3

If you look here: Operator precedence , you can see that "call" is near the bottom of the table, so this is executed first. After execution of this first step, you could rewrite the statement as: health_points += 15.

A little higher is addition, which is executed next (+= is short for addition: health_points = health_points + available_items.pop("stamina grains")). And at the top is assignment, so this is executed last. Resulting in health_points == 35.

vvasch
  • 356
  • 2
  • 8
  • Thanks, but it bothers me I didn't answer a detail in your question. When you evaluate a statement, you need to replace the call with its return value (as I did and others said). The "weird" thing about the pop method is that it has a side-effect: removing the key from the dictionary. You can mentally see this as two things happening at once: returning the value AND removing the key. They don't interfere with each other (when seen from the outside). See the answer of @gilch for the inside, but you shouldn't care about this. – vvasch Apr 25 '20 at 08:09
  • This is not how Python operator precedence works. First, operator precedence controls argument grouping, not execution order. Second, the `+` in `+=` cannot be separated from the `=` - the `+` in `+=` does not have the precedence of addition. Third, the `:=` entry at the top of the table is specifically for `:=` assignment *expressions*, not assignment statements or augmented assignment statement; `+=` is entirely outside the precedence hierarchy for expressions. – user2357112 Apr 25 '20 at 23:51
  • Hi, you know obviously more about this than me and are probably more correct. The assignment expression is not the same as assignment. In my answer I linked to 6.17, in 6.16 it is stated that "while evaluating an assignment, the right-hand side is evaluated before the left-hand side". So that's indeed the reason why the assignment is last. I can also follow your second point, separation of `+=` is just mentally. But can you give an example of why we can't use the table in 6.17 to mentally follow the flow of evaluation? – vvasch Apr 26 '20 at 06:45
  • Operator precedence doesn't control execution order. For example, even though function calls have higher precedence than division, [the division executes first](https://ideone.com/rhebWK) in `1 / 0 + f()`, and `f` does not get called. – user2357112 Apr 26 '20 at 10:04
  • Thank you for the example. You seem right about the execution, but this is internal stuff. I think OP wasn't interested in that (see his comment on gilch's answer, that answer really shows the internals). If I have the errorless expression `1 / 1 + f()`, it evaluates to 3 and not to 1/3. Nor does it raise an error because I try to add the function object f to 0 and call the result, like so `(0 +f )()`. The reason it evaluates to 3 can be derived from the table I linked. So although my explanation wasn't very precise, I 'm sure it could help OP. – vvasch Apr 26 '20 at 14:42
  • `1/0+print('hi')` is a `ZeroDivisionError`, but `1/0**print('hi')` is a `TypeError` and prints "hi". – gilch Apr 27 '20 at 01:16
1

CPython is implemented as a stack machine. If you want to see exactly what order the subexpressions are evaluated, it can be helpful to disassemble them:

>>> from dis import dis
>>> dis('''
... available_items = {"health potion": 10, "cake of the cure": 5, "green elixir": 20, "strength sandwich": 25, "stamina grains": 15, "power stew": 30}
...
... health_points = 20
...
... health_points += available_items.pop("stamina grains")
... ''')
  2           0 LOAD_CONST               0 (10)
              2 LOAD_CONST               1 (5)
              4 LOAD_CONST               2 (20)
              6 LOAD_CONST               3 (25)
              8 LOAD_CONST               4 (15)
             10 LOAD_CONST               5 (30)
             12 LOAD_CONST               6 (('health potion', 'cake of the cure', 'green elixir', 'strength sandwich', 'stamina grains', 'power stew'))
             14 BUILD_CONST_KEY_MAP      6
             16 STORE_NAME               0 (available_items)

  4          18 LOAD_CONST               2 (20)
             20 STORE_NAME               1 (health_points)

  6          22 LOAD_NAME                1 (health_points)
             24 LOAD_NAME                0 (available_items)
             26 LOAD_METHOD              2 (pop)
             28 LOAD_CONST               7 ('stamina grains')
             30 CALL_METHOD              1
             32 INPLACE_ADD
             34 STORE_NAME               1 (health_points)
             36 LOAD_CONST               8 (None)
             38 RETURN_VALUE

While subexpressions (mostly) evaluate left-to-right, operator precendence and parentheses can change this. And the assignment is a statement, not an expression. Even though the += is to the left of the .pop(), you can see that the call happens before the assignment in the compiled bytecode.

Notice that the (pop) call pushes its return value on the stack at the CALL_METHOD instruction, so it can be used by the INPLACE_ADD. At that time, the value is not referenced in the dict, only on the stack, but at no point has the value been lost. And the result of the addition is available to the STORE_NAME instruction after that.

gilch
  • 10,813
  • 1
  • 23
  • 28
1

From the first Google result searching "Python+dictionary +pop" (the emphasis is mine):

The pop() method removes and returns an element from a dictionary having the given key.

So, it is true that pop action is the first one performed, and it is also true that it removes the element from the dictionary, but it also returns that value, that is used to update healt_points variable.

In details, according to evaluation order table:

Python evaluates expressions from left to right. Notice that while evaluating an assignment, the right-hand side is evaluated before the left-hand side.

So, considering the expression

health_points = health_points  + available_items.pop("stamina grains")
  1. We have an assignment, so the right hand side will be evaluated first: health_points + available_items.pop("stamina grains")
  2. On the right side, the evaluation occurs from left to right: health_points is evaluated first, and its value is 20
  3. Now available_items.pop("stamina grains") is evaluated. Call pop method for available_items dictionary
  4. The key "stamina grains" is found, and its value (15) is returned
  5. As a side effect, pop removes the "stamina grains" from the dictionary. The value 15 is returned anyway: it is just an integer representing the reuslt of the method. It is no more bound to the existance of the element of the dictionary
  6. The expression is now health_points = 20 + 15. The right hand side evaluation is the integer 35, that now can be assigned to health_points. That completes the left side evaluation
Roberto Caboni
  • 7,252
  • 10
  • 25
  • 39
  • @Muslim you are welcome. Anyway I completed my answer with a detailed description of the evaluation steps. I hope it helps even more. – Roberto Caboni Apr 25 '20 at 08:45