2

So when I create a class similar to the one below I get unexpected behavior where it appears as if the instances of the beverage class are sharing the "stock" variable, but since the stock variable is declared in the init declaration, I thought it should be limited to an instance.

class Beverage():

    def __init__(self, owner, beverages=[]):
        self.stock = beverages
        self.owner = owner

    def add_drink(self, beverage):
        self.stock.append(beverage)

    def get_drink(self, beverage):
        if beverage in self.stock:
             print "(%s) Has beverage (%s)!" %(self.owner,beverage)
             self.stock.remove(beverage)
        else:
             print "(%s) Does not have beverage (%s)!" %(self.owner,beverage)

her = Beverage("her",["milkshake"])
me = Beverage("I")
you = Beverage("You")
you.add_drink("milkshake")
you.add_drink("punch")
her.get_drink("milkshake")
me.get_drink("milkshake")
me.add_drink("tea")
you.get_drink("milkshake")

Output

(her) Has beverage (milkshake)!
(I) Has beverage (milkshake)!
(You) Does not have beverage (milkshake)!

The output appears to me that it should be:

(her) Has beverage (milkshake)!
(I) Does not have beverage (milkshake)!
(You) has beverage (milkshake)!

Since "you" and "her" added the beverage "milkshake" to their stocks, yet somehow "me" is able to access it as if it is a shared variable. When searching for similar posts on StackOverflow, I turned up a bunch of similar ones in which the coder had defined the variables on the class level, so it made sense that they were shared, but here the stock variable is declared on an instance level so I assumed Python would treat it as instance specific. Any explanation of why the code behaves this way?

John Mathews
  • 231
  • 3
  • 6

2 Answers2

4

me and you are indeed sharing the same beverage list, which is the default value for the argument. This is a well-known gotcha - using a mutable default value will cause the default value to change when the object is mutated.

The equally well-known answer is to do this:

def __init__(self, owner, beverages=None):
    if beverages is None:
        beverages = []
    ...

This guarantees the creation of a brand-new beverages list for each instantiation that doesn't provide one.

EDIT: if not beverages: is not an adequate test. It's entirely possible the user will pass in an empty list, and then the code would act just as it does when there's no argument and create a new list. Hence the more explicit if beverages is None:

As Ignacio Vazquez-Abrams points out this has been discussed ad nauseam in "Least Astonishment" and the Mutable Default Argument

holdenweb
  • 33,305
  • 7
  • 57
  • 77
2

Using lists as default values is a dangerous thing (the list beverages points to the same list). You might fix it by setting the default value of beverages to None, and then adding the line

if not beverages:
    beverages = []
Community
  • 1
  • 1
colcarroll
  • 3,632
  • 17
  • 25
  • 1
    Or `beverages = beverages or []` :) – thefourtheye Mar 05 '14 at 04:02
  • Not an especially good idea - if the user passes an empty list in you will create another one, and if the caller has other references to that list she will wonder why her changes to beverages aren't reflected in the "other" references. Of course the answer is there are two different lists. – holdenweb Mar 05 '14 at 04:30