Slot functions¶
Implementation in CPython¶
Throughout the CPython core, you’ll find large struct
initialisations that look like this:
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
sizeof(PyFloatObject),
0,
(destructor)float_dealloc, /* tp_dealloc */
(printfunc)float_print, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
(reprfunc)float_repr, /* tp_repr */
...
0, /* tp_init */
0, /* tp_alloc */
float_new, /* tp_new */
};
These are the initialised data of the type
object (float
in this case).
The fields of the struct
are known as “slots”.
Some are descriptive data, but many
(here and in subordinate structures)
are pointers to private functions implementing operations on the objects of that type,
for example:
static PyObject *
float_repr(PyFloatObject *v)
{
return float_str_or_repr(v, 0, 'r');
}
When it is necessary to execute that operation, the function in that slot is invoked:
PyObject *
PyObject_Repr(PyObject *v)
{
...
res = (*Py_TYPE(v)->tp_repr)(v);
...
if (!PyString_Check(res)) {
PyErr_Format(PyExc_TypeError,
"__repr__ returned non-string (type %.200s)",
Py_TYPE(res)->tp_name);
Py_DECREF(res);
return NULL;
}
return res;
}
As we can see,
the function in slot tp_repr
provides the implementation of __repr__
,
for objects of that type.
Generally the special methods from the Python data model are defined in this way.
When a special method is defined in C,
as __repr__
is here for float
,
the entry for “__repr__” in the type dictionary is an object (descriptor) that wraps the slot:
>>> float.__repr__
<slot wrapper '__repr__' of 'float' objects>
When a special method is defined at the Python level, and entered into the dictionary of a type, a C-callable wrapper is created and assigned to the corresponding slot:
>>> class C(object):
... def __repr__(self) : return "myrepr"
...
>>> C.__repr__
<unbound method C.__repr__>
>>> C().__repr__
<bound method C.__repr__ of myrepr>
Notice that in the last output,
in reproducing the contents of the bound method,
CPython called (through the slot) the function we defined in Python,
so that the value to which it is bound is called “myrepr”.
For a plain object
, the result would have been:
>>> object().__repr__
<method-wrapper '__repr__' of object object at 0x0000000002C590C0>
In a type that does not define a given special method, that method’s slot contains a value inherited from its parent:
>>> C.__str__
<slot wrapper '__str__' of 'object' objects>
For the particular case of __str__
,
the implementation (in object
) calls __repr__
,
that is, it calls whatever is in the tp_repr
slot:
>>> C().__str__()
'myrepr'
We may give a Python class a new definition of a special method by assigning it as an attribute, even after definition of the class:
>>> def f(c) : return "mystr"
...
>>> C.__str__ = f
>>> C().__str__()
'mystr'
This attribute then appears in the dictionary of the type, although it is not possible to make such an entry directly:
>>> C.__dict__
mappingproxy({'__module__': '__main__', '__repr__': <function C.__repr__ at 0x0000019BDBA679D8>,
'__dict__': <attribute '__dict__' of 'C' objects>,
'__weakref__': <attribute '__weakref__' of 'C' objects>,
'__doc__': None, '__str__': <function f at 0x0000019BDB78C1E0>})
>>> C.__dict__["__str__"] = f
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
But this is not the only effect:
because the attribute name is one of the special method names,
setting the attribute also enters (a wrapper of) the function f
in the C-level slot.
This is a behaviour built into type.__setattr__
(in the slot tp_setattro
of the type type
).
Built-in types do not generally permit redefinition of special functions:
>>> float.__str__ = f
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'float'
Approach taken by Jython¶
Outline¶
Jython was first developed before Java MethodHandle
entered the language,
so it could provide no direct equivalent to assignable CPython slots.
(We have the choice now, but it would be a big change with user impact.)
Instead, Jython proceeds somewhat oppositely:
rather than building a data structure in the type object,
the base PyObject
defines methods with the same names as the Python special methods,
__repr__
, __add__
, and so on.
These act like the slots, but are expressed directly as actions (code),
rather than as values (pointers to code).
The action of the base implementation of each slot method
has to be whatever CPython would do if it came to a slot and found it empty:
usually it will raise an AttributeError
.
A constraint of the approach is that this type of “slot” may only be assigned
by defining a new (Java) class that extends PyObject
.
So how do we permit “assignment to a slot” at the Python level,
once the target class has been defined?
In the types of object where this must be allowed,
Jython looks in the type’s dictionary for an attribute corresponding to the special method.
This behaviour has to be built into the definition of each special method.
The normal pattern of implementation¶
Any Java sub-class of PyObject
,
representing a particular Python type,
overrides every method corresponding to a slot that that type assigns.
The body of the method calls an implementation method specific to that type.
Jython exposes this method as a special method of the object in Python,
by marking it with a Jython-specific annotation,
through a dictionary entry (descriptor) that will call the implementation method.
For example,
here is the approach taken to define __repr__
in float
:
@Override
public PyString __repr__() {
return float___repr__();
}
@ExposedMethod(doc = BuiltinDocs.float___repr___doc)
final PyString float___repr__() {
return Py.newString(formatDouble(SPEC_REPR));
}
Here, PyObject.__repr__
is a slot overidden by PyFloat
,
and float___repr__
is the implementation that is both
called by the Java slot and exposed in the dictionary of float
as __repr__
.
Java-inheritance and the Derived
classes¶
The curious case of __str__
and __repr__
¶
There is a particularly interesting, and fundamental,
example where the pattern of implementation is broken,
in PyObject
(at the time of writing):
@ExposedMethod(names = "__str__", doc = BuiltinDocs.object___str___doc)
public PyString __repr__() {
return new PyString(toString());
}
@Override
public String toString() {
return object_toString();
}
@ExposedMethod(names = "__repr__", doc = BuiltinDocs.object___repr___doc)
final String object_toString() {
...
return String.format("<%s object at %s>", name, Py.idstr(this));
}
There are three additional odd things here.
One is that the Python name of the Java __repr__
(in the dictionary of the object type) will be __str__
.
A second is that another method not called object___repr__
is exposed as __repr__
.
Finally,
the return type of object_toString
, exposed as __repr__
, is Java String
.
The exposer has to generate code to wrap this return value,
as a PyString
(str
) if it is genuinely a String
,
or as None
if it is (impossibly here) a Java null
:
public class org.python.core.PyObject$object_toString_exposer extends org.python.core.PyBuiltinMethodNarrow {
public org.python.core.PyObject __call__();
Code:
0: aload_0
1: getfield #38 // Field self:Lorg/python/core/PyObject;
4: checkcast #40 // class org/python/core/PyObject
7: invokevirtual #44 // Method org/python/core/PyObject.object_toString:()Ljava/lang/String;
10: dup
11: ifnonnull 21
14: pop
15: getstatic #49 // Field org/python/core/Py.None:Lorg/python/core/PyObject;
18: goto 24
21: invokestatic #53 // Method org/python/core/Py.newString:(Ljava/lang/String;)Lorg/python/core/PyString;
24: areturn
}