Как стать автором
Обновить

Ultimate benchmark пяти с половиной способов проверить наличие атрибута объекта в Python

Python
Вот тут подымался вопрос о том, как определить, есть ли у объекта атрибут и как это сделать максимально быстро, однако достаточно глубоко тема исследована не была.


Это, собственно, и послужило причиной написания данной коротенькой статьи. Для тестирования я выбрал следующие (известные мне) способы определения наличия атрибута:
  1. Самый, пожалуй, очевидный — использовать встроенную функцию hasattr(obj, name).
  2. Другой распространенный способ — попытаться обратиться к атрибуту и, если не выйдет, принять меры, обработав AttributeError.
  3. Тоже использовать обработку AttributeError, однако обращаться к свойству не напрямую, а через getattr(obj, name). Ситуация выглядит надуманной, но в реальном применении возможны ситуации, когда имя атрибута для проверки формируется динамически и getattr там как нельзя кстати.
  4. Очень быстрый (см. чуть ниже результаты теста) метод — посмотреть в __dict__ объекта (если он у него есть, конечно). Проблема при применении этого метода заключается, пожалуй, лишь в том, что __dict__'ы раздельны для экземляра класса, самого класса и всех его предков. Если не знать точно, где находится нужный нам атрибут, этот метод не имеет для нас практической ценности. Следует заметить и то, что смотреть в __dict__-ы можно тоже двумя путями — используя методы dict.has_key(key_name) и dict.__contains__(key_name), который соответствует ключевому слову in (assert 'My Key Name' in my_dict). С учетом всех преимуществ, недостатков и двух вариантов реализации __dict__ на два отдельных метода не тянет, считаем его «полуторным».
  5. Последний, самый экзотический метод, заключается в просмотре dir(obj) на предмет имени нужного нам атрибута. Кстати, в процессе проверки вложенных __slots__-классов были обнаружены некоторые интересные моменты, связанные с dir(), но об этом в отдельной статье :)
С методами, надеюсь, разобрались. Для того, чтобы получить более реальную ситуацию, я создал 3 класса, которые наследуются «цепочкой» — TestClass от TestClass2, который, в свою очередь, от TestClass3, предком которого является object. У каждого класса есть «instance attribute» с именем вида c3_ia, назначающийся в конструкторе класса, и «class attribute» c2_ca, определяемый на стадии компиляции класса.

В каждом тесте я пытаюсь получить «instance attribute» и «class attribute» класса верхнего уровня TestClass, «instance attribute» и «class attribute», определенные в классе TestClass3 и какой-то несуществующий атрибут fake.

Все тесты прогонялись 10'000'000 раз. Время, которое в сумме было потрачено на выполнение этих 10M одинаковых операций, и считается временем прохождения теста. Чтобы никому не было обидно, суммарное время высчитывается поровну, для существующих и несуществующих атрибутов.

Вроде всё. Теперь результаты.

Групповой зачет:
dict_lookup_contains : 5.800250 [2 subtests failed]
dict_lookup : 7.672500 [2 subtests failed]
hasattr : 12.171750 [0 subtests failed]
exc_direct : 27.785500 [0 subtests failed]
exc_getattr : 32.088875 [0 subtests failed]
dir : 267.500500 [0 subtests failed]

