0

I have defined two classes representing players and games. Also I have testing data and a function which calculate the score of a game. The problem is in such a function: It doesn't creates any global variable nor mutates any input/global data nor perform any IO... It should be a "pure" function, but when executed many times, it outputs diferent things each

Below I give a minimal reprex. The __str__(self): and __repr__(self): are given for better representation but are not part of the problem.

class Player:
    """ Player is kind of a C - struct.  It holds 
    a string which represents a name 
    a int which represent a position in a ranking
    a float which represent points
    """
    def __init__(self, name: str, rank: int, points: float):
        self.name = name
        self.rank = rank
        self.points  = points

    def __str__(self):
        """The error doesn't happend here as far as I have checked"""
        return ' - '.join([self.name, str(self.rank), str(self.points)]) 

    def __repr__(self):
        """The error doesn't happend here as far as I have checked"""
        return self.__str__()

class Game:
    """ Game represents a list of Player. many methods have been remove because they don't contribute to the problem."""
    def __init__(self, list_of_players = []):
        self._list_of_players = list_of_players

    def add_new(self, new) -> None:
        """Append a new value with type checking."""
        if type(new) == Player:
            self._list_of_players.append(new)
        else: 
            raise ValueError('some error')

    @property
    def list_of_players(self): 
        """This getter return the list in order by rank. I think the error can happend here"""
        self._list_of_players.sort(key = lambda c: c.rank, reverse = True)
        return self._list_of_players

    def __str__(self):
        """The error doesn't happend here as far as I have checked"""
        s = '\n'.join(map(str, self.list_of_players))
        return s

    def __repr__(self):
        """The error doesn't happend here as far as I have checked"""
        return self.__str__()

The testing data and the scoring function are this

test_data = [('name0', 2), ('name1', 5), ('name2', 1), ('name3', 0), ('name4', 10), ('name5', 1)]

def calculate_score(data, weight = 1, score_func=lambda x: x + 2):
    """Calculate the score"""
    total_score = 0
    game        = Game()
    for n, i in data:
        score  = score_func(i)
        points = (weight * score) / len(data)
        game.add_new(Player(name = n, rank = i, points = points))

    return game

When I execute many times calculate_score the following happens:

>>> calculate_score(test_data)
name4 - 10 - 2.0
name1 - 5 - 1.1666666666666667
name0 - 2 - 0.6666666666666666
name2 - 1 - 0.5
name5 - 1 - 0.5
name3 - 0 - 0.3333333333333333

>>> calculate_score(test_data)
name4 - 10 - 2.0
name4 - 10 - 2.0
name1 - 5 - 1.1666666666666667
name1 - 5 - 1.1666666666666667
name0 - 2 - 0.6666666666666666
name0 - 2 - 0.6666666666666666
name2 - 1 - 0.5
name5 - 1 - 0.5
name2 - 1 - 0.5
name5 - 1 - 0.5
name3 - 0 - 0.3333333333333333
name3 - 0 - 0.3333333333333333

>>> calculate_score(test_data)
name4 - 10 - 2.0
name4 - 10 - 2.0
name4 - 10 - 2.0
name1 - 5 - 1.1666666666666667
name1 - 5 - 1.1666666666666667
name1 - 5 - 1.1666666666666667
name0 - 2 - 0.6666666666666666
name0 - 2 - 0.6666666666666666
name0 - 2 - 0.6666666666666666
name2 - 1 - 0.5
name5 - 1 - 0.5
name2 - 1 - 0.5
name5 - 1 - 0.5
name2 - 1 - 0.5
name5 - 1 - 0.5
name3 - 0 - 0.3333333333333333
name3 - 0 - 0.3333333333333333
name3 - 0 - 0.3333333333333333

The game variable should be deleted after the execution of calculate_score isn't it?.

I'm using python 3.6 in jupyterlab

lsmor
  • 4,698
  • 17
  • 38
  • Short fix (duplicate answers why it happens): Change `def __init__(self, list_of_players = []):` to `def __init__(self, list_of_players=()):`, and change the assignment in the initializer to `self._list_of_players = list(list_of_players)` (which also protects you from ever inadvertently modifying the caller's `list` when you modify your own copy of it, and allows the caller to pass in arbitrary iterables, rather than requiring they pass a `list`). – ShadowRanger Jul 26 '19 at 15:17
  • `def __init__(self, list_of_players = [])` That function definition isn't doing what you think. See the duplicate question at the top. – John Gordon Jul 26 '19 at 15:23
  • How does that explain why the instance of class Game, which is created inside the function scope, is not deleted when the function is complete? – D_Serg Jul 26 '19 at 15:25
  • @ShadowRanger I agree that yours is a valid solution but there is something I don't understand yet, as @D_serg have posted, why this behaviour is valid when the object is instantiated and then remove as it happens in `calculate_score`?. I would expect the line `game = Game()` to create a fresh new object... also for the "function object" `__init__()` – lsmor Jul 29 '19 at 06:56
  • @ShadowRanger Ok I've just seen that `id(object.__init__())` is the same for all intances, which makes sense... but It is kind of weird that at the end of the day two different instances of `Game` share pointers to the same object. – lsmor Jul 29 '19 at 07:46
  • @Ismor: `id(object.__init__())` has nothing to do with anything; `__init__` always returns `None` (it initializes an existing object, it doesn't create it/return it). The problem is the default `list_of_players=[]` argument; every object created when you don't pass `list_of_players` shares the same `list`. Read the duplicate. – ShadowRanger Jul 29 '19 at 15:40
  • @D_Serg: Why do you expect the instance to be deleted? It's returned, so it'll live as long as the caller keeps the reference. In an interactive interpreter session, even if it isn't explicitly stored it gets stored in `_`. And I'm unclear on how you think it isn't being deleted. – ShadowRanger Jul 29 '19 at 15:44
  • @ShadowRanger As the dulicated answer says: _a function is an object being evaluated on its definition_ so if I created a function as `def myfunc(l=[]) ...`, It'd behave the same as in my question but if I re-executed the line `def myfunc(l=[]) ...` the default argument would reset to empty list because a whole new instance of `myfunc` would be created. I was thinking that every time I execute `game = Game()` a whole new instance of `Game.__init__` would be created which is not true as confirm by checking the id's – lsmor Jul 29 '19 at 16:04
  • You wildly overestimate the usefulness of checking `id`s (bound methods are going to throw you). In any event, functions don't get recreated when you make an object (the bound methods get recreated when you call them on an object, but they share the underlying function), so don't assume they'll do anything special to avoid your problem. – ShadowRanger Jul 29 '19 at 16:11

0 Answers0