Вот тут подымался вопрос о том, как определить, есть ли у объекта атрибут и как это сделать максимально быстро, однако достаточно глубоко тема исследована не была.
Это, собственно, и послужило причиной написания данной коротенькой статьи. Для тестирования я выбрал следующие (известные мне) способы определения наличия атрибута:
В каждом тесте я пытаюсь получить «instance attribute» и «class attribute» класса верхнего уровня
Все тесты прогонялись 10'000'000 раз. Время, которое в сумме было потрачено на выполнение этих 10M одинаковых операций, и считается временем прохождения теста. Чтобы никому не было обидно, суммарное время высчитывается поровну, для существующих и несуществующих атрибутов.
Вроде всё. Теперь результаты.
В принципе, тут особо комментировать нечего — таблица говорит сама за себя. Краткие итоги:
Чуть не забыл… Компьютер, на котором выполнялся тест: CPU: Intel Pentium D CPU 3.40GHz (2 ядра (но использовалось, очевидно, только одно)); RAM: 2Gb. Если это кому-то интересно, конечно.
Update #1 (Результаты теста на
Использовался следующий класс в качестве замены предыдущей цепочке:
Результаты замера (с учётом
Теперь переходим к сравнению результатов…
Ключ:
Ключ:
Это, собственно, и послужило причиной написания данной коротенькой статьи. Для тестирования я выбрал следующие (известные мне) способы определения наличия атрибута:
- Самый, пожалуй, очевидный — использовать встроенную функцию
hasattr(obj, name)
. - Другой распространенный способ — попытаться обратиться к атрибуту и, если не выйдет, принять меры, обработав
AttributeError
. - Тоже использовать обработку
AttributeError
, однако обращаться к свойству не напрямую, а черезgetattr(obj, name)
. Ситуация выглядит надуманной, но в реальном применении возможны ситуации, когда имя атрибута для проверки формируется динамически иgetattr
там как нельзя кстати. - Очень быстрый (см. чуть ниже результаты теста) метод — посмотреть в
__dict__
объекта (если он у него есть, конечно). Проблема при применении этого метода заключается, пожалуй, лишь в том, что__dict__
'ы раздельны для экземляра класса, самого класса и всех его предков. Если не знать точно, где находится нужный нам атрибут, этот метод не имеет для нас практической ценности. Следует заметить и то, что смотреть в__dict__
-ы можно тоже двумя путями — используя методыdict.has_key(key_name)
иdict.__contains__(key_name)
, который соответствует ключевому словуin
(assert 'My Key Name' in my_dict
). С учетом всех преимуществ, недостатков и двух вариантов реализации__dict__
на два отдельных метода не тянет, считаем его «полуторным». - Последний, самый экзотический метод, заключается в просмотре
dir(obj)
на предмет имени нужного нам атрибута. Кстати, в процессе проверки вложенных__slots__
-классов были обнаружены некоторые интересные моменты, связанные сdir()
, но об этом в отдельной статье :)
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