Персональный зачет:
test_dict_lookup_true_this_ca : FAILED [AssertionError()]
test_dict_lookup_true_parent_ca : FAILED [AssertionError()]
test_dict_lookup_contains_true_this_ca : FAILED [AssertionError()]
test_dict_lookup_contains_true_parent_ca : FAILED [AssertionError()]
test_exc_direct_true_this_ca : 5.133000
test_exc_direct_true_parent_ca : 5.710000
test_dict_lookup_contains_true_parent_ia : 5.789000
test_dict_lookup_contains_false : 5.804000
test_dict_lookup_contains_true_this_ia : 5.804000
test_exc_direct_true_this_ia : 6.037000
test_exc_direct_true_parent_ia : 6.412000
test_hasattr_true_this_ca : 6.615000
test_exc_getattr_true_this_ca : 7.144000
test_hasattr_true_this_ia : 7.193000
test_hasattr_true_parent_ca : 7.240000
test_dict_lookup_false : 7.614000
test_dict_lookup_true_this_ia : 7.645000
test_exc_getattr_true_this_ia : 7.769000
test_dict_lookup_true_parent_ia : 7.817000
test_hasattr_true_parent_ia : 7.926000
test_exc_getattr_true_parent_ca : 8.003000
test_exc_getattr_true_parent_ia : 8.691000
test_hasattr_false : 17.100000
test_exc_direct_false : 49.748000
test_exc_getattr_false : 56.276000
test_dir_true_this_ia : 266.847000
test_dir_true_this_ca : 267.053000
test_dir_false : 267.398000
test_dir_true_parent_ca : 267.849000
test_dir_true_parent_ia : 268.663000


В принципе, тут особо комментировать нечего — таблица говорит сама за себя. Краткие итоги:
  • Поиск в __dict__ через in — оптимальное решение, если точно известно, где мы ищем.
  • hasattr показывает стабильно ровную работу при любых запросах, очень хорошо использовать тогда, когда вероятность того, что атрибута не будет, есть.
  • try/except + прямой запрос свойства быстро работает, когда никакого исключения не случается, иначе — сильно чихает (test_exc_direct_false работал аж 49.748 секунд!). Вывод — можно использовать тогда, когда вероятность того, что атрибут будет там, где ему положено быть, очень и очень велика.
  • dir — заслуженный слоупок Python-а. Использовать его для целей проверки наличия атрибута — расстрельная статья.
Вот исходный код тестировщика:

#!/usr/bin/env python
# coding: utf8

import time

__times__ = 10000000

def timeit(func, res):
'''Check if 'func' returns 'res', if true, execute it '__times__' times (__times__ should be defined in parent namespace) measuring elapsed time.'''
assert func() == res

t_start = time.clock()
for i in xrange(__times__):
func()
return time.clock() - t_start

# Define test classes and create instance of top-level class.
class TestClass3(object):
c3_ca = 1

def __init__(self):
self.c3_ia = 1

class TestClass2(TestClass3):
c2_ca = 1

def __init__(self):
TestClass3.__init__(self)
self.c2_ia = 2

class TestClass(TestClass2):
c1_ca = 1

def __init__(self):
TestClass2.__init__(self)
self.c1_ia = 2

obj = TestClass()

# Legend:
#
# hasattr, exc_direct, exc_getattr, dict_lookup, dict_lookup_contains, dir - attribute accessing methods.
# true, false - if 'true' we are checking for really existing attribute.
# this, parent - if 'this' we are looking for attribute in the top-level class, otherwise in the top-level class' parent's parent.
# ca, ia - test class attribute ('ca') or instance attribute ('ia') access.
#
# Note about __dict__ lookups: they are not suitable for generic attribute lookup because instance's __dict__ stores only instance's attributes. To look for class attributes we should query them from class' __dict__.

# Test query through hasattr
def test_hasattr_true_this_ca():
return hasattr(obj, 'c1_ca')

def test_hasattr_true_this_ia():
return hasattr(obj, 'c1_ia')

def test_hasattr_true_parent_ca():
return hasattr(obj, 'c3_ca')

def test_hasattr_true_parent_ia():
return hasattr(obj, 'c3_ia')

def test_hasattr_false():
return hasattr(obj, 'fake')

# Test direct access to attribute inside try/except
def test_exc_direct_true_this_ca():
try:
obj.c1_ca
return True
except AttributeError:
return False

def test_exc_direct_true_this_ia():
try:
obj.c1_ia
return True
except AttributeError:
return False

def test_exc_direct_true_parent_ca():
try:
obj.c3_ca
return True
except AttributeError:
return False

