Si alguna vez ha trabajado con lenguajes de bajo nivel como C o C ++, probablemente haya escuchado sobre punteros. Le permiten aumentar en gran medida la efectividad de diferentes piezas de código. Pero también pueden confundir a los principiantes, e incluso a los desarrolladores experimentados, y generar errores en la administración de la memoria. ¿Hay punteros en Python, puedo emularlos de alguna manera?
Los punteros son ampliamente utilizados en C y C ++. De hecho, estas son variables que contienen las direcciones de memoria en las que se encuentran otras variables. Para
repasar los punteros, lea esta
reseña .
Gracias a este artículo, comprenderá mejor el modelo de objetos en Python y descubrirá por qué los punteros en realidad no existen en este lenguaje. En caso de que necesite simular el comportamiento de los punteros, aprenderá a emularlos sin la pesadilla de la administración de la memoria.
Con este artículo, usted:
- Aprende por qué Python no tiene punteros.
- Aprende la diferencia entre las variables C y los nombres en Python.
- Aprende a emular punteros en Python.
- Usa
ctypes
experimentar con punteros reales.
Nota : Aquí, el término "Python" se aplica a la implementación de Python en C, que se conoce como CPython. Todas las discusiones sobre el dispositivo de lenguaje son válidas para CPython 3.7, pero pueden no corresponder a iteraciones posteriores.
¿Por qué no hay punteros en Python?
No lo se ¿Pueden existir punteros en Python de forma nativa? Probablemente, pero aparentemente, los punteros contradicen el concepto de
Zen de Python , porque provocan cambios implícitos en lugar de explícitos. Los punteros suelen ser bastante complejos, especialmente para principiantes. Además, lo empujan a tomar decisiones fallidas oa hacer algo realmente peligroso, como leer desde un área de memoria, donde no debería haberlo leído.
Python intenta abstraer los detalles de implementación del usuario, como una dirección de memoria. A menudo en este lenguaje, el énfasis está en la usabilidad, no en la velocidad. Por lo tanto, los punteros en Python no tienen mucho sentido. Pero no se preocupe, el idioma predeterminado le brinda algunos de los beneficios de usar punteros.
Para comprender los punteros en Python, repasemos brevemente las características de la implementación del lenguaje. En particular, debe comprender:
- Qué son los objetos mutables e inmutables.
- Cómo se ordenan las variables / nombres en Python.
Aferrarse a sus direcciones de memoria, vamos!
Objetos en Python
Todo en Python es un objeto. Por ejemplo, abra REPL y vea cómo
isinstance()
:
>>> isinstance(1, object) True >>> isinstance(list(), object) True >>> isinstance(True, object) True >>> def foo(): ... pass ... >>> isinstance(foo, object) True
Este código demuestra que todo en Python es en realidad un objeto. Cada objeto contiene al menos tres tipos de datos:
- Contador de referencia.
- Tipo
- Valor.
Se utiliza
un contador de referencia para administrar la memoria. Los detalles sobre esta administración se escriben en
Administración de memoria en Python . El tipo se utiliza en el nivel de CPython para proporcionar seguridad de tipo durante el tiempo de ejecución. Y el valor es el valor real asociado con el objeto.
Pero no todos los objetos son iguales. Hay una diferencia importante: los objetos son mutables e inmutables. Comprender esta distinción entre los tipos de objetos lo ayudará a comprender mejor la primera capa de la cebolla llamada "punteros en Python".
Objetos mutables e inmutables
Hay dos tipos de objetos en Python:
- Objetos inmutables (no se pueden cambiar);
- Objetos modificables (sujetos a cambios).
Reconocer esta diferencia es la primera clave para viajar por el mundo de los punteros en Python. Aquí hay una caracterización de la inmutabilidad de algunos tipos populares:
Como puede ver, muchos de los tipos primitivos comúnmente utilizados son inmutables. Puede verificar esto escribiendo algún código de Python. Necesitará dos herramientas de la biblioteca estándar:
id()
devuelve la dirección de memoria del objeto;
is
devuelve True
si y solo si dos objetos tienen la misma dirección de memoria.
Puede ejecutar este código en un entorno REPL:
>>> x = 5 >>> id(x) 94529957049376
Aquí establecemos la variable
x
en
5
. Si intenta cambiar el valor usando la suma, obtendrá un nuevo objeto:
>>> x += 1 >>> x 6 >>> id(x) 94529957049408
Aunque parezca que este código simplemente cambia el valor de
x
, en realidad está obteniendo un
nuevo objeto como respuesta.
El tipo
str
también es inmutable:
>>> s = "real_python" >>> id(s) 140637819584048 >>> s += "_rocks" >>> s 'real_python_rocks' >>> id(s) 140637819609424
Y en este caso,
s
después de la operación
+=
obtiene una dirección de memoria
diferente .
Bonificación : el operador
+=
traduce en varias llamadas a métodos.
Para algunos objetos, como una lista,
+=
convierte a
__iadd__()
(
__iadd__()
local). Se cambiará a sí mismo y devolverá la misma ID. Sin embargo,
str
e
int
no tienen estos métodos y, como resultado, se
__add__()
lugar de
__iadd__()
.
Consulte la
documentación del modelo de datos de Python
para obtener más detalles .Cuando intentamos cambiar directamente el valor de cadena de
s
obtenemos un error:
>>> s[0] = "R"
Rastreo posterior (las últimas llamadas se muestran en último lugar):
File "<stdin>", line 1, in <mdule> TypeError: 'str' object does not support item assignment
El código anterior se bloquea y Python informa que
str
no admite este cambio, que corresponde a la definición de inmutabilidad de tipo
str
.
Compare con un objeto mutable, por ejemplo, con una lista:
>>> my_list = [1, 2, 3] >>> id(my_list) 140637819575368 >>> my_list.append(4) >>> my_list [1, 2, 3, 4] >>> id(my_list) 140637819575368
Este código demuestra la diferencia principal entre los dos tipos de objetos. Inicialmente,
my_list
tiene una ID. Incluso después de agregar
4
a la lista,
my_list
todavía tiene
la misma ID. La razón es que la
list
tipos es mutable.
Aquí hay otra demostración de la mutabilidad de la lista mediante la asignación:
>>> my_list[0] = 0 >>> my_list [0, 2, 3, 4] >>> id(my_list) 140637819575368
En este código, cambiamos
my_list
y lo configuramos a
0
como primer elemento. Sin embargo, la lista retuvo la misma ID después de esta operación. El siguiente paso en nuestro camino para
aprender Python será explorar su ecosistema.
Nos ocupamos de las variables.
Las variables en Python son fundamentalmente diferentes de las variables en C y C ++. Esencialmente, simplemente no existen en Python.
En lugar de variables, hay nombres .
Puede sonar pedante, y en su mayor parte lo es. Muy a menudo, puede tomar nombres en Python como variables, pero debe comprender la diferencia. Esto es especialmente importante cuando estudias un tema tan difícil como los punteros.
Para que sea más fácil de entender, veamos cómo funcionan las variables en C, qué representan y luego comparemos con el trabajo de los nombres en Python.
Variables en C
Tome el código que define la variable
x
:
int x = 2337;
La ejecución de esta línea corta pasa por varias etapas diferentes:
- Asignación de memoria suficiente para un número.
- Asignación de
2337
a esta ubicación de memoria.
- La asignación que
x
indica este valor.
Una memoria simplificada podría verse así:

Aquí, la variable
x
tiene una dirección falsa de
0x7f1
y un valor de
2337
. Si luego desea cambiar el valor de
x
, puede hacer esto:
x = 2338;
Este código establece la variable
x
nuevo valor de
2338
, sobrescribiendo así el valor
anterior . Esto significa que la variable
x
mutable . Esquema de memoria actualizado para el nuevo valor:

Tenga en cuenta que la ubicación de
x
no
x
cambiado, solo el valor en sí. Esto es importante Esto nos dice que
x
es
un lugar en la memoria , y no solo un nombre.
También puede considerar este problema como parte del concepto de propiedad. Por un lado,
x
posee un lugar en la memoria. Primero,
x
es un cuadro vacío que puede contener solo un número entero, en el que se pueden almacenar valores enteros.
Cuando asigna a
x
algún valor, coloca el valor en un cuadro que pertenece a
x
. Si desea introducir una nueva variable
y
, puede agregar esta línea:
int y = x;
Este código crea un nuevo cuadro llamado
y
y copia el valor de
x
en él. Ahora el circuito de memoria se ve así:

Tenga en cuenta la nueva ubicación
y
-
0x7f5
. Aunque el valor
x
se copió a
x
, la variable
y
posee una nueva dirección en la memoria. Por lo tanto, puede sobrescribir el valor de
y
sin afectar a
x
:
y = 2339;
Ahora el circuito de memoria se ve así:

Repito: cambiaste el valor de
y
, pero no la ubicación. Además, no afectó a la variable original
x
.
Con nombres en Python, la situación es completamente diferente.
Nombres en Python
No hay variables en Python, nombres en su lugar. Puede usar el término "variables" a su discreción, sin embargo, es importante conocer la diferencia entre variables y nombres.
Tomemos el código equivalente del ejemplo C anterior y escribámoslo en Python:
>>> x = 2337
Como en C, el código pasa por varios pasos separados durante la ejecución de esto:
- Se crea PyObject.
- Al número de PyObject se le asigna un código de tipo.
2337
asignado un valor para PyObject.
- Se crea el nombre
x
. x
apunta al nuevo PyObject.- El recuento de referencia de PyObject se incrementa en 1.
Nota :
PyObject no es lo mismo que un objeto en Python, esta entidad es específica de CPython y representa la estructura básica de todos los objetos de Python.
PyObject se define como una estructura C, por lo que si se pregunta por qué no puede llamar directamente al código de tipo o al contador de referencia, entonces la razón es que no tiene acceso directo a las estructuras.
Los métodos de
llamada como
sys.getrefcount () pueden ayudar a obtener algún tipo de material interno.
Si hablamos de memoria, entonces puede verse así:

Aquí, el circuito de memoria es muy diferente del circuito en C que se muestra arriba. En lugar de que
x
posea un bloque de memoria que almacena el valor
2337
, un objeto Python recién creado posee la memoria en la que vive
2337
. El nombre de Python
x
no posee directamente
ninguna dirección en la memoria, al igual que una variable C posee una celda estática.
Si desea asignar
x
nuevo valor, pruebe este código:
>>> x = 2338
El comportamiento del sistema será diferente de lo que sucede en C, pero no diferirá demasiado del enlace original en Python.
En este código:
- Se crea un nuevo PyObject.
- Al número de PyObject se le asigna un código de tipo.
2
asigna un valor para PyObject.
x
apunta al nuevo PyObject.
- El recuento de referencia del nuevo PyObject se incrementa en 1.
- El recuento de referencia del antiguo PyObject se reduce en 1.
Ahora el circuito de memoria se ve así:

Esta ilustración demuestra que
x
apunta a una referencia a un objeto y no posee el área de memoria como antes. También verá que el comando
x = 2338
no es una asignación, sino un enlace del nombre
x
al enlace.
Además, el objeto anterior (que contiene el valor
2337
) ahora está en la memoria con un recuento de referencia de 0 y
el recolector de basura lo eliminará.
Puede ingresar un nuevo nombre
y
, como en el ejemplo C:
>>> y = x
Aparecerá un nuevo nombre en la memoria, pero no necesariamente un nuevo objeto:

Ahora verá que
no se ha creado un nuevo objeto Python, solo se
ha creado un nuevo nombre que apunta al mismo objeto. Además, el contador de referencia de objeto aumentó en 1. Puede verificar la equivalencia de la identidad de los objetos para confirmar su identidad:
>>> y is x True
Este código muestra que
x
e
y
son un objeto. Pero no se equivoque:
y
todavía es inmutable. Por ejemplo, puede realizar una operación de suma con
y
:
>>> y += 1 >>> y is x False
Después de llamar a la adición, devolverá un nuevo objeto Python. Ahora el recuerdo se ve así:

Se ha creado un nuevo objeto
y
ahora lo señala. Es curioso que obtendríamos exactamente el mismo estado final si vinculamos directamente
y
a
2339
:
>>> y = 2339
Después de esta expresión, obtenemos un estado final de memoria, como en la operación de suma. Permítame recordarle que en Python no asigna variables, sino que vincula nombres a enlaces.
Sobre pasantes en Python
Ahora comprende cómo se crean nuevos objetos en Python y cómo se les asignan nombres. Es hora de hablar sobre los objetos internos.
Tenemos este código de Python:
>>> x = 1000 >>> y = 1000 >>> x is y True
Como antes,
x
e
y
son nombres que apuntan al mismo objeto Python. Pero este objeto que contiene el valor
1000
no siempre puede tener la misma dirección de memoria. Por ejemplo, si sumas dos números y obtienes 1000, obtendrás otra dirección:
>>> x = 1000 >>> y = 499 + 501 >>> x is y False
Esta vez, la cadena
x is y
devuelve
False
. Si te da vergüenza, no te preocupes. Esto es lo que sucede cuando se ejecuta este código:
- Se crea un objeto Python (
1000
).
- Se le da el nombre
x
.
- Se crea un objeto Python (
499
).
- Se crea un objeto Python (
501
).
- Estos dos objetos se suman.
- Se crea un nuevo objeto Python (
1000
).
- Se le da el nombre
y
.
Explicaciones técnicas : Los pasos descritos se realizan solo cuando este código se ejecuta dentro de REPL. Si toma el ejemplo anterior, péguelo en el archivo y ejecútelo, entonces la línea
x is y
devolverá
True
.
La razón es el ingenio rápido del compilador CPython, que intenta realizar
optimizaciones de mirilla que ayudan a guardar los pasos de ejecución de código tanto como sea posible. Los detalles se pueden encontrar en el
código fuente del optimizador de agujeros CPython .
¿Pero no es un desperdicio? Bueno, sí, pero paga este precio por todos los grandes beneficios de Python. ¡No necesita pensar en eliminar tales objetos intermedios, y ni siquiera necesita saber acerca de su existencia! La broma es que estas operaciones se realizan relativamente rápido, y no sabrías sobre ellas hasta ese momento.
Los creadores de Python sabiamente notaron esta sobrecarga y decidieron hacer varias optimizaciones. Su resultado es un comportamiento que puede sorprender a los principiantes:
>>> x = 20 >>> y = 19 + 1 >>> x is y True
En este ejemplo, el código es casi el mismo que el anterior, excepto que obtenemos
True
. Se trata de objetos internos. Python crea previamente un subconjunto específico de objetos en la memoria y los almacena en el espacio de nombres global para uso diario.
¿Qué objetos dependen de la implementación de Python? En CPython 3.7, los internos son:
- Enteros que van desde
-5
a 256
.
- Cadenas que contienen solo letras ASCII, números o guiones bajos.
Esto se debe a que estas variables se usan con mucha frecuencia en muchos programas. Al realizar prácticas internas, Python evita la asignación de memoria para objetos persistentes.
Las líneas de menos de 20 caracteres de tamaño y que contengan letras ASCII, números o guiones bajos serán internados porque se supone que deben usarse como identificadores:
>>> s1 = "realpython" >>> id(s1) 140696485006960 >>> s2 = "realpython" >>> id(s2) 140696485006960 >>> s1 is s2 True
Aquí
s1
y
s2
apuntan a la misma dirección en la memoria. Si no insertáramos una letra, número o guión bajo ASCII, obtendríamos un resultado diferente:
>>> s1 = "Real Python!" >>> s2 = "Real Python!" >>> s1 is s2 False
Este ejemplo usa un signo de exclamación, por lo que las cadenas no están internados y son objetos diferentes en la memoria.
Bonificación : si desea que estos objetos se refieran al mismo objeto interno, puede usar
sys.intern()
. Una forma de utilizar esta función se describe en la documentación:
El internamiento de cadenas es útil para un ligero aumento en el rendimiento de búsqueda del diccionario: si las claves en el diccionario y la clave a buscar se internan, entonces las comparaciones de claves (después del hash) se pueden hacer comparando punteros en lugar de cadenas. ( Fuente )
Los internos a menudo confunden a los programadores. Solo recuerde que si comienza a dudar, siempre puede usar
id()
y
is
para determinar la equivalencia de los objetos.
Emulación de puntero de Python
El hecho de que los punteros estén ausentes de forma nativa en Python no significa que no pueda aprovechar los punteros. En realidad, hay varias formas de emular punteros en Python. Aquí nos fijamos en dos de ellos:
- Úselo como punteros a tipos mutables.
- Utilizando objetos Python especialmente preparados.
Usar como punteros de tipo mutable
Ya sabes qué son los tipos mutables. Es gracias a su mutabilidad que podemos emular el comportamiento de los punteros. Digamos que necesita replicar este código:
void add_one(int *x) { *x += 1; }
Este código toma un puntero a un número (
*x
) e incrementa el valor en 1. Aquí está la función principal para ejecutar el código:
En el fragmento anterior, asignamos
y
a
2337
, mostramos el valor actual, lo incrementamos en 1 y luego mostramos un nuevo valor. Lo siguiente aparece en la pantalla:
y = 2337 y = 2338
Una forma de replicar este comportamiento en Python es usar un tipo mutable. Por ejemplo, aplique una lista y cambie el primer elemento:
>>> def add_one(x): ... x[0] += 1 ... >>> y = [2337] >>> add_one(y) >>> y[0] 2338
Aquí
add_one(x)
refiere al primer elemento y aumenta su valor en 1. Usar la lista significa que como resultado obtenemos el valor cambiado. Entonces, ¿hay punteros en Python? No El comportamiento descrito se hizo posible porque la lista es de tipo mutable. Si intentas usar una tupla, obtienes un error:
>>> z = (2337,) >>> add_one(z)
Rastreo posterior (las últimas llamadas son las últimas):
File "<stdin>", line 1, in <module> File "<stdin>", line 2, in add_one TypeError: 'tuple' object does not support item assignment
Este código demuestra la inmutabilidad de la tupla, por lo que no admite la asignación de elementos.
list
no
list
el único tipo mutable; los punteros de parte también se emulan mediante
dict
.
Suponga que tiene una aplicación que debe rastrear la ocurrencia de eventos interesantes. Esto se puede hacer creando un diccionario y usando uno de sus elementos como contador:
>>> counters = {"func_calls": 0} >>> def bar(): ... counters["func_calls"] += 1 ... >>> def foo(): ... counters["func_calls"] += 1 ... bar() ... >>> foo() >>> counters["func_calls"] 2
En este ejemplo, el diccionario usa contadores para rastrear el número de llamadas a funciones. Después de llamar a
foo()
contador aumentó en 2, como se esperaba. Y todo gracias a la
dict
.
No olvide, esto es solo una
emulación del comportamiento del puntero, no tiene nada que ver con punteros reales en C y C ++. Podemos decir que estas operaciones son más caras que si se realizaran en C o C ++.
Usando objetos Python
dict
es una excelente manera de emular punteros en Python, pero a veces es tedioso recordar qué nombre de clave usaste. Especialmente si usa el diccionario en diferentes partes de la aplicación. Una clase personalizada de Python puede ayudar aquí.
Digamos que necesita rastrear métricas en una aplicación. Una excelente manera de ignorar detalles molestos es crear una clase:
class Metrics(object): def __init__(self): self._metrics = { "func_calls": 0, "cat_pictures_served": 0, }
Este código define la clase
Metrics
. Todavía usa el diccionario para almacenar datos actualizados que se encuentran en la
_metrics
miembro
_metrics
. Esto le dará la mutabilidad requerida. Ahora solo necesita acceder a estos valores. Puedes hacer esto usando las propiedades:
class Metrics(object):
Aquí usamos
@property . Si eres nuevo en decoradores, lee el artículo
Primer en decoradores Python . En este caso, el decorador
@property
permite acceder a
func_calls
y
cat_pictures_served
, como si fueran atributos:
>>> metrics = Metrics() >>> metrics.func_calls 0 >>> metrics.cat_pictures_served 0
El hecho de que pueda referirse a estos nombres como atributos significa que está abstraído del hecho de que estos valores están almacenados en el diccionario. Además, hace que los nombres de los atributos sean más explícitos. Por supuesto, debería poder aumentar los valores:
class Metrics(object):
:
inc_func_calls()
inc_cat_pics()
metrics
. , , :
>>> metrics = Metrics() >>> metrics.inc_func_calls() >>> metrics.inc_func_calls() >>> metrics.func_calls 2
func_calls
inc_func_calls()
Python. , -
metrics
, .
: ,
inc_func_calls()
inc_cat_pics()
@property.setter
int
, .
Metrics
:
class Metrics(object): def __init__(self): self._metrics = { "func_calls": 0, "cat_pictures_served": 0, } @property def func_calls(self): return self._metrics["func_calls"] @property def cat_pictures_served(self): return self._metrics["cat_pictures_served"] def inc_func_calls(self): self._metrics["func_calls"] += 1 def inc_cat_pics(self): self._metrics["cat_pictures_served"] += 1
ctypes
, - Python, CPython? ctypes , C. ctypes,
Extending Python With C Libraries and the «ctypes» Module .
, , . -
add_one()
:
void add_one(int *x) { *x += 1; }
,
x
1. , (shared) . ,
add.c
, gcc:
$ gcc -c -Wall -Werror -fpic add.c $ gcc -shared -o libadd1.so add.o
C
add.o
.
libadd1.so
.
libadd1.so
. ctypes Python:
>>> import ctypes >>> add_lib = ctypes.CDLL("./libadd1.so") >>> add_lib.add_one <_FuncPtr object at 0x7f9f3b8852a0>
ctypes.CDLL ,
libadd1
.
add_one()
, , Python-. , . Python , .
, ctypes :
>>> add_one = add_lib.add_one >>> add_one.argtypes = [ctypes.POINTER(ctypes.c_int)]
, C. , , :
>>> add_one(1) Traceback (most recent call last): File "<stdin>", line 1, in <module> ctypes.ArgumentError: argument 1: <class 'TypeError'>: \ expected LP_c_int instance instead of int
Python ,
add_one()
, . , ctypes . :
>>> x = ctypes.c_int() >>> x c_int(0)
x
0
. ctypes
byref()
, .
:
.
, . , .
add_one()
:
>>> add_one(ctypes.byref(x)) 998793640 >>> x c_int(1)
Genial 1. , Python .
Conclusión
Python . , Python.
Python:
Python .