2. Definindo Tipos de Extensão: Tutorial¶
O Python permite que o desenvolvedor de um módulo de extensão em C defina novos tipos que podem ser manipulados a partir do código Python, da mesma forma que os tipos embutidos str e list. O código para de todos tipos de extensão segue um padrão, mas há alguns detalhes que você precisa entender antes de começar. Este documento é uma introdução suave ao tópico.
2.1. O básico¶
O tempo de execução do CPython trata todos os objetos Python como variáveis do tipo PyObject*, que funciona como um “tipo base” para todos os objetos Python. A estrutura PyObject em si contém apenas a contagem de referências do objeto e um ponteiro para o “objeto de tipo” correspondente. É nesse objeto de tipo que tudo acontece: ele determina quais funções em C o interpretador chama quando, por exemplo, um atributo é acessado em um objeto, um método é chamado ou o objeto é multiplicado por outro. Essas funções em C são chamadas de “métodos de tipo”.
Então, se você quiser definir um novo tipo de extensão, você precisa criar um novo objeto de tipo.
Esse tipo de coisa só pode ser explicado com exemplo, então aqui está um módulo mínimo, mas completo, que define um novo tipo nomeado Custom dentro de um módulo de extensão C custom:
Nota
O que estamos mostrando aqui é a maneira tradicional de definir tipos de extensão estáticos. Deve ser adequada para a maioria dos usos. A API C também permite definir tipos de extensão alocados em heap usando a função PyType_FromSpec(), que não é abordada neste tutorial.
#define PY_SSIZE_T_CLEAN
#include <Python.h>
typedef struct {
PyObject_HEAD
/* Campos específico do tipo vão aqui. */
} CustomObject;
static PyTypeObject CustomType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom.Custom",
.tp_doc = PyDoc_STR("Custom objects"),
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
};
static int
custom_module_exec(PyObject *m)
{
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot custom_module_slots[] = {
{Py_mod_exec, custom_module_exec},
// Só use isso quando estiver usando tipos estáticos
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef custom_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = custom_module_slots,
};
PyMODINIT_FUNC
PyInit_custom(void)
{
return PyModuleDef_Init(&custom_module);
}
Isso é bastante informação para assimilar de uma só vez, mas algumas partes devem parecer familiares pelo que foi visto no capítulo anterior. Este arquivo define três elementos:
O que um objeto
Customcontém: esta é uma estruturaCustomObject, alocada uma vez para cada instância deCustom.Como o tipo
Customse comporta: esta é a estruturaCustomType, que define um conjunto de sinalizadores e ponteiros de função que o interpretador inspeciona quando operações específicas são solicitadas.Como definir e executar o módulo
custom: esta é a funçãoPyInit_custome a estrutura associadacustom_modulepara definir o módulo, e a funçãocustom_module_execpara configurar um novo objeto de módulo.
O primeiro bit é
typedef struct {
PyObject_HEAD
} CustomObject;
Isto é o que um objeto Custom conterá. PyObject_HEAD é obrigatório no início de cada estrutura de objeto e define um campo chamado ob_base do tipo PyObject, contendo um ponteiro para um objeto de tipo e uma contagem de referências (esses podem ser acessados usando os macros Py_TYPE e Py_REFCNT, respectivamente). O objetivo do macro é abstrair o layout e permitir campos adicionais em construções de depuração.
Nota
Não há ponto e vírgula acima após o macro PyObject_HEAD. Tenha cuidado para não adicionar um por engano: alguns compiladores podem emitir erros.
Obviamente, objetos geralmente armazenam dados adicionais além do padrão PyObject_HEAD boilerplate; por exemplo, aqui está a definição do padrão Python floats:
typedef struct {
PyObject_HEAD
double ob_fval;
} PyFloatObject;
O segundo bit é a definição do objeto de tipo.
estático PyTypeObject CustomType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom.Custom",
.tp_doc = PyDoc_STR("Custom objeto"),
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_new = PyType_GenericNew,
};
Nota
Recomendamos usar inicializadores nomeados no estilo C99, como mostrado acima, para evitar listar todos os campos de PyTypeObject com os quais você não precisa se preocupar e também para não depender da ordem de declaração desses campos
The actual definition of PyTypeObject in object.h has
many more fields than the definition above. The
remaining fields will be filled with zeros by the C compiler, and it’s
common practice to not specify them explicitly unless you need them.
Vamos separá-lo, um campo de cada vez
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
Essa linha é um boilerplate obrigatório para inicializar o campo ob_base mencionado acima.
.tp_name = "custom.Custom",
O nome do nosso tipo. Ele aparecerá na representação textual padrão do nosso objeto e em algumas mensagens erro, por exemplo:
>>> "" + custom.Custom()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "custom.Custom") to str
Observe que o nome é um nome pontilhado que inclui o nome módulo e o nome do tipo dentro do módulo. O módulo, neste caso, é custom e o tipo é Custom, portanto, definimos o nome do tipo como custom.Custom. O uso do pontilhado real caminho de importação é importante para tornar seu tipo compatível com o pydoc e os módulos pickle.
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
Isso serve para que o Python saiba quanta memória deve ser alocada ao criar novas instâncias do Custom. O tp_itemsize é usado somente para objeto de tamanho variável e, caso contrário, deve ser zero.
Nota
Se você quiser que seu tipo possa ser subclassificado a partir do Python, e seu tipo tiver o mesmo tp_basicsize que seu tipo base, você poderá ter problemas com herança múltipla. Uma subclasse Python do seu tipo terá de listar o seu tipo primeiro em __bases__, caso contrário ela não conseguirá chamar o método __new__() do seu tipo sem gerar um erro. Você pode evitar esse problema garantindo que seu tipo tenha um valor de tp_basicsize maior que o do tipo base. Na maior parte do tempo, isso já será verdadeiro, pois ou o tipo base será object, ou você estará adicionando membros de dados ao seu tipo base e, portanto, aumentando seu tamanho.
Definimos os sinalizadores da classe como Py_TPFLAGS_DEFAULT.
.tp_flags = Py_TPFLAGS_DEFAULT,
Todos os tipos devem incluir essa constante em seus sinalizadores. Ela habilita todos os membros definidos até pelo menos o Python 3.3. Se você precisar de membros adicionais, será necessário fazer um OR com os sinalizadores correspondentes.
Fornecemos uma docstring para o tipo em tp_doc tp_doc.
.tp_doc = PyDoc_STR("Custom objects"),
Para habilitar a criação de objetos, precisamos fornecer um manipulador para tp_new. Esse é o equivalente ao método Python __new__(), mas precisa ser especificado explicitamente. Neste caso, podemos simplesmente usar a implementação padrão fornecida pela função da API PyType_GenericNew().
.tp_new = PyType_GenericNew,
Todo o restante do arquivo deve ser familiar, exceto por alguns códigos em custom_module_exec():
se (PyType_Ready(&CustomType) < 0) {
retorno -1;
}
Isso inicializa o tipo Custom, preenchendo vários membros com os valores padrão apropriados, incluindo ob_type, que definimos inicialmente como NULL.
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
Isso adiciona o tipo ao dicionário do módulo.
Isso nos permite criar instâncias de Custom chamando a classe Custom:
>>> import custom
>>> mycustom = custom.Custom()
É isso! Só resta compilá-lo; coloque o código acima em um arquivo chamado custom.c,
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "custom"
version = "1"
Em um arquivo chamado pyproject.toml, e
from setuptools import Extension, setup
setup(ext_modules=[Extension("custom", ["custom.c"])])
Em um arquivo chamado setup.py; e então digitando
$ python -m pip install .
no console deverá gerar um arquivo custom.so em um subdiretório e instalá-lo; agora abra o Python — você já deve conseguir import custom e experimentar objetos Custom.
Isso não foi tão difícil, foi?
Naturalmente, o tipo Custom atual é bastante desinteressante. Não tem dados e não faz nada. Não pode nem ser subclassificado.
2.2. Adicionando dados e métodos ao exemplo básico¶
Vamos estender o exemplo básico para adicionar alguns dados e métodos. Também vamos tornar o tipo utilizável como uma classe base. Iremos criar um novo módulo, custom2, que adiciona esses recursos:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */
typedef struct {
PyObject_HEAD
PyObject *first; /* first name */
PyObject *last; /* last name */
int number;
} CustomObject;
static void
Custom_dealloc(PyObject *op)
{
CustomObject *self = (CustomObject *) op;
Py_XDECREF(self->first);
Py_XDECREF(self->last);
Py_TYPE(self)->tp_free(self);
}
static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
CustomObject *self;
self = (CustomObject *) type->tp_alloc(type, 0);
if (self != NULL) {
self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
if (self->first == NULL) {
Py_DECREF(self);
return NULL;
}
self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
if (self->last == NULL) {
Py_DECREF(self);
return NULL;
}
self->number = 0;
}
return (PyObject *) self;
}
static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
CustomObject *self = (CustomObject *) op;
static char *kwlist[] = {"first", "last", "number", NULL};
PyObject *first = NULL, *last = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
&first, &last,
&self->number))
return -1;
if (first) {
Py_XSETREF(self->first, Py_NewRef(first));
}
if (last) {
Py_XSETREF(self->last, Py_NewRef(last));
}
return 0;
}
static PyMemberDef Custom_members[] = {
{"first", Py_T_OBJECT_EX, offsetof(CustomObject, first), 0,
"first name"},
{"last", Py_T_OBJECT_EX, offsetof(CustomObject, last), 0,
"last name"},
{"number", Py_T_INT, offsetof(CustomObject, number), 0,
"custom number"},
{NULL} /* Sentinel */
};
static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
CustomObject *self = (CustomObject *) op;
if (self->first == NULL) {
PyErr_SetString(PyExc_AttributeError, "first");
return NULL;
}
if (self->last == NULL) {
PyErr_SetString(PyExc_AttributeError, "last");
return NULL;
}
return PyUnicode_FromFormat("%S %S", self->first, self->last);
}
static PyMethodDef Custom_methods[] = {
{"name", Custom_name, METH_NOARGS,
"Return the name, combining the first and last name"
},
{NULL} /* Sentinel */
};
static PyTypeObject CustomType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom2.Custom",
.tp_doc = PyDoc_STR("Custom objects"),
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_new = Custom_new,
.tp_init = Custom_init,
.tp_dealloc = Custom_dealloc,
.tp_members = Custom_members,
.tp_methods = Custom_methods,
};
static int
custom_module_exec(PyObject *m)
{
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot custom_module_slots[] = {
{Py_mod_exec, custom_module_exec},
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef custom_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom2",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = custom_module_slots,
};
PyMODINIT_FUNC
PyInit_custom2(void)
{
return PyModuleDef_Init(&custom_module);
}
Esta versão do módulo possui várias alterações.
O tipo Custom agora possui três atributos de dados em sua estrutura C: first, last e number. As variáveis first e last são strings Python contendo o primeiro e o último nome. O atributo number é um inteiro em C.
A estrutura do objeto é atualizada de acordo
typedef struct {
PyObject_HEAD
PyObject *first; /* first name */
PyObject *last; /* last name */
int number;
} CustomObject;
Como agora temos dados para gerenciar, precisamos ter mais cuidado com a alocação e a desalocação do objeto. No mínimo, precisamos de método de desalocação:
static void
Custom_dealloc(PyObject *op)
{
CustomObject *self = (CustomObject *) op;
Py_XDECREF(self->first);
Py_XDECREF(self->last);
Py_TYPE(self)->tp_free(self);
}
que é atribuído ao membro tp_dealloc:
tp_dealloc = Custom_dealloc,
Esse método primeiro limpa a contagem de referências dos dois atributos Python. Py_XDECREF`lida corretamente com o caso em que seu argumento é ``NULL`() (o que pode acontecer aqui se tp_new falhou no meio do processo). Em seguida, ele chama o membro tp_free do tipo do objeto (obtido por Py_TYPE(self)) para liberar a memória do objeto. Observe que o tipo do objeto pode não ser CustomType, pois o objeto pode ser uma instância de uma subclasse.
Nota
O cast explícito para CustomObject * acima é necessário porque definimos Custom_dealloc para receber um argumento do tipo PyObject *, enquanto o ponteiro de função tp_dealloc``espera receber um argumento ``PyObject *. Ao atribuir nossa função ao slot tp_dealloc de um tipo, estamos declarando que ela só pode ser chamada com instâncias da classe CustomObject; portanto, o cast para (CustomObject *) é seguro. Isso é polimorfismo orientado a objetos, em C!
Em códigos existentes, ou em versões anteriores deste tutorial, você pode consultar funções semelhantes receberem diretamente um ponteiro para estrutura de objeto do subtipo (CustomObject*), assim:
Custom_dealloc(CustomObject *self)
{
Py_XDECREF(self->first);
Py_XDECREF(self->last);
Py_TYPE(self)->tp_free((PyObject *) self);
}
...
.tp_dealloc = (destructor) Custom_dealloc,
Isso faz a mesma coisa em todas as arquiteturas que o CPython oferece suporte, mas, de acordo com o padrão da linguagem C, isso invoca um comportamento indefinido.
Queremos nos certificar de que o primeiro e o último nome sejam inicializados como strings vazias, portanto, fornecemos uma implementação:: tp_new
static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
CustomObject *self;
self = (CustomObject *) type->tp_alloc(type, 0);
if (self != NULL) {
self->first = PyUnicode_FromString("");
if (self->first == NULL) {
Py_DECREF(self);
return NULL;
}
self->last = PyUnicode_FromString("");
if (self->last == NULL) {
Py_DECREF(self);
return NULL;
}
self->number = 0;
}
return (PyObject *) self;
}
e instale-o no membro:: tp_new
.tp_new = Custom_new,
O manipulador tp_new é responsável por criar (em oposição a inicializar) os objetos do tipo. Ele é exposto no Python como o método __new__(). Não é obrigatório definir um membro tp_new e, de fato, muitos tipos de extensão simplesmente reutilizam PyType_GenericNew(), como na primeira versão do tipo Custom acima. Neste caso, usamos o manipulador tp_new para inicializar os atributos first e last com valores padrão que não sejam NULL.
O tp_new recebe o tipo que está sendo instanciado (não necessariamente CustomType, caso uma subclasse esteja sendo instanciada) e quaisquer argumentos passados quando o tipo foi chamado, e deve retornar a instância criada. Manipuladores tp_new sempre aceitam argumentos posicionais e argumentos nomeados, mas frequentemente os ignoram, deixando o tratamento dos argumentos para os métodos inicializadores (ou seja, tp_init em C ou __init__ em Python).
Nota
tp_new não deve chamar tp_init explicitamente, pois o interpretador fará isso por conta própria.
A implementação de tp_new chama o slot tp_alloc para alocar memória:
self = (CustomObject *) type->tp_alloc(type, 0);
Como a alocação de memória pode falhar, precisamos verificar se o resultado de tp_alloc é NULL antes de prosseguir.
Nota
Nós não preenchemos o slot tp_alloc por conta própria. Em vez disso, PyType_Ready() o preenche herdando-o da nossa classe base, que por padrão é object. A maioria dos tipos usa a estratégia de alocação padrão.
Nota
If you are creating a co-operative tp_new (one
that calls a base type’s tp_new or __new__()),
you must not try to determine what method to call using method resolution
order at runtime. Always statically determine what type you are going to
call, and call its tp_new directly, or via
type->tp_base->tp_new. If you do not do this, Python subclasses of your
type that also inherit from other Python-defined classes may not work correctly.
(Specifically, you may not be able to create instances of such subclasses
without getting a TypeError.)
Também definimos uma função de inicialização que aceita argumentos para fornecer valores iniciais para nosso instância:
static intCustom_init(PyObject *op, PyObject *args, PyObject *kwds){ CustomObject *self = (CustomObject *) op; static char *kwlist[] = {"first", "last", "number", NULL}; PyObject *first = NULL, *last = NULL, *tmp; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist, &first, &last, &self->number)) return -1; if (first) { tmp = self->first; Py_INCREF(first); self->first = first; Py_XDECREF(tmp); } if (last) { tmp = self->last; Py_INCREF(last); self->last = last; Py_XDECREF(tmp); } return 0;}
preenchendo o slot :: tp_init.
.tp_init = Custom_init,
O slot tp_init é exposto em Python como o método __init__(). Ele é usado para inicializar um objeto depois que ele é criado. Inicializadores sempre aceitam argumentos posicionais e argumentos nomeados, e devem retornar 0 em caso de sucesso ou -1 em caso de erro.
Ao contrário do manipulador tp_new, não há garantia de que seja chamado (por exemplo, o módulo pickle por padrão não chama __init__() em instâncias desserializadas). Além disso, tp_init pode ser chamado várias vezes. Qualquer um pode chamar o método __init__() em nossos objetos. Por esse motivo, precisamos ter cuidado redobrado ao atribuir os novos valores de atributos. Poderíamos ficar tentados, por exemplo, a atribuir o membro first assim:
if (first) { Py_XDECREF(self->first); Py_INCREF(first); self->first = first;}
But this would be risky. Our type doesn’t restrict the type of the
first member, so it could be any kind of object. It could have a
destructor that causes code to be executed that tries to access the
first member; or that destructor could detach the
thread state and let arbitrary code run in other
threads that accesses and modifies our object.
Para sermos extremamente cautelosos e nos proteger dessa possibilidade, quase sempre atribuímos novamente os membros antes de decrementar a contagem de referências. Quando não precisamos fazer isso?
quando sabemos absolutamente que a contagem de referência é maior que 1;
when we know that deallocation of the object [1] will neither detach the thread state nor cause any calls back into our type’s code;
quando estamos decrementando uma contagem de referências em um manipulador
tp_deallocde um tipo que não oferece suporte à coleta de lixo cíclica [2].
Queremos expor nossas variáveis de instância como atributos. Há várias maneiras de fazer isso. A forma mais simples é estabelecer definições de membros:
static PyMemberDef Custom_members[] = { {"first", Py_T_OBJECT_EX, offsetof(CustomObject, first), 0, "first name"}, {"last", Py_T_OBJECT_EX, offsetof(CustomObject, last), 0, "last name"}, {"number", Py_T_INT, offsetof(CustomObject, number), 0, "custom number"}, {NULL} /* Sentinel */};
e colocar as definições no slot tp_members:
.tp_members = Custom_members,
Cada definição de membro possui um nome de membro, um tipo, um deslocamento, sinalizadores de acesso e uma string de documentação. Consulte a seção Generic Attribute Management abaixo para mais detalhes.
Uma desvantagem dessa abordagem é que ela não fornece uma forma de restringir os tipos de objetos que podem ser atribuídos aos atributos em Python. Esperamos que os nomes first e last sejam strings, mas qualquer objeto Python pode ser atribuído. Além disso, os atributos podem ser excluídos, fazendo com que os ponteiros em C sejam definidos como NULL. Mesmo que possamos garantir que os membros sejam inicializados com valores não NULL, eles ainda podem acabar sendo definidos como NULL caso os atributos sejam apagados.
Definimos um único método,:meth:!Custom.name, que exibe o nome do objeto como a concatenação do primeiro nome com o último nome
static PyObject *Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy)){ CustomObject *self = (CustomObject *) op; if (self->first == NULL) { PyErr_SetString(PyExc_AttributeError, "first"); return NULL; } if (self->last == NULL) { PyErr_SetString(PyExc_AttributeError, "last"); return NULL; } return PyUnicode_FromFormat("%S %S", self->first, self->last);}
O método é implementado como uma função em C que recebe uma instância de Custom (ou de uma subclasse de Custom) como primeiro argumento. Métodos sempre recebem a instância como primeiro argumento. Eles também costumam aceitar argumentos posicionais e nomeados, mas, neste caso, não recebemos nenhum e não precisamos aceitar uma tupla de argumentos posicionais nem um dicionário de argumentos nomeados. Este método equivale ao método Python:
def name(self): return "%s %s" % (self.first, self.last)
Observe que precisamos verificar a possibilidade de que nossos atributos first e last`estejam com valor ``NULL`. Isso ocorre porque eles podem ser excluídos e, nesse caso, ficam definidos como NULL. Seria melhor impedir a exclusão desses atributos e restringir seus valores para que sejam sempre strings. Veremos como fazer isso na próxima seção.
Agora que definimos o método, precisamos criar uma array de definições de métodos:
static PyMethodDef Custom_methods[] = {
{"name", Custom_name, METH_NOARGS,
"Return the name, combining the first and last name"
},
{NULL} /* Sentinel */
};
note que usamos o sinalizador METH_NOARGS para indicar que o método não espera argumentos além de self)
e atribuí-lo para o slot:: tp_methods
.tp_methods = Custom_methods,
Por fim, tornaremos nosso tipo utilizável como classe base para herança. Escrevemos nossos métodos com cuidado até agora, garantindo que eles não façam suposições sobre o tipo do objeto que está sendo criado ou utilizado; portanto, tudo o que precisamos fazer é adicionar a:c:macro:Py_TPFLAGS_BASETYPE à definição dos sinalizadores da nossa classe:
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
Renomeamos PyInit_custom() para PyInit_custom2(), atualizamos o nome do módulo na estrutura PyModuleDef, e atualizamos o nome completo da classe na estrutura PyTypeObject.
Finalmente, atualizamos nosso arquivo setup.py para incluir o novo módulo,
from setuptools import Extension, setupsetup(ext_modules=[ Extension("custom", ["custom.c"]), Extension("custom2", ["custom2.c"]),])
então podemos instalar novamente para realizarmos o import custom2
$ python -m pip install .
2.3. Fornecendo controle mais preciso sobre atributos de dados¶
Nesta seção, forneceremos um controle mais preciso sobre como os atributos first e last são definidos no exemplo Custom. Na versão anterior do nosso módulo, as variáveis de instância first e last podiam receber valores que não fossem strings ou até mesmo ser excluídas. Queremos garantir que esses atributos sempre contenham strings.
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */
typedef struct {
PyObject_HEAD
PyObject *first; /* first name */
PyObject *last; /* last name */
int number;
} CustomObject;
static void
Custom_dealloc(PyObject *op)
{
CustomObject *self = (CustomObject *) op;
Py_XDECREF(self->first);
Py_XDECREF(self->last);
Py_TYPE(self)->tp_free(self);
}
static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
CustomObject *self;
self = (CustomObject *) type->tp_alloc(type, 0);
if (self != NULL) {
self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
if (self->first == NULL) {
Py_DECREF(self);
return NULL;
}
self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
if (self->last == NULL) {
Py_DECREF(self);
return NULL;
}
self->number = 0;
}
return (PyObject *) self;
}
static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
CustomObject *self = (CustomObject *) op;
static char *kwlist[] = {"first", "last", "number", NULL};
PyObject *first = NULL, *last = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
&first, &last,
&self->number))
return -1;
if (first) {
Py_SETREF(self->first, Py_NewRef(first));
}
if (last) {
Py_SETREF(self->last, Py_NewRef(last));
}
return 0;
}
static PyMemberDef Custom_members[] = {
{"number", Py_T_INT, offsetof(CustomObject, number), 0,
"custom number"},
{NULL} /* Sentinel */
};
static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
CustomObject *self = (CustomObject *) op;
return Py_NewRef(self->first);
}
static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
CustomObject *self = (CustomObject *) op;
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
return -1;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"The first attribute value must be a string");
return -1;
}
Py_SETREF(self->first, Py_NewRef(value));
return 0;
}
static PyObject *
Custom_getlast(PyObject *op, void *closure)
{
CustomObject *self = (CustomObject *) op;
return Py_NewRef(self->last);
}
static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
CustomObject *self = (CustomObject *) op;
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
return -1;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"The last attribute value must be a string");
return -1;
}
Py_SETREF(self->last, Py_NewRef(value));
return 0;
}
static PyGetSetDef Custom_getsetters[] = {
{"first", Custom_getfirst, Custom_setfirst,
"first name", NULL},
{"last", Custom_getlast, Custom_setlast,
"last name", NULL},
{NULL} /* Sentinel */
};
static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
CustomObject *self = (CustomObject *) op;
return PyUnicode_FromFormat("%S %S", self->first, self->last);
}
static PyMethodDef Custom_methods[] = {
{"name", Custom_name, METH_NOARGS,
"Return the name, combining the first and last name"
},
{NULL} /* Sentinel */
};
static PyTypeObject CustomType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom3.Custom",
.tp_doc = PyDoc_STR("Custom objects"),
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_new = Custom_new,
.tp_init = Custom_init,
.tp_dealloc = Custom_dealloc,
.tp_members = Custom_members,
.tp_methods = Custom_methods,
.tp_getset = Custom_getsetters,
};
static int
custom_module_exec(PyObject *m)
{
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot custom_module_slots[] = {
{Py_mod_exec, custom_module_exec},
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef custom_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom3",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = custom_module_slots,
};
PyMODINIT_FUNC
PyInit_custom3(void)
{
return PyModuleDef_Init(&custom_module);
}
Para oferecer um controle maior sobre os atributos:attr:!first`e :attr:!last`, usaremos funções personalizadas de acesso (getters) e modificação (setters). A seguir estão as funções para obter e definir o atributo first:
static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
CustomObject *self = (CustomObject *) op;
Py_INCREF(self->first);
return self->first;
}
static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
CustomObject *self = (CustomObject *) op;
PyObject *tmp;
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
return -1;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"The first attribute value must be a string");
return -1;
}
tmp = self->first;
Py_INCREF(value);
self->first = value;
Py_DECREF(tmp);
return 0;
}
A função getter recebe um objeto Custom e um closure, que é um ponteiro para void. Neste caso, o closure é ignorado. (O closure oferece suporte a um modo de usar mais avançado, no qual dados de definição são passados ao getter e ao setter. Isso pode, por exemplo, permitir que um único conjunto de funções getter e setter decida qual atributo obter ou definir com base nos dados presentes no closure.)
A função setter recebe o objeto Custom, o novo valor e o closure. O novo valor pode ser NULL, caso em que o atributo está sendo excluído. No nosso setter, levantamos um erro se o atributo for excluído ou se o novo valor não for uma string.
Nós criar um vetor de estruturas:: PyGetSetDef
static PyGetSetDef Custom_getsetters[] = {
{"first", Custom_getfirst, Custom_setfirst,
"first name", NULL},
{"last", Custom_getlast, Custom_setlast,
"last name", NULL},
{NULL} /* Sentinel */
};
e registra isso num slot tp_getset:
.tp_getset = Custom_getsetters,
A última entrada em uma estrutura PyGetSetDef é o “closure” mencionado acima. Neste caso, não estamos usando um closure, então simplesmente passamos NULL.
Também removemos as definições de membros para esses atributos:
static PyMemberDef Custom_members[] = {
{"number", Py_T_INT, offsetof(CustomObject, number), 0,
"custom number"},
{NULL} /* Sentinel */
};
Também precisamos atualizar o manipulador :c:member:`~PyTypeObject.tp_init`para permitir apenas strings [3] como valores passados:
static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
CustomObject *self = (CustomObject *) op;
static char *kwlist[] = {"first", "last", "number", NULL};
PyObject *first = NULL, *last = NULL, *tmp;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
&first, &last,
&self->number))
return -1;
if (first) {
tmp = self->first;
Py_INCREF(first);
self->first = first;
Py_DECREF(tmp);
}
if (last) {
tmp = self->last;
Py_INCREF(last);
self->last = last;
Py_DECREF(tmp);
}
return 0;
}
Com essas alterações, podemos garantir que os membros first e last nunca sejam NULL, de modo que podemos remover quase todas as verificações de valores NULL. Isso significa que a maioria das chamadas para:c:func:Py_XDECREF`pode ser substituída por chamadas para :c:func:`Py_DECREF. O único lugar em que não podemos substituir essas chamadas é na implementação de tp_dealloc, onde existe a possibilidade de que a inicialização desses membros tenha falhado em tp_new.
Também renomeamos a função de inicialização do módulo e o nome do módulo dentro da função de inicialização, como fizemos anteriormente, e adicionamos uma definição extra ao arquivo setup.py.
2.4. Apoiando a coleta de lixo cíclica¶
Python has a cyclic garbage collector (GC) that can identify unneeded objects even when their reference counts are not zero. This can happen when objects are involved in cycles. For example, consider:
>>> l = []
>>> l.append(l)
>>> del l
Neste exemplo, criamos uma lista que contém a si mesma. Quando a apagamos, ela ainda possui uma referência apontando para si própria. Sua contagem de referências não cai para zero. Felizmente, o coletor de lixo cíclico do Python acabará percebendo que a lista é lixo e irá liberá-la.
No segunda versão do exemplo:class:!Custom, permitimos que qualquer tipo de objeto fosse armazenado nos atributos first ou last [4]. Além disso, na segunda e na terceira versões, permitimos herdar de Custom, e subclasses podem adicionar atributos arbitrários. Por qualquer um desses dois motivos, instâncias de Custom podem participar de ciclos:
>>> import custom3
>>> class Derived(custom3.Custom): pass
...
>>> n = Derived()
>>> n.some_attribute = n
Para permitir que uma instância de Custom que participa de um ciclo de referência seja corretamente detectada e coletada pelo coletor de lixo cíclico, nosso tipo Custom precisa preencher dois slots adicionais e ativar um sinalizador que habilita esses slots:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */
typedef struct {
PyObject_HEAD
PyObject *first; /* first name */
PyObject *last; /* last name */
int number;
} CustomObject;
static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
CustomObject *self = (CustomObject *) op;
Py_VISIT(self->first);
Py_VISIT(self->last);
return 0;
}
static int
Custom_clear(PyObject *op)
{
CustomObject *self = (CustomObject *) op;
Py_CLEAR(self->first);
Py_CLEAR(self->last);
return 0;
}
static void
Custom_dealloc(PyObject *op)
{
PyObject_GC_UnTrack(op);
(void)Custom_clear(op);
Py_TYPE(op)->tp_free(op);
}
static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
CustomObject *self;
self = (CustomObject *) type->tp_alloc(type, 0);
if (self != NULL) {
self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
if (self->first == NULL) {
Py_DECREF(self);
return NULL;
}
self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
if (self->last == NULL) {
Py_DECREF(self);
return NULL;
}
self->number = 0;
}
return (PyObject *) self;
}
static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
CustomObject *self = (CustomObject *) op;
static char *kwlist[] = {"first", "last", "number", NULL};
PyObject *first = NULL, *last = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
&first, &last,
&self->number))
return -1;
if (first) {
Py_SETREF(self->first, Py_NewRef(first));
}
if (last) {
Py_SETREF(self->last, Py_NewRef(last));
}
return 0;
}
static PyMemberDef Custom_members[] = {
{"number", Py_T_INT, offsetof(CustomObject, number), 0,
"custom number"},
{NULL} /* Sentinel */
};
static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
CustomObject *self = (CustomObject *) op;
return Py_NewRef(self->first);
}
static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
CustomObject *self = (CustomObject *) op;
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
return -1;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"The first attribute value must be a string");
return -1;
}
Py_XSETREF(self->first, Py_NewRef(value));
return 0;
}
static PyObject *
Custom_getlast(PyObject *op, void *closure)
{
CustomObject *self = (CustomObject *) op;
return Py_NewRef(self->last);
}
static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
CustomObject *self = (CustomObject *) op;
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
return -1;
}
if (!PyUnicode_Check(value)) {
PyErr_SetString(PyExc_TypeError,
"The last attribute value must be a string");
return -1;
}
Py_XSETREF(self->last, Py_NewRef(value));
return 0;
}
static PyGetSetDef Custom_getsetters[] = {
{"first", Custom_getfirst, Custom_setfirst,
"first name", NULL},
{"last", Custom_getlast, Custom_setlast,
"last name", NULL},
{NULL} /* Sentinel */
};
static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
CustomObject *self = (CustomObject *) op;
return PyUnicode_FromFormat("%S %S", self->first, self->last);
}
static PyMethodDef Custom_methods[] = {
{"name", Custom_name, METH_NOARGS,
"Return the name, combining the first and last name"
},
{NULL} /* Sentinel */
};
static PyTypeObject CustomType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "custom4.Custom",
.tp_doc = PyDoc_STR("Custom objects"),
.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
.tp_new = Custom_new,
.tp_init = Custom_init,
.tp_dealloc = Custom_dealloc,
.tp_traverse = Custom_traverse,
.tp_clear = Custom_clear,
.tp_members = Custom_members,
.tp_methods = Custom_methods,
.tp_getset = Custom_getsetters,
};
static int
custom_module_exec(PyObject *m)
{
if (PyType_Ready(&CustomType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot custom_module_slots[] = {
{Py_mod_exec, custom_module_exec},
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef custom_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "custom4",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = custom_module_slots,
};
PyMODINIT_FUNC
PyInit_custom4(void)
{
return PyModuleDef_Init(&custom_module);
}
Primeiro, o método de travessia informa ao coletor de lixo cíclico quais subobjetos podem participar de ciclos:
static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
CustomObject *self = (CustomObject *) op;
int vret;
if (self->first) {
vret = visit(self->first, arg);
if (vret != 0)
return vret;
}
if (self->last) {
vret = visit(self->last, arg);
if (vret != 0)
return vret;
}
return 0;
}
Para cada subobjeto que pode participar de ciclos, precisamos chamar a função visit(), que é passada para o método de travessia. A função visit() recebe como argumentos o subobjeto e o argumento extra arg fornecido ao método de travessia. Ela retorna um valor inteiro que deve ser retornado se for diferente de zero.
O Python fornece o macro Py_VISIT(), que automatiza as chamadas às funções visit. Com Py_VISIT(), podemos minimizar a quantidade de boilerplate em Custom_traverse:
static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
CustomObject *self = (CustomObject *) op;
Py_VISIT(self->first);
Py_VISIT(self->last);
return 0;
}
Nota
A implementação de tp_traverse deve nomear seus argumentos exatamente como visit e arg para que seja possível usar Py_VISIT().
Segundo, precisamos fornecer um método para limpar quaisquer subobjetos que possam participar de ciclos:
static int
Custom_clear(PyObject *op)
{
CustomObject *self = (CustomObject *) op;
Py_CLEAR(self->first);
Py_CLEAR(self->last);
return 0;
}
Observe o uso do macro Py_CLEAR(). Ele é a forma recomendada e segura de limpar atributos de dados de tipos arbitrários enquanto decrementa suas contagens de referências. Se você chamasse Py_XDECREF() no atributo antes de defini-lo como NULL, haveria a possibilidade de que o destrutor do atributo chamasse novamente algum código que lesse o atributo (especialmente se houver um ciclo de referência).
Nota
Você poderia emular Py_CLEAR() ao escrever:
PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);
Apesar disso, é muito mais fácil e menos propenso a erros, usar sempre Py_CLEAR() ao excluir um atributo. Não tente micro‐otimizar às custas da robustez!
O desalocador Custom_dealloc pode executar código arbitrário ao limpar atributos. Isso significa que o GC pode ser acionado dentro da função. Como o GC presume que a contagem de referências não é zero, precisamos remover o objeto do rastreamento do GC chamando PyObject_GC_UnTrack`antes de limpar os membros. A seguir está nossa versão reimplementada do desalocador usando :c:func:`PyObject_GC_UnTrack() e Custom_clear:
static void
Custom_dealloc(PyObject *op)
{
PyObject_GC_UnTrack(op);
(void)Custom_clear(op);
Py_TYPE(op)->tp_free(op);
}
Por fim, adicionamos o macro Py_TPFLAGS_HAVE_GC aos sinalizadores da classe:
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
Isso é praticamente tudo. Se tivéssemos escrito manipuladores personalizados para tp_alloc ou tp_free, precisaríamos adaptá-los para a coleta de lixo cíclica. A maior parte das extensões usará as versões fornecidas automaticamente.
2.5. Criando subclasses de outros tipos¶
É possível criar novos tipos de extensão derivados de tipos já existentes. É mais fácil herdar dos tipos embutidos, já que uma extensão pode usar diretamente o PyTypeObject de que precisa. Pode ser difícil compartilhar essas estruturas PyTypeObject entre módulos de extensão distintos.
Neste exemplo, criaremos um tipo SubList`que herda do tipo embutido :class:`list. O novo tipo será totalmente compatível com listas comuns, mas terá um método adicional, increment(), que incrementa um contador interno:
>>> import sublist
>>> s = sublist.SubList(range(3))
>>> s.extend(s)
>>> print(len(s))
6
>>> print(s.increment())
1
>>> print(s.increment())
2
#define PY_SSIZE_T_CLEAN
#include <Python.h>
typedef struct {
PyListObject list;
int state;
} SubListObject;
static PyObject *
SubList_increment(PyObject *op, PyObject *Py_UNUSED(dummy))
{
SubListObject *self = (SubListObject *) op;
self->state++;
return PyLong_FromLong(self->state);
}
static PyMethodDef SubList_methods[] = {
{"increment", SubList_increment, METH_NOARGS,
PyDoc_STR("increment state counter")},
{NULL},
};
static int
SubList_init(PyObject *op, PyObject *args, PyObject *kwds)
{
SubListObject *self = (SubListObject *) op;
if (PyList_Type.tp_init(op, args, kwds) < 0)
return -1;
self->state = 0;
return 0;
}
static PyTypeObject SubListType = {
.ob_base = PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "sublist.SubList",
.tp_doc = PyDoc_STR("SubList objects"),
.tp_basicsize = sizeof(SubListObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_init = SubList_init,
.tp_methods = SubList_methods,
};
static int
sublist_module_exec(PyObject *m)
{
SubListType.tp_base = &PyList_Type;
if (PyType_Ready(&SubListType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
return -1;
}
return 0;
}
static PyModuleDef_Slot sublist_module_slots[] = {
{Py_mod_exec, sublist_module_exec},
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL}
};
static PyModuleDef sublist_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "sublist",
.m_doc = "Example module that creates an extension type.",
.m_size = 0,
.m_slots = sublist_module_slots,
};
PyMODINIT_FUNC
PyInit_sublist(void)
{
return PyModuleDef_Init(&sublist_module);
}
Como você pode consultar, o código-fonte se assemelha bastante aos exemplos de Custom das seções anteriores. Vamos detalhar as principais diferenças entre eles.
typedef struct {
PyListObject list;
int state;
} SubListObject;
A diferença principal para objetos de tipos derivados é que a estrutura de objeto do tipo base deve ser o primeiro valor. O tipo base já inclui o PyObject_HEAD() no início de sua própria estrutura.
Quando um objeto Python é uma instância de SubList, seu ponteiro``PyObject *`` pode ser convertido com segurança tanto para PyListObject * quanto para SubListObject *:
static int
SubList_init(PyObject *op, PyObject *args, PyObject *kwds)
{
SubListObject *self = (SubListObject *) op;
if (PyList_Type.tp_init(op, args, kwds) < 0)
return -1;
self->state = 0;
return 0;
}
Vemos acima como chamar o método __init__() do tipo base.
Esse padrão é importante ao escrever um tipo que possui membros personalizados em tp_new e tp_dealloc. O manipulador tp_alloc; em vez disso, deve permitir que a classe base faça isso, chamando o seu próprio tp_new.
A estrutura PyTypeObject provê o campo:c:member:~PyTypeObject.tp_base, que especifica a classe base concreta do tipo. Devido a problemas de compilação multiplataforma, não se deve preencher esse campo diretamente com uma referência para:c:type:PyList_Type; isso deve ser feito dentro da função Py_mod_exec
static int
sublist_module_exec(PyObject *m)
{
SubListType.tp_base = &PyList_Type;
if (PyType_Ready(&SubListType) < 0) {
return -1;
}
if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
return -1;
}
return 0;
}
Before calling PyType_Ready(), the type structure must have the
tp_base slot filled in. When we are deriving an
existing type, it is not necessary to fill out the tp_alloc
slot with PyType_GenericNew() – the allocation function from the base
type will be inherited.
After that, calling PyType_Ready() and adding the type object to the
module is the same as with the basic Custom examples.
Notas de rodapé