def test_exc_direct_true_parent_ia():
try:
obj.c3_ia
return True
except AttributeError:
return False

def test_exc_direct_false():
try:
obj.fake
return True
except AttributeError:
return False

# Test getattr access to attribute inside try/except
def test_exc_getattr_true_this_ca():
try:
getattr(obj, 'c1_ca')
return True
except AttributeError:
return False

def test_exc_getattr_true_this_ia():
try:
getattr(obj, 'c1_ia')
return True
except AttributeError:
return False

def test_exc_getattr_true_parent_ca():
try:
getattr(obj, 'c3_ca')
return True
except AttributeError:
return False

def test_exc_getattr_true_parent_ia():
try:
getattr(obj, 'c3_ia')
return True
except AttributeError:
return False

def test_exc_getattr_false():
try:
getattr(obj, 'fake')
return True
except AttributeError:
return False

# Test attribute lookup in dir()
def test_dir_true_this_ca():
return 'c1_ca' in dir(obj)

def test_dir_true_this_ia():
return 'c1_ia' in dir(obj)

def test_dir_true_parent_ca():
return 'c3_ca' in dir(obj)

def test_dir_true_parent_ia():
return 'c3_ia' in dir(obj)

def test_dir_false():
return 'fake' in dir(obj)

# Test attribute lookup in __dict__
def test_dict_lookup_true_this_ca():
return obj.__dict__.has_key('c1_ca')

def test_dict_lookup_true_this_ia():
return obj.__dict__.has_key('c1_ia')

def test_dict_lookup_true_parent_ca():
return obj.__dict__.has_key('c3_ca')

def test_dict_lookup_true_parent_ia():
return obj.__dict__.has_key('c3_ia')

def test_dict_lookup_false():
return obj.__dict__.has_key('fake')

# Test attribute lookup in __dict__ through __contains__
def test_dict_lookup_contains_true_this_ca():
return 'c1_ca' in obj.__dict__

def test_dict_lookup_contains_true_this_ia():
return 'c1_ia' in obj.__dict__

def test_dict_lookup_contains_true_parent_ca():
return 'c3_ca' in obj.__dict__

def test_dict_lookup_contains_true_parent_ia():
return 'c3_ia' in obj.__dict__

def test_dict_lookup_contains_false():
return 'fake' in obj.__dict__

# TEST
tests = {
'hasattr': {
'test_hasattr_true_this_ca': True,
'test_hasattr_true_this_ia': True,
'test_hasattr_true_parent_ca': True,
'test_hasattr_true_parent_ia': True,
'test_hasattr_false': False,
},
'exc_direct': {
'test_exc_direct_true_this_ca': True,
'test_exc_direct_true_this_ia': True,
'test_exc_direct_true_parent_ca': True,
'test_exc_direct_true_parent_ia': True,
'test_exc_direct_false': False,
},
'exc_getattr': {
'test_exc_getattr_true_this_ca': True,
'test_exc_getattr_true_this_ia': True,
'test_exc_getattr_true_parent_ca': True,
'test_exc_getattr_true_parent_ia': True,
'test_exc_getattr_false': False,
},
'dict_lookup': {
'test_dict_lookup_true_this_ca': True,
'test_dict_lookup_true_this_ia': True,
'test_dict_lookup_true_parent_ca': True,
'test_dict_lookup_true_parent_ia': True,
'test_dict_lookup_false': False,
},
'dict_lookup_contains': {
'test_dict_lookup_contains_true_this_ca': True,
'test_dict_lookup_contains_true_this_ia': True,
'test_dict_lookup_contains_true_parent_ca': True,
'test_dict_lookup_contains_true_parent_ia': True,
'test_dict_lookup_contains_false': False,
},
'dir': {
'test_dir_true_this_ca': True,
'test_dir_true_this_ia': True,
'test_dir_true_parent_ca': True,
'test_dir_true_parent_ia': True,
'test_dir_false': False,
},
}

