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)