Ellens Alien Game :: Task 6 :: Don't understand the count error

Hi,
I have a question about the count task on Ellens Alien Game. I don’t understand why the pylint corrector tells me this error :

AssertionError: 1 != 0 : Created a new Alien and called total_aliens_created for it. 0 was returned, but the tests expected that total_aliens_created would equal 1.

Which I don’t understand the error even if on my IDE I manage to count the number of Aliens from my class.

Here is my following program :

class Alien:
    count_object = 0

    def __init__(self, x_coordinate, y_coordinate):
        self.x_coordinate = x_coordinate
        self.y_coordinate = y_coordinate
        self.health = 3
        Alien.count_object += 1

        def hit(self):
        self.health -= 1

    def is_alive(self):
        self.health > 0

    def teleport(self, x, y):
        self.x_coordinate = x
        self.y_coordinate = y

    def collision_detection(self, other):
        pass

    @property
    def total_aliens_created(self):
        return Alien.count_object


def new_aliens_collection(positions):
    new_list = [ ]
    for x, y in positions:
        new_list.append(Alien(x, y))
    return new_list

I am down to discuss for any error or modifications of my program :)

I added code block formatting for you :slight_smile:

The issue is that the tests have a hidden implementation requirement: Alien.total_aliens_created must be an attribute (ie Alien.total_aliens_created = foo) and not a property.

1 Like

Hi @RafalD3molisher :wave:

TL;DR: The cleanest solution by far here is to declare a class attribute, modify that attribute in your __init__(), and then let access be public to callers. Managing or “protecting” attributes (class or instance) is a bit of a Python anti-pattern in (almost all) situations.

If you are interested in the complicated and gory details, read below. :slightly_smiling_face:


To expand a bit on what @IsaacG has said …

With the way the tests are calling Alien.total_aliens_created, it must be defined as a class attribute, not a property object.

@property returns a special descriptor object that uses a class instance to construct a PyProperty_Type. To oversimplify — it cannot resolve/return attributes named cls.<attribute> — it can only resolve/return attributes named self.<attribute>.

For it to resolve cls.<attribute>, it would need to wrap a function that took cls (the class) as a parameter and not self (the instance created from the class). It would also need to be a @classmethod as well as a @property. But as of Python 3.11, you cannot chain @classmethod and @property together.

So there is no easy way in Python 3.11/3.11+ to define a class property via the @property decorator. But it can be done via a metaclass.

:warning: can != should :warning:


I wrote the code below after doing some digging and falling down a rabbit hole. I wanted to know if it was possible to define an @property on a class. I managed to write code that passed the exercise tests, but it is verbose (it defines two property decorators AND a property.setter) and it is neither readable nor elegant:

class AlienMeta(type):     
    _total_aliens_created = 0

    @property
    def total_aliens_created(cls):
        return cls._total_aliens_created

    @total_aliens_created.setter
    def total_aliens_created(cls, value):
        cls._total_aliens_created = value


class Alien(metaclass=AlienMeta):
    
    def __init__(self, x_coordinate, y_coordinate):
        self.x_coordinate = x_coordinate
        self.y_coordinate = y_coordinate
        self.health = 3
        Alien.total_aliens_created += 1

    @property
    def total_aliens_created(self):
        return type(self).total_aliens_created
    
    def hit(self):
        self.health -= 1

    def is_alive(self):
        return self.health > 0

    def teleport(self, x, y):
        self.x_coordinate = x
        self.y_coordinate = y

    def collision_detection(self, other):
        pass

def new_aliens_collection(positions):
    new_list = [ ]
    for x, y in positions:
        new_list.append(Alien(x, y))
    return new_list

I can’t really think of a scenario where I’d want to use it. But it does work to manage the “class property”.

Comment out the @total_aliens_created.setter block and a caller cannot assign a value to Alien.total_aliens_created. Instead, an AttributeError: property 'total_aliens_created' of 'AlienMeta' object has no setter error is thrown.

But that seems like way too much work for way to little value IMHO. :smile:

1 Like

Top thank you :)