# Perform tests
results = {}
results_exc = {}

for (test_group_name, test_group) in tests.iteritems():
results_group = results[test_group_name] = {}
results_exc_group = results_exc[test_group_name] = {}
for (test_name, test_expected_result) in test_group.iteritems():
test_func = locals()[test_name]
print '%s::%s...' % (test_group_name, test_name)
try:
test_time = timeit(test_func, test_expected_result)
results_group[test_name] = test_time
except Exception, exc:
results_group[test_name] = None
results_exc_group[test_name] = exc

# Process results
group_results = []

for (group_name, group_tests) in results.iteritems():
group_true_time = 0.0
group_true_count = 0
group_false_time = 0.0
group_false_count = 0
group_fail_count = 0

for (test_name, test_time) in group_tests.iteritems():
if test_time is not None:
if tests[group_name][test_name]:
group_true_count += 1
group_true_time += test_time
else:
group_false_count += 1
group_false_time += test_time
else:
group_fail_count += 1

group_time = (group_true_time / group_true_count + group_false_time / group_false_count) / 2
group_results.append((group_name, group_time, group_fail_count))

group_results.sort(key = lambda (group_name, group_time, group_fail_count): group_time)

# Output results
print
print 'Групповой зачет:'

for (group_name, group_time, group_fail_count) in group_results:
print '%-25s: %10f [%d subtests failed]' % (group_name, group_time, group_fail_count)

print 'Персональный зачет:'
all_results = []
for (group_name, group_tests) in results.iteritems():
for (test_name, test_time) in group_tests.iteritems():
all_results.append((group_name, test_name, test_time))
all_results.sort(key = lambda (group_name, test_name, test_time): test_time)

for (group_name, test_name, test_time) in all_results:
if test_time is not None:
print '%-50s: %10f' % (test_name, test_time)
else:
print '%-50s: FAILED [%r]' % (test_name, results_exc[group_name][test_name])


Чуть не забыл… Компьютер, на котором выполнялся тест: CPU: Intel Pentium D CPU 3.40GHz (2 ядра (но использовалось, очевидно, только одно)); RAM: 2Gb. Если это кому-то интересно, конечно.

Update #1 (Результаты теста на __getattribute__ и сравнение с предыдущими результатами):
Использовался следующий класс в качестве замены предыдущей цепочке:

__attributes__ = ('c1_ca', 'c3_ca', 'c1_ia', 'c3_ia')
class TestClass(object):
def __getattribute__(self, name):
if name in __attributes__:
return 1
else:
raise AttributeError()


Результаты замера (с учётом getattr(obj, name, None) is not None)

Групповой зачет:
dict_lookup : n/a [5 subtests failed]
dict_lookup_contains : n/a [5 subtests failed]
hasattr : 20.181182 [0 subtests failed]
getattr : 26.283962 [0 subtests failed]
exc_direct : 41.779489 [0 subtests failed]
exc_getattr : 47.757879 [0 subtests failed]
dir : 98.622183 [4 subtests failed]

