¿Cuál es la relación entre el modelo de datos Python y las funciones integradas?


Mientras leo las respuestas de Python en Stack Overflow, sigo viendo a algunas personas diciéndoles a los usuarios que usen los métodos especiales del modelo de datos o los atributos directamente.

Luego veo un consejo contradictorio (a veces de mí mismo) diciendo que no haga eso, y en su lugar use funciones incorporadas y los operadores directamente.

¿Por qué es eso? ¿Cuál es la relación entre los métodos especiales "dunder" y los atributos del modelo de datos Python y funciones incorporadas ?

¿Cuándo se supone que debo usar los nombres especiales?

Author: Community, 2016-10-27

2 answers

¿Cuál es la relación entre el datamodel de Python y las funciones incorporadas?

  • Los builtins y los operadores usan los métodos o atributos datamodel subyacentes.
  • Los builtins y los operadores tienen un comportamiento más elegante y en general son más compatibles hacia adelante.
  • Los métodos especiales del datamodel son interfaces semánticamente no públicas.
  • Los builtins y los operadores de lenguaje están diseñados específicamente para ser la interfaz de usuario para el comportamiento implementado por métodos especiales.

Por lo tanto, debería preferir usar las funciones y operadores incorporados cuando sea posible sobre los métodos y atributos especiales del datamodel.

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 conlleva los siguientes riesgos:

  • Usted puede encontrar tiene más cambios 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.)
  • Sus colegas probablemente pensarán mal de sus habilidades lingüísticas y conciencia, y lo considerarán un olor a código, llevándolo a usted y al resto de su código a un mayor escrutinio.
  • Las funciones incorporadas son un comportamiento fácil de interceptar. El uso de métodos especiales limita directamente la potencia de su Python para la introspección y depuración.

En profundidad

Las funciones y operadores incorporados invocan los métodos especiales y usan los atributos especiales en el modelo de datos de Python. Son la chapa legible y mantenible que oculta los elementos internos de los objetos. En general, los usuarios deben usar los builtins y operadores dados en el lenguaje en lugar de llamar a los métodos especiales o usar los atributos especiales directamente.

Las funciones incorporadas y los operadores también pueden tener un comportamiento alternativo o más elegante que los métodos especiales datamodel más primitivos. Por ejemplo:

  • next(obj, default) le permite proporcionar un valor predeterminado en lugar de elevar StopIteration cuando se agota un iterador, mientras que obj.__next__() no lo hace.
  • str(obj) volver a obj.__repr__() cuando obj.__str__() no está disponible, mientras que llamar a obj.__str__() directamente generaría un error de atributo.
  • obj != other volver a not obj == other en Python 3 cuando no __ne__ - calling obj.__ne__(other) no tomaría ventaja de esto.

(Las funciones incorporadas también pueden ser fácilmente eclipsadas, si es necesario o deseable, en el ámbito global de un módulo o en el módulo builtins, para personalizar aún más el comportamiento.)

Mapeando los builtins y operadores al datamodel

Aquí hay una asignación, con notas, de las funciones y operadores incorporados 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 dar 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 operator tiene length_hint que tiene una alternativa implementada por un método especial respectivo si __len__ no se implementa:

length_hint(obj)  obj.__length_hint__() 

Búsquedas punteadas

Las búsquedas punteadas son contextuales. Sin implementación de métodos especiales, primero busque en la jerarquía de clases descriptores de datos (como propiedades y ranuras), luego en la instancia __dict__ (por ejemplo variables), luego en la jerarquía de clases para descriptores que no son datos (como los métodos). Los 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 - siéntase libre de omitir estas entradas y volver más tarde - recuerde que la instancia del descriptor está 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 es instanciada (definida) el siguiente método descriptor __set_name__ se llama si cualquier descriptor lo tiene para informar al descriptor de su nombre de atributo. (Esto es nuevo en Python 3.6.) cls es lo mismo que type(obj) anterior, y 'attr' representa el nombre del atributo:

class cls:
    @descriptor_type
    def attr(self): pass # -> descriptor.__set_name__(cls, 'attr') 

Elementos (notación de subíndice)

La notación de subíndice también es contextual:

obj[name]         -> obj.__getitem__(name)
obj[name] = item  -> obj.__setitem__(name, item)
del obj[name]     -> obj.__delitem__(name)

Un caso especial para las subclases de dict, __missing__ se llama si __getitem__ no encuentra la clave:

obj[name]         -> obj.__missing__(name)  

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 gestor de contexto define __enter__, que se llama al entrar en el bloque de código (su valor de retorno, normalmente self, está 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, lo volverá a subir al salir del método.

Si no hay excepción, __exit__ obtiene None para esos tres argumentos en su lugar, y el valor devuelto 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 (a partir de sus metaclases) que soportan clases base abstractas:

isinstance(obj, cls) -> cls.__instancecheck__(obj)
issubclass(sub, cls) -> cls.__subclasscheck__(sub)

Una conclusión importante es que mientras que los builtins como next y bool no cambian entre Python 2 y 3, los nombres de implementación subyacentes están cambiando.

Por lo tanto, el uso de builtins también ofrece más compatibilidad hacia adelante.

¿Cuándo se supone que debo usar los nombres especiales?

En Python, los nombres que comienzan con guiones bajos son semánticamente nombres no públicos para los usuarios. El subrayado es la manera del creador de decir, " manos fuera, no toques."

Esto no es solo cultural, sino que también lo es en el tratamiento de Python de las API. Cuando un paquete __init__.py usa import * para proporcionar una API desde un subpaquete, si el subpaquete no proporciona un __all__, excluye los nombres que comienzan con guiones bajos. El subpaquete __name__ también sería excluido.

Las herramientas de autocompletado IDE se mezclan en su consideración de nombres que comienzan con guiones bajos para que no sean públicos. Sin embargo, aprecio mucho el no ver __init__, __new__, __repr__, __str__, __eq__, etc. (ni ninguna de las interfaces no públicas creadas por el usuario) cuando escribo el nombre de un objeto y un periodo.

Así afirmo: {[80]]}

