¿Cuál es la forma más rápida de verificar si una clase tiene una función definida?

132

Estoy escribiendo un algoritmo de búsqueda de espacio de estado AI, y tengo una clase genérica que se puede usar para implementar rápidamente un algoritmo de búsqueda. Una subclase definiría las operaciones necesarias, y el algoritmo hace el resto.

Aquí es donde me quedo atascado: quiero evitar la regeneración del estado padre una y otra vez, por lo que tengo la siguiente función, que devuelve las operaciones que se pueden aplicar legalmente a cualquier estado:

def get_operations(self, include_parent=True):
    ops = self._get_operations()
    if not include_parent and self.path.parent_op:
        try:
            parent_inverse = self.invert_op(self.path.parent_op)
            ops.remove(parent_inverse)
        except NotImplementedError:
            pass
    return ops

Y la función invert_op arroja por defecto.

¿Hay alguna forma más rápida de verificar si la función no está definida que detectar una excepción?

Estaba pensando en algo en el sentido de verificar el presente en dir, pero eso no parece correcto. hasattr se implementa llamando a getattr y verificando si aumenta, que no es lo que quiero.

Alex
fuente
8
"hasattr se implementa llamando a getattr y verificando si aumenta, que no es lo que quiero". Por qué no? ¿Por qué te importa lo que hace la implementación?
desvío
44
has_op = lambda obj, op: callable(getattr(obj, op, None))
samplebias
1
Proveedores: hasattr(connection, 'invert_opt').
kenorb

Respuestas:

204

Sí, use getattr()para obtener el atributo y callable()para verificarlo es un método:

invert_op = getattr(self, "invert_op", None)
if callable(invert_op):
    invert_op(self.path.parent_op)

Tenga en cuenta que getattr()normalmente arroja una excepción cuando el atributo no existe. Sin embargo, si especifica un valor predeterminado ( Noneen este caso), lo devolverá en su lugar.

Nathan Ostgard
fuente
3
Tenga en cuenta también que la implementación de getattren este caso detecta una excepción silenciosamente y devuelve el valor predeterminado en su lugar, al igual que lo hasattrhace, contra lo que el OP estuvo por alguna razón.
Santa
3
¿Qué pasa si la función no está en esa clase, sino en la clase principal? En este caso obtengo un True, incluso cuando los niños nunca implementan esa función (usando hasattr)
darkgaze
46

Funciona tanto en Python 2 como en Python 3.

hasattr(connection, 'invert_opt')

hasattrdevuelve Truesi el objeto de conexión tiene una función invert_optdefinida. Aquí está la documentación para que pueda pastar

https://docs.python.org/2/library/functions.html#hasattr https://docs.python.org/3/library/functions.html#hasattr

Antonio
fuente
55
Aunque se aprecia el código, siempre debe tener una explicación que lo acompañe. Esto no tiene que ser largo pero se espera.
peterh - Restablece a Mónica el
bueno, puedes apuntar a un artículo, aunque no dolería :)
Vitaliy Terziev
55
Esto también devuelve True si la conexión tiene un atributo connection.invert_opt = 'foo'.
Robert Hönig el
20

¿Hay alguna forma más rápida de verificar si la función no está definida que detectar una excepción?

¿Por qué estás en contra de eso? En la mayoría de los casos de Pythonic, es mejor pedir perdón que permiso. ;-)

hasattr se implementa llamando a getattr y verificando si aumenta, que no es lo que quiero.

De nuevo, ¿por qué es eso? Lo siguiente es bastante pitónico:

    try:
        invert_op = self.invert_op
    except AttributeError:
        pass
    else:
        parent_inverse = invert_op(self.path.parent_op)
        ops.remove(parent_inverse)

O,

    # if you supply the optional `default` parameter, no exception is thrown
    invert_op = getattr(self, 'invert_op', None)  
    if invert_op is not None:
        parent_inverse = invert_op(self.path.parent_op)
        ops.remove(parent_inverse)

Tenga en cuenta, sin embargo, que getattr(obj, attr, default)básicamente se implementa al detectar una excepción también. ¡No hay nada de malo en eso en Python land!

Papa Noel
fuente
4

Las respuestas en este documento verifican si una cadena es el nombre de un atributo del objeto. Se necesita un paso adicional (usando invocable) para verificar si el atributo es un método.

Por lo tanto, se reduce a: ¿cuál es la forma más rápida de verificar si un objeto obj tiene un atributo attrib? La respuesta es