Персональный зачет:
test_dir_true_parent_ia : FAILED [AssertionError()]
test_dir_true_this_ia : FAILED [AssertionError()]
test_dir_true_this_ca : FAILED [AssertionError()]
test_dir_true_parent_ca : FAILED [AssertionError()]
test_dict_lookup_true_parent_ia : FAILED [AttributeError()]
test_dict_lookup_true_this_ia : FAILED [AttributeError()]
test_dict_lookup_true_this_ca : FAILED [AttributeError()]
test_dict_lookup_true_parent_ca : FAILED [AttributeError()]
test_dict_lookup_false : FAILED [AttributeError()]
test_dict_lookup_contains_true_this_ia : FAILED [AttributeError()]
test_dict_lookup_contains_true_parent_ia : FAILED [AttributeError()]
test_dict_lookup_contains_true_parent_ca : FAILED [AttributeError()]
test_dict_lookup_contains_true_this_ca : FAILED [AttributeError()]
test_dict_lookup_contains_false : FAILED [AttributeError()]
test_exc_direct_true_this_ca : 13.346949
test_exc_direct_true_parent_ca : 13.970407
test_exc_direct_true_this_ia : 14.621696
test_hasattr_true_this_ca : 15.077735
test_exc_direct_true_parent_ia : 15.146182
test_exc_getattr_true_parent_ca : 16.305500
test_getattr_true_this_ia : 16.976973
test_hasattr_true_parent_ia : 17.196719
test_hasattr_true_parent_ca : 17.613231
test_getattr_true_this_ca : 18.331266
test_exc_getattr_true_parent_ia : 18.720518
test_hasattr_false : 21.983571
test_getattr_true_parent_ca : 22.087115
test_exc_getattr_true_this_ca : 23.072045
test_hasattr_true_this_ia : 23.627484
test_getattr_true_parent_ia : 24.474635
test_getattr_false : 32.100426
test_exc_getattr_true_this_ia : 34.555669
test_exc_direct_false : 69.287669
test_exc_getattr_false : 72.352324
test_dir_false : 98.622183


Теперь переходим к сравнению результатов…

Ключ: имя группы время, потраченное на нормальных классах [количество сбоев на них же] | среднее между обоими показателями по времени -- соотношение __getattribute__-показателя к нормальному | время, потраченное на __getattribute__-классах [количество сбоев там же]
Групповой зачет (сортировка по среднему времени):
dict_lookup : 7.672500 [2] | n/a -- n/a | n/a [5]
dict_lookup_contains : 5.800250 [2] | n/a -- n/a | n/a [5]
hasattr : 12.171750 [0] | 16.176466 -- 1.658035 | 20.181182 [0]
getattr : 15.350072 [0] | 20.817017 -- 1.712302 | 26.283962 [0]
exc_direct : 27.785500 [0] | 34.782495 -- 1.503644 | 41.779489 [0]
exc_getattr : 32.088875 [0] | 39.923377 -- 1.488300 | 47.757879 [0]
dir : 267.500500 [0] | 183.061342 -- 0.368680 | 98.622183 [4]

Групповой зачет (сортировка по соотношению):
dict_lookup : 7.672500 [2] | n/a -- n/a | n/a [5]
dict_lookup_contains : 5.800250 [2] | n/a -- n/a | n/a [5]
dir : 267.500500 [0] | 183.061342 -- 0.368680 | 98.622183 [4]
exc_getattr : 32.088875 [0] | 39.923377 -- 1.488300 | 47.757879 [0]
exc_direct : 27.785500 [0] | 34.782495 -- 1.503644 | 41.779489 [0]
hasattr : 12.171750 [0] | 16.176466 -- 1.658035 | 20.181182 [0]
getattr : 15.350072 [0] | 20.817017 -- 1.712302 | 26.283962 [0]


Ключ: имя теста время, потраченное на нормальных классах | среднее между обоими показателями по времени -- соотношение __getattribute__-показателя к нормальному | время, потраченное на __getattribute__-классах
Персональный зачет (сортировка по среднему времени):
test_dict_lookup_true_parent_ia &nbsp
Теги:pythonпрограммированиеобзорbenchmarkоптимизация программоптимизация
Хабы: Python
Всего голосов 20: ↑18 и ↓2 +16
Просмотры4.8K

Похожие публикации

Python для веб-разработки
24 июня 202190 000 ₽SkillFactory
Python для работы с данными
25 июня 202131 500 ₽Нетология
Факультет Python-разработки
15 июля 2021180 000 ₽GeekBrains
Аудит и оптимизация QA-процессов
25 июня 202113 000 ₽Лаборатория Качества
Основы языка Python
29 июня 202120 800 ₽Luxoft Training

Лучшие публикации за сутки