getitem - ¿Cuál es la relación entre el modelo de datos de Python y las funciones integradas?
models python (2)
¿Cuál es la relación entre el datamodel de Python y las funciones incorporadas?
- Los incorporados y los operadores utilizan los métodos o atributos de los modelos de datos subyacentes.
- Las funciones incorporadas y los operadores tienen un comportamiento más elegante y, en general, son más compatibles hacia adelante.
- Los métodos especiales del modelo de datos son interfaces semánticamente no públicas.
- Los operadores incorporados y de lenguaje están específicamente diseñados para ser la interfaz de usuario para el comportamiento implementado por métodos especiales.
Por lo tanto, debería preferir utilizar las funciones integradas y los operadores cuando sea posible sobre los métodos y atributos especiales del modelo de datos.
Las API semánticamente internas tienen más probabilidades de cambiar que las interfaces públicas. Aunque Python en realidad no considera nada "privado" y expone los aspectos internos, eso no significa que sea una buena idea abusar de ese acceso. Hacerlo tiene los siguientes riesgos:
- Es posible que tenga más cambios importantes al actualizar su ejecutable de Python o al cambiar a otras implementaciones de Python (como PyPy, IronPython o Jython, o alguna otra implementación imprevista).
- Es probable que sus colegas piensen mal de sus habilidades lingüísticas y su conciencia, y lo consideren un olor a código, lo que lo llevará a usted y al resto de su código a un mayor escrutinio.
- Las funciones incorporadas son fáciles de interceptar comportamiento para. El uso de métodos especiales limita directamente el poder de su Python para la introspección y depuración.
A fondo
Las funciones integradas y los operadores invocan los métodos especiales y utilizan los atributos especiales en el modelo de datos de Python. Son la chapa legible y mantenible que oculta las partes internas de los objetos. En general, los usuarios deben usar los elementos incorporados y los operadores dados en el lenguaje en lugar de llamar a los métodos especiales o usar los atributos especiales directamente.
Las funciones integradas y los operadores también pueden tener un comportamiento alternativo o más elegante que los métodos especiales de modelos de datos más primitivos. Por ejemplo:
-
next(obj, default)
permite proporcionar un valor predeterminado en lugar de elevarStopIteration
cuando seStopIteration
un iterador, mientras queobj.__next__()
no lo hace. -
str(obj)
retrocede aobj.__repr__()
cuandoobj.__str__()
no está disponible, mientras que llamar aobj.__str__()
directamente generaría un error de atributo. -
obj != other
retroceso paranot obj == other
en Python 3 cuando no__ne__
- llamando aobj.__ne__(other)
no aprovecharía esto.
(Las funciones incorporadas también pueden ser fácilmente eclipsadas, si es necesario o deseable, en el alcance global de un módulo o en el módulo builtins
, para personalizar aún más el comportamiento).
Mapeo de los builtins y operadores al datamodel
Aquí hay una asignación, con notas, de las funciones integradas y los operadores a los respectivos métodos y atributos especiales que usan o devuelven. Tenga en cuenta que la regla habitual es que la función incorporada generalmente se asigna a un método especial del mismo nombre, pero esto no es lo suficientemente consistente como para justificar la entrega de este mapa a continuación:
builtins/ special methods/
operators -> datamodel NOTES (fb == fallback)
repr(obj) obj.__repr__() provides fb behavior for str
str(obj) obj.__str__() fb to __repr__ if no __str__
bytes(obj) obj.__bytes__() Python 3 only
unicode(obj) obj.__unicode__() Python 2 only
format(obj) obj.__format__() format spec optional.
hash(obj) obj.__hash__()
bool(obj) obj.__bool__() Python 3, fb to __len__
bool(obj) obj.__nonzero__() Python 2, fb to __len__
dir(obj) obj.__dir__()
vars(obj) obj.__dict__ does not include __slots__
type(obj) obj.__class__ type actually bypasses __class__ -
overriding __class__ will not affect type
help(obj) obj.__doc__ help uses more than just __doc__
len(obj) obj.__len__() provides fb behavior for bool
iter(obj) obj.__iter__() fb to __getitem__ w/ indexes from 0 on
next(obj) obj.__next__() Python 3
next(obj) obj.next() Python 2
reversed(obj) obj.__reversed__() fb to __len__ and __getitem__
other in obj obj.__contains__(other) fb to __iter__ then __getitem__
obj == other obj.__eq__(other)
obj != other obj.__ne__(other) fb to not obj.__eq__(other) in Python 3
obj < other obj.__lt__(other) get >, >=, <= with @functools.total_ordering
complex(obj) obj.__complex__()
int(obj) obj.__int__()
float(obj) obj.__float__()
round(obj) obj.__round__()
abs(obj) obj.__abs__()
El módulo del operator
tiene length_hint
que tiene un respaldo implementado por un método especial respectivo si no se implementa __len__
:
length_hint(obj) obj.__length_hint__()
Búsquedas de puntos
Las búsquedas punteadas son contextuales. Sin la implementación de métodos especiales, primero busque en la jerarquía de clases los descriptores de datos (como propiedades y ranuras), luego en la instancia __dict__
(por ejemplo, las variables), luego en la jerarquía de clases para los descriptores que no son de datos (métodos similares). Métodos especiales implementan los siguientes comportamientos:
obj.attr obj.__getattr__(''attr'') provides fb if dotted lookup fails
obj.attr obj.__getattribute__(''attr'') preempts dotted lookup
obj.attr = _ obj.__setattr__(''attr'', _) preempts dotted lookup
del obj.attr obj.__delattr__(''attr'') preempts dotted lookup
Descriptores
Los descriptores son un poco avanzados. No dude en omitir estas entradas y volver más tarde. Recuerde que la instancia del descriptor se encuentra en la jerarquía de clases (como métodos, ranuras y propiedades). Un descriptor de datos implementa __set__
o __delete__
:
obj.attr descriptor.__get__(obj, type(obj))
obj.attr = val descriptor.__set__(obj, val)
del obj.attr descriptor.__delete__(obj)
Cuando la clase se crea una instancia (definida), se llama al siguiente método descriptor __set_name__
si algún descriptor lo tiene para informar al descriptor de su nombre de atributo. (Esto es nuevo en Python 3.6.) cls
es igual que el type(obj)
anterior, y ''attr''
representa el nombre del atributo:
class cls:
@descriptor_type
def attr(self): pass # -> descriptor.__set_name__(cls, ''attr'')
Artículos (notación de subíndice)
La notación del subíndice también es contextual:
obj[name] -> obj.__getitem__(name)
obj[name] = item -> obj.__setitem__(name, item)
del obj[name] -> obj.__delitem__(name)
Se llama un caso especial para las subclases de dict
, __missing__
si __getitem__
no encuentra la clave:
obj[name] -> obj.__missing__(name)
Los operadores
También hay métodos especiales para +, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |
Operadores, por ejemplo:
obj + other -> obj.__add__(other), fallback to other.__radd__(obj)
obj | other -> obj.__or__(other), fallback to other.__ror__(obj)
y operadores in situ para asignación aumentada, +=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=
, por ejemplo:
obj += other -> obj.__iadd__(other)
obj |= other -> obj.__ior__(other)
y operaciones unarias:
+obj -> obj.__pos__()
-obj -> obj.__neg__()
~obj -> obj.__invert__()
Gestores de contexto
Un administrador de contexto define __enter__
, que se llama al ingresar al bloque de código (su valor de retorno, usualmente self, tiene un alias con as
), y __exit__
, que se garantiza que se llamará al salir del bloque de código, con información de excepción.
with obj as cm: -> cm = obj.__enter__()
raise Exception(''message'')
-> obj.__exit__(Exception, Exception(''message''), traceback_object)
Si __exit__
obtiene una excepción y luego devuelve un valor falso, la volverá a subir al dejar el método.
Si no hay excepción, __exit__
obtiene None
para esos tres argumentos en su lugar, y el valor de retorno no tiene sentido:
with obj: -> obj.__enter__()
pass
-> obj.__exit__(None, None, None)
Algunos métodos especiales de metaclase
De manera similar, las clases pueden tener métodos especiales (desde sus metaclases) que admiten clases base abstractas:
isinstance(obj, cls) -> cls.__instancecheck__(obj)
issubclass(sub, cls) -> cls.__subclasscheck__(sub)
Una conclusión importante es que, si bien las incorporaciones como next
y bool
no cambian entre Python 2 y 3, los nombres de implementación subyacentes están cambiando.
Así, el uso de los builtin también ofrece una mayor compatibilidad hacia adelante.
¿Cuándo se supone que debo usar los nombres especiales?
En Python, los nombres que comienzan con guiones bajos son nombres semánticamente no públicos para los usuarios. El subrayado es la forma en que el creador dice "no toques, no toques".
Esto no es solo cultural, sino que también se encuentra en el tratamiento de API de Python. Cuando __init__.py
un paquete usa import *
para proporcionar una API de un subpaquete, si el subpaquete no proporciona un __all__
, excluye los nombres que comienzan con guiones bajos. El __name__
del subpaquete también se excluiría.
Las herramientas de autocompletado de IDE se mezclan en su consideración de nombres que comienzan con guiones bajos para que no sean públicos. Sin embargo, aprecio mucho no ver __init__
, __new__
, __repr__
, __str__
, __eq__
, etc. (ni ninguna de las interfaces no públicas creadas por el usuario) cuando __eq__
el nombre de un objeto y un punto.
Por eso afirmo:
Los métodos especiales "dunder" no son parte de la interfaz pública. Evite usarlos directamente.
Entonces, ¿cuándo usarlos?
El principal caso de uso es cuando implementa su propio objeto personalizado o subclase de un objeto incorporado.
Trate de usarlos solo cuando sea absolutamente necesario. Aquí hay unos ejemplos:
Use el atributo especial __name__
en funciones o clases
Cuando decoramos una función, normalmente obtenemos una función de envoltura a cambio que oculta información útil sobre la función. @wraps(fn)
el @wraps(fn)
para asegurarnos de no perder esa información, pero si necesitamos el nombre de la función, debemos usar el atributo __name__
directamente:
from functools import wraps
def decorate(fn):
@wraps(fn)
def decorated(*args, **kwargs):
print(''calling fn,'', fn.__name__) # exception to the rule
return fn(*args, **kwargs)
return decorated
De manera similar, hago lo siguiente cuando necesito el nombre de la clase del objeto en un método (usado, por ejemplo, en __repr__
):
def get_class_name(self):
return type(self).__name__
# ^ # ^- must use __name__, no builtin e.g. name()
# use type, not .__class__
Uso de atributos especiales para escribir clases personalizadas o incorporaciones subclasificadas
Cuando queremos definir un comportamiento personalizado, debemos usar los nombres de los modelos de datos.
Esto tiene sentido, ya que somos los implementadores, estos atributos no son privados para nosotros.
class Foo(object):
# required to here to implement == for instances:
def __eq__(self, other):
# but we still use == for the values:
return self.value == other.value
# required to here to implement != for instances:
def __ne__(self, other): # docs recommend for Python 2.
# use the higher level of abstraction here:
return not self == other
Sin embargo, incluso en este caso, no usamos self.value.__eq__(other.value)
o not self.__eq__(other)
(vea mi respuesta aquí para ver una prueba de que este último puede conducir a un comportamiento inesperado). Debe utilizar el nivel más alto de abstracción.
Otro punto en el que deberíamos usar los nombres de métodos especiales es cuando estamos en la implementación de un niño y queremos delegar en el padre. Por ejemplo:
class NoisyFoo(Foo):
def __eq__(self, other):
print(''checking for equality'')
# required here to call the parent''s method
return super(NoisyFoo, self).__eq__(other)
Conclusión
Los métodos especiales permiten a los usuarios implementar la interfaz para objetos internos.
Utilice las funciones y operadores incorporados siempre que pueda. Solo use los métodos especiales donde no haya una API pública documentada.
Mientras leo las respuestas de Python en Stack Overflow, sigo viendo a algunas personas que les dicen a los usuarios que usen los methods o attributes especiales del modelo de datos directamente.
Entonces veo consejos contradictorios (a veces de mí mismo) que dicen que no se debe hacer eso, y en lugar de usar las funciones integradas y los operadores directamente.
¿Porqué es eso? ¿Cuál es la relación entre los métodos especiales "dunder" y los atributos del modelo de datos de Python y las funciones incorporadas ?
¿Cuándo se supone que debo usar los nombres especiales?
Mostraré un uso que aparentemente no pensaste, comentaré los ejemplos que mostraste y argumentaré contra la afirmación de privacidad a partir de tu propia respuesta.
Estoy de acuerdo con su propia respuesta de que, por ejemplo, se debe usar len(a)
, no a.__len__()
. Lo pondría así: len
existe para que podamos usarlo, y __len__
existe para que len
pueda usarlo . O, sin embargo, eso realmente funciona internamente, ya que len(a)
puede ser mucho más rápido , al menos por ejemplo para listas y cadenas:
>>> timeit(''len(a)'', ''a = [1,2,3]'', number=10**8)
4.22549770486512
>>> timeit(''a.__len__()'', ''a = [1,2,3]'', number=10**8)
7.957335462257106
>>> timeit(''len(s)'', ''s = "abc"'', number=10**8)
4.1480574509332655
>>> timeit(''s.__len__()'', ''s = "abc"'', number=10**8)
8.01780160432645
Pero además de definir estos métodos en mis propias clases para uso por funciones y operadores integrados, ocasionalmente también los uso de la siguiente manera:
Digamos que necesito asignar una función de filtro a alguna función y quiero usar un conjunto s
como filtro. No voy a crear una función adicional lambda x: x in s
o def f(x): return x in s
. No. Ya tengo una función perfectamente fina que puedo usar: el método __contains__
del conjunto. Es más sencillo y más directo. Y aún más rápido, como se muestra aquí (ignore que lo guardo como f
aquí, eso es solo para esta demostración de tiempo):
>>> timeit(''f(2); f(4)'', ''s = {1, 2, 3}; f = s.__contains__'', number=10**8)
6.473739433621368
>>> timeit(''f(2); f(4)'', ''s = {1, 2, 3}; f = lambda x: x in s'', number=10**8)
19.940786514456924
>>> timeit(''f(2); f(4)'', ''s = {1, 2, 3}/ndef f(x): return x in s'', number=10**8)
20.445680107760325
Entonces, si bien no llamo directamente a los métodos mágicos como s.__contains__(x)
, ocasionalmente los paso a algún lugar como some_function_needing_a_filter(s.__contains__)
. Y creo que eso está perfectamente bien, y mejor que la alternativa lambda / def.
Mis pensamientos sobre los ejemplos que mostró:
- Ejemplo 1 : Cuando se le preguntó cómo obtener el tamaño de una lista, respondió los
items.__len__()
. Incluso sin ningún razonamiento. Mi veredicto: Eso está mal. Debe serlen(items)
. - Ejemplo 2 : ¡Menciona
d[key] = value
primero! Y luego agregad.__setitem__(key, value)
con el razonamiento "si a su teclado le faltan las teclas del corchete" , que rara vez se aplica y que dudo fue grave. Creo que fue solo el pie en la puerta para el último punto, mencionando que así es como podemos apoyar la sintaxis de corchete en nuestras propias clases. Lo que lo convierte de nuevo en una sugerencia de utilizar corchetes. - methods : Sugiere
obj.__dict__
. Malo, como el ejemplo de__len__
. Pero sospecho que simplemente no sabíavars(obj)
, y puedo entenderlo, ya quevars
es menos común / conocido y el nombre difiere del "dict" en__dict__
. - attributes : Sugiere
__class__
. Debe ser detype(obj)
. Sospecho que es similar a la historia de__dict__
, aunque creo que eltype
es más conocido.
Acerca de la privacidad: en su propia respuesta usted dice que estos métodos son "semánticamente privados". Estoy totalmente en desacuerdo. Los guiones bajos iniciales y dobles son para eso, pero no los métodos especiales "dunder / magic" del modelo de datos con guiones bajos dobles + iniciales.
- Las dos cosas que utiliza como argumentos son el comportamiento de importación y el autocompletado de IDE. Pero la importación y estos métodos especiales son áreas diferentes, y el IDE que probé (el popular PyCharm) no está de acuerdo con usted.
_foo
una clase / objeto con los métodos_foo
y__bar__
y luego el autocompletado no ofrecía_foo
pero sí ofrecía__bar__
. Y cuando utilicé ambos métodos, PyCharm solo me advirtió sobre_foo
(llamándolo "miembro protegido"), no sobre__bar__
. - PEP 8 dice "indicador de" uso interno "débil explícitamente para el subrayado inicial único , y explícitamente para los subrayados iniciales dobles menciona el nombre del gestor de nombres y luego explica que es para " atributos que no desea que usen las subclases " . Pero el comentario sobre el doble guiado + guiones bajos no dice nada de eso.
- La página del modelo de datos con la que usted se vincula dice que estos nombres de métodos especiales son "Enfoque de Python para la sobrecarga de operadores" . No hay nada de privacidad allí. Las palabras privado / privacidad / protegido ni siquiera aparecen en ninguna parte de esa página.
También recomiendo leer este artículo de Andrew Montalenti sobre estos métodos, enfatizando que "la convención de dunder es un espacio de nombres reservado para el equipo central de Python" y "Nunca, nunca, invente sus propios dunders" porque "el equipo central de Python reservó un poco feo espacio de nombres para ellos mismos " . Lo que coincide con la instrucción de PEP 8 "Nunca inventes los nombres [dunder / magic]; solo úsalos como está documentado" . Creo que Andrew es acertado, es solo un espacio de nombres feos del equipo central. Y es con el propósito de sobrecargar al operador, no sobre la privacidad (no es el punto de Andrew sino el mío y el de la página del modelo de datos).
Además del artículo de Andrew, también revisé varios más acerca de estos métodos "mágicos" / "dunder", y no encontré ninguno de ellos hablando de privacidad en absoluto. Eso no es de lo que se trata.
Nuevamente, debemos usar len(a)
, no a.__len__()
. Pero no por la privacidad.