'attrib' in obj.__dict__

Esto es así porque un dict aprieta sus claves, por lo que verificar la existencia de la clave es rápido.

Ver comparaciones de tiempo a continuación.

>>> class SomeClass():
...         pass
...
>>> obj = SomeClass()
>>>
>>> getattr(obj, "invert_op", None)
>>>
>>> %timeit getattr(obj, "invert_op", None)
1000000 loops, best of 3: 723 ns per loop
>>> %timeit hasattr(obj, "invert_op")
The slowest run took 4.60 times longer than the fastest. This could mean that an intermediate result is being cached.
1000000 loops, best of 3: 674 ns per loop
>>> %timeit "invert_op" in obj.__dict__
The slowest run took 12.19 times longer than the fastest. This could mean that an intermediate result is being cached.
10000000 loops, best of 3: 176 ns per loop
Thorwhalen
fuente
Esto falla en las clases que usan __slots__. __slots__ayuda a acelerar el acceso a atributos en ~ 10%. stackoverflow.com/a/14119024/1459669
noɥʇʎԀʎzɐɹƆ
3

Me gusta la respuesta de Nathan Ostgard y la voté. Pero otra forma en que podría resolver su problema sería usar un decorador de memorización, que almacenaría en caché el resultado de la llamada a la función. Entonces puede seguir adelante y tener una función costosa que resuelve algo, pero luego, cuando lo llama una y otra vez, las llamadas posteriores son rápidas; la versión memorizada de la función busca los argumentos en un dict, encuentra el resultado en el dict desde que la función real calculó el resultado y devuelve el resultado de inmediato.

Aquí hay una receta para un decorador memorable llamado "lru_cache" por Raymond Hettinger. Una versión de esto ahora es estándar en el módulo functools en Python 3.2.

http://code.activestate.com/recipes/498245-lru-and-lfu-cache-decorators/

http://docs.python.org/release/3.2/library/functools.html

steveha
fuente
2

Como cualquier cosa en Python, si te esfuerzas lo suficiente, puedes llegar a las entrañas y hacer algo realmente desagradable. Ahora, aquí está la parte desagradable:

def invert_op(self, op):
    raise NotImplementedError

def is_invert_op_implemented(self):
    # Only works in CPython 2.x of course
    return self.invert_op.__code__.co_code == 't\x00\x00\x82\x01\x00d\x00\x00S'

Por favor, háganos un favor, solo siga haciendo lo que tiene en su pregunta y NO lo use nunca a menos que esté en el equipo de PyPy pirateando al intérprete de Python. Lo que tienes allí es Pythonic, lo que tengo aquí es puro MAL .

YH Wong
fuente
Esto será cierto si el método genera alguna excepción. También debe verificar si co_nameses igual a ('NotImplementedError',). Sin embargo, no estoy seguro de si esto lo hace más o menos malvado.
poco
1

También puedes repasar la clase:

import inspect


def get_methods(cls_):
    methods = inspect.getmembers(cls_, inspect.isfunction)
    return dict(methods)

# Example
class A(object):
    pass

class B(object):
    def foo():
        print('B')


# If you only have an object, you can use `cls_ = obj.__class__`
if 'foo' in get_methods(A):
    print('A has foo')

if 'foo' in get_methods(B):
    print('B has foo')
Martin Thoma
fuente
0

Si bien la búsqueda de atributos en la propiedad __dict__ es realmente rápida, no puede usar esto para los métodos, ya que no aparecen en el hash __dict__. Sin embargo, podría recurrir a una solución alternativa en su clase, si el rendimiento es tan crítico:

class Test():
    def __init__():
        # redefine your method as attribute
        self.custom_method = self.custom_method

    def custom_method(self):
        pass

Luego verifique el método como:

t = Test()
'custom_method' in t.__dict__

Comparación de tiempo con getattr:

>>%timeit 'custom_method' in t.__dict__
55.9 ns ± 0.626 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

>>%timeit getattr(t, 'custom_method', None)
116 ns ± 0.765 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

No es que esté alentando este enfoque, pero parece funcionar.

[EDITAR] El aumento del rendimiento es aún mayor cuando el nombre del método no está en una clase dada:

>>%timeit 'rubbish' in t.__dict__
65.5 ns ± 11 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

>>%timeit getattr(t, 'rubbish', None)
385 ns ± 12.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
Jaroslav Loebl
fuente
1
__dict__podría ser anulado No se puede confiar.
Xiao