Los métodos especiales "dunder" no son parte de la interfaz pública. Evite usarlos directamente.

Entonces, ¿cuándo usarlos?

El caso de uso principal es cuando implementa su propio objeto personalizado o subclase de un objeto incorporado.

Trate de usarlos solo cuando sea absolutamente necesario. He aquí algunos ejemplos:

Utilice el atributo especial __name__ en funciones o clases

Cuando decoramos un función, normalmente obtenemos una función de envoltura a cambio que oculta información útil sobre la función. Usaríamos el decorador @wraps(fn) para asegurarnos de no perder esa información, pero si necesitamos el nombre de la función, necesitamos 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

Del mismo modo, hago lo siguiente cuando necesito el nombre de la clase del objeto en un método (utilizado, 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__

Usando atributos especiales para escribir clases o subclases personalizadas builtins

Cuando queremos definir el 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) (ver mi respuesta aquí para probar que esto último puede conducir a un comportamiento inesperado.) En su lugar, deberíamos usar el nivel más alto de abstracción.

Otro punto en el que necesitaríamos usar el especial method names es cuando estamos en la implementación de un hijo 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 el funcionamiento interno de los objetos.

Utilice las funciones y los operadores integrados siempre que pueda. Solo use los métodos especiales cuando no haya una API pública documentada.

 34
Author: Aaron Hall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-26 19:50:54

Voy a mostrar algunos usos que aparentemente no pensó, comentar los ejemplos que mostró, y argumentar en contra de la reclamación de privacidad de su 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 su uso por funciones y operadores incorporados, ocasionalmente también los uso de la siguiente manera:

Digamos que necesito dar una función de filtro a alguna función y quiero usar un conjunto s como filtro. No voy a crear una función extra 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 simple 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 demo de sincronización):

>>> 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

Así que mientras no llamo directamente a métodos mágicos como s.__contains__(x), ocasionalmente los paso a en algún lugar como some_function_needing_a_filter(s.__contains__). Y creo que está perfectamente bien, y mejor que la alternativa lambda / def.


Mis pensamientos sobre los ejemplos que mostraste: {[37]]}

  • Ejemplo 1: Cuando se le preguntó cómo obtener el tamaño de una lista, respondió items.__len__(). Incluso sin ningún razonamiento. Mi veredicto: Eso está mal. Debe ser len(items).
  • Ejemplo 2 : ¡Menciona primero d[key] = value! Y luego agrega d.__setitem__(key, value) con el razonamiento "si a tu teclado le faltan las teclas entre corchetes", que rara vez se aplica y que dudo que fuera serio. Creo que fue solo el pie en la puerta para el último punto, mencionando que así es como podemos soportar la sintaxis de corchetes cuadrados en nuestras propias clases. Lo que lo convierte en una sugerencia para usar square corchete.
  • Ejemplo 3: Sugiere obj.__dict__. Malo, como el ejemplo __len__. Pero sospecho que simplemente no lo sabía vars(obj), y puedo entenderlo, ya que vars es menos común/conocido y el nombre difiere del "dict" en __dict__.
  • Ejemplo 4: Sugiere __class__. Debe ser type(obj). Sospecho que es similar a la historia __dict__, aunque creo que type es más conocida.

Acerca de la privacidad: En tu propia respuesta dices que estos métodos son "semantically private". Estoy totalmente en desacuerdo. Los guiones bajos simples y dobles iniciales son para eso, pero no los métodos especiales "dunder/magic" del modelo de datos con guiones bajos dobles iniciales+finales.

  • Las dos cosas que se usan como argumentos son el comportamiento de importación y el autocompletado del 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. He creado una clase / objeto con los métodos _foo y __bar__ y luego autocompletado no ofreció _foo pero hizo oferta __bar__. Y cuando usé ambos métodos de todos modos, PyCharm solo me advirtió sobre _foo (llamándolo un "miembro protegido"), no sobre __bar__.
  • PEP 8 dice: " débil "uso interno" indicador " explícitamente para un subrayado, y explícitamente por el doble líder subraya menciona el nombre de destrozarlo y más adelante explica que para "los atributos que no quiero que las subclases usen " . Pero el comentario sobre los guiones bajos doble inicial+final no dice nada de eso.
  • La página del modelo de datos a la que usted mismo enlaza dice que estos nombres de métodos especiales son"El enfoque de Python para la sobrecarga de operadores". Nada sobre 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 dunder es un espacio de nombres reservado para el equipo core Python" y "Nunca, nunca, inventen sus propios dunders" porque "El equipo core Python reservó un espacio de nombres algo feo para ellos mismos". Que coincide con la instrucción del PEP 8 "Nunca inventes nombres[dunder/magic]; solo úsalos como está documentado". Creo que Andrew está en el clavo, es solo un feo espacio de nombres del equipo central. Y es para el propósito de la sobrecarga del operador, no sobre la privacidad(no el punto de Andrew, sino el mío y la página del modelo de datos).

Además del artículo de Andrew, también revisé varios más sobre estos métodos "mágicos"/"dunder", y no encontré ninguno de ellos hablando de privacidad en absoluto. No se trata de eso.

Nuevamente, debemos usar len(a), no a.__len__(). Pero no por la privacidad.

 10
Author: Stefan Pochmann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:03:02