Interacting with Python (by example)

Cypclass object are actually also all Python extension types that can be transparently passed to the Python interpreter and manipulated as standard Python objects.

Lets start with the previous example:

cdef cypclass Character:
    int health

    __init__(self, int health):
        self.health = health

    int update_health(self, int amount):
        if -amount > health:
            self.health = 0
        else:
            self.health += amount
        return self.health

cdef cypclass Player(Character):
    int score

    __init__(self, int health):
        self.health = health
        self.score = 0

    Player __iadd__(self, int bonus):
        self.score += bonus
        return self

    void dump(self) with gil:
        print("Player Character (health: %d, score: %d)" % (self.health, self.score))

This code compiles to a Python extension module that can be imported in Python.

From Python, we can then create objects and manipulate them as Python objects:

# # import Player from the extension module
# from [...] import Player

# Create your Player character
# with an initial health of 10
p = Player(10)

We can call their methods, including special methods:

# Pick up a +2 bonus
p += 2

# Take a -1 hit to health
p.update_health(-1)

# Print the stats of your Player
p.dump()

And we can even access their attributes directly:

health, score = p.health, p.score
print(health, score)

A variation

Let's introduce a variantion on the previous example:

instead of

    void dump(self) with gil:
        print("Player Character (health: %d, score: %d)" % (self.health, self.score))

let's use

    object __repr__(self) with gil:
        return "Player Character (health: %d, score %d)" % (self.health, self.score)

    void dump(self) with gil:
        print(self)

We have introduced a method __repr__ that returns a Python object as represented by the object type. More specifically, it returns a Python string representing the object. To allow this, the method is annotated with gil.

The special name __repr__ has no particular meaning in Cython+, so when the object is seen as a cypclass it is just a method like any other.

But when seen as a Python object, it is the special Python method __repr__ that is called whn representing the object.

This allows us to modify the dump method: we can now simply pass self to the print function. At the call site Cython+ will see that print expects a Python object as argument, but since every cypclass object is also a kind of Python object this is not an issue. The print function will eventually call the object's __repr__ method and print the resulting string.

A problematic corner case

Let's say we want to be able to choose the initial score of our Player as well.

We could simply add another __init__ method like so:

    __init__(self, int health, int score):
        self.health = health
        self.score = score

This works, but there are now two __init__ methods available in the Player class: this is called method overloading. 

Since there are now two __init__ methods for the Player class, Cython+ does not know which one should be called when Python calls __init__, and in the end this particular method is not exposed in Python.

This means that when Python now tries to create a Player object, it will by way of its Method Resolution Order algorithm end up trying to call the __init__ method which is inherited from Character, since that's the only one exposed in Python.

Note that removing the first __init__ method will still result in the same problem, because the __init__(self, int health, int score) method does not actually override the __init__(self, int health) method inherited from the Character cypclass. That's because they have different signatures and Cython+ needs to know the types and number of arguments at the call site, but uses a virtual table system to bind the correct method at runtime. In any case, it would still be possible to call p.__init__(10) on an object p of type Player, and this would call the __init__ method defined in Character.

Several solutions to this problem are under investigation. Ideally Cython+ would expose a sort of wrapper method to Python that would forward the arguments to the correct cypclass method based on their number and types. In the meantime, for this specific case a solution is to use a static method such as __new__ instead.

Full code

cdef cypclass Character:
    int health

    __init__(self, int health):
        self.health = health

    int update_health(self, int amount):
        if -amount > health:
            self.health = 0
        else:
            self.health += amount
        return self.health

cdef cypclass Player(Character):
    int score

    __init__(self, int health):
        self.health = health
        self.score = 0

    Player __iadd__(self, int bonus):
        self.score += bonus
        return self

    object __repr__(self) with gil:
        return "Player Character (health: %d, score %d)" % (self.health, self.score)

    void dump(self) with gil:
        print(self)