Jaki jest najszybszy sposób sprawdzenia, czy klasa ma zdefiniowaną funkcję?

140

Piszę algorytm wyszukiwania w przestrzeni stanów AI i mam klasę ogólną, której można użyć do szybkiego wdrożenia algorytmu wyszukiwania. Podklasa definiuje niezbędne operacje, a algorytm zajmie się resztą.

Tutaj utknąłem: chcę uniknąć ciągłego ponownego generowania stanu nadrzędnego, więc mam następującą funkcję, która zwraca operacje, które można legalnie zastosować do dowolnego stanu:

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

Funkcja invert_op generuje domyślnie.

Czy istnieje szybszy sposób sprawdzenia, czy funkcja nie jest zdefiniowana, niż przechwytywanie wyjątku?

Myślałem o czymś w rodzaju sprawdzania obecności w reż, ale to nie wydaje się właściwe. hasattr jest implementowany przez wywołanie getattr i sprawdzenie, czy podnosi, co nie jest tym, czego chcę.

Alex
źródło
8
„Funkcja hasattr jest implementowana przez wywołanie getattr i sprawdzenie, czy podnosi, co nie jest tym, czego chcę”. Dlaczego nie? Dlaczego zależy ci na tym, co robi wdrożenie?
detly
4
has_op = lambda obj, op: callable(getattr(obj, op, None))
samplebias
1
Spróbuj: hasattr(connection, 'invert_opt').
kenorb

Odpowiedzi:

217

Tak, użyj, getattr()aby pobrać atrybut i callable()sprawdzić, czy jest to metoda:

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

Zauważ, że getattr()zwykle zgłasza wyjątek, gdy atrybut nie istnieje. Jeśli jednak określisz wartość domyślną ( Nonew tym przypadku), zostanie ona zwrócona.

Nathan Ostgard
źródło
3
Należy również zauważyć, że implementacja getattrw tym przypadku po cichu przechwytuje wyjątek i zamiast tego zwraca wartość domyślną, tak jak hasattrrobi to, czemu OP z jakiegoś powodu był przeciwny.
Santa
4
A jeśli funkcja nie znajduje się w tej klasie, ale w klasie nadrzędnej? W tym przypadku otrzymuję True, nawet gdy dzieci nigdy nie implementują tej funkcji (używając hasattr)
darkgaze
47

Działa zarówno w Pythonie 2, jak i Pythonie 3

hasattr(connection, 'invert_opt')

hasattrzwraca, Truejeśli obiekt połączenia ma invert_optzdefiniowaną funkcję . Oto dokumentacja, którą możesz wypasać

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

Antony
źródło
5
Chociaż kod jest mile widziany, zawsze powinien mieć dołączone wyjaśnienie. Nie musi to trwać długo, ale można się tego spodziewać.
peterh - Przywróć Monikę
dobry, możesz wskazać na artykuł, choć nie zaszkodzi :)
Vitaliy Terziev
6
To również zwraca True, jeśli połączenie ma atrybut connection.invert_opt = 'foo'.
Robert Hönig
20

Czy istnieje szybszy sposób sprawdzenia, czy funkcja nie jest zdefiniowana, niż przechwytywanie wyjątku?

Dlaczego się temu sprzeciwiasz? W większości przypadków Pythonic lepiej prosić o przebaczenie niż o pozwolenie. ;-)

hasattr jest implementowany przez wywołanie getattr i sprawdzenie, czy podnosi, co nie jest tym, czego chcę.

Dlaczego tak jest? Poniższy tekst jest dość Pythonic:

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

Lub,

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

Należy jednak pamiętać, że getattr(obj, attr, default)jest to zasadniczo realizowane poprzez przechwytywanie wyjątku. Nie ma w tym nic złego w krainie Pythona!

Święty
źródło
4

Odpowiedzi tutaj sprawdzają, czy ciąg jest nazwą atrybutu obiektu. Aby sprawdzić, czy atrybut jest metodą, potrzebny jest dodatkowy krok (z możliwością wywołania).

Sprowadza się więc do: jaki jest najszybszy sposób sprawdzenia, czy obiekt obj ma atrybut attribute. Odpowiedź to

'attrib' in obj.__dict__

Dzieje się tak, ponieważ dyktowanie haszuje swoje klucze, więc sprawdzenie istnienia klucza jest szybkie.

Zobacz porównania czasu poniżej.

>>> 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
źródło
To kończy się niepowodzeniem w klasach, które używają __slots__. __slots__pomaga przyspieszyć dostęp do atrybutów o ~ 10%. stackoverflow.com/a/14119024/1459669
noɥʇʎԀʎzɐɹƆ
3

Jak wszystko w Pythonie, jeśli wystarczająco się postarasz, możesz dostać się do odwagi i zrobić coś naprawdę paskudnego. A teraz paskudna część:

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'

Zrób nam przysługę, po prostu rób to, co masz w swoim pytaniu i NIE używaj tego, chyba że jesteś w zespole PyPy, który włamuje się do interpretera Pythona. To, co tam masz, to Pythonic, a ja mam tutaj czyste ZŁO .

YH Wong
źródło
Będzie to prawdą, jeśli metoda zgłosi wyjątek. Powinieneś również sprawdzić, czy co_namesjest równy ('NotImplementedError',). Nie jestem jednak pewien, czy to czyni to mniej czy bardziej złym.
kindall
3

Podoba mi się odpowiedź Nathana Ostgarda i zagłosowałem za nią. Ale innym sposobem rozwiązania problemu byłoby użycie dekoratora zapamiętującego, który buforowałby wynik wywołania funkcji. Możesz więc iść dalej i mieć kosztowną funkcję, która coś wymyśli, ale potem, gdy ją wywołujesz, kolejne połączenia są szybkie; zapamiętana wersja funkcji wyszukuje argumenty w dyktandzie, znajduje wynik w dyktacie z chwili, gdy rzeczywista funkcja obliczyła wynik, i zwraca wynik od razu.

Oto przepis na dekorator zapamiętujący o nazwie „lru_cache” autorstwa Raymonda Hettingera. Wersja tego jest teraz standardem w module functools w Pythonie 3.2.

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

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

steveha
źródło
1

Możesz również przejść do klasy:

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
źródło
0

Podczas gdy sprawdzanie atrybutów we właściwości __dict__ jest naprawdę szybkie, nie możesz tego użyć dla metod, ponieważ nie pojawiają się one w hashu __dict__. Możesz jednak uciec się do rozwiązania hakerskiego w swojej klasie, jeśli wydajność jest tak krytyczna:

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

    def custom_method(self):
        pass

Następnie sprawdź metodę jako:

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

Porównanie czasu z 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)

Nie dlatego, że zachęcam do takiego podejścia, ale wydaje się, że działa.

[EDYCJA] Zwiększenie wydajności jest jeszcze większe, gdy nazwa metody nie znajduje się w danej klasie:

>>%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
źródło
1
__dict__może zostać nadpisany. Nie można mu ufać.
Xiao,