Абстрактные классы и PyMongo; не может создать экземпляр абстрактного класса
Я создал пустой абстрактный класс AbstractStorage
и унаследовал Storage
класс от него:
import abc
import pymongo as mongo
host = mongo.MongoClient()
print(host.alive()) # True
class AbstractStorage(metaclass=abc.ABCMeta):
pass
class Storage(AbstractStorage):
dbh = host
def __init__(self):
print('__init__')
Storage()
Я ожидал, что результат будет
True
__init__
однако, тот, который я получаю,
True
Traceback (most recent call last):
File "/home/vaultah/run.py", line 16, in <module>
Storage()
TypeError: Can't instantiate abstract class Storage with abstract methods dbh
Проблема (видимо) исчезнет, если я уберу metaclass=abc.ABCMeta
(чтобы AbstractStorage
становится обычным классом) и / или если я установлю dbh
к какой-то другой ценности.
Что тут происходит?
2 ответа
На самом деле это не проблема ABC, это проблема PyMongo. Здесь есть проблема. Кажется, что Pymongo переопределяет __getattr__
вернуть какой-то класс базы данных. Это означает, что host.__isabstractmethod__
возвращает объект базы данных, который является истинным в логическом контексте. Это заставляет ABCMeta полагать, что host
это абстрактный метод:
>>> bool(host.__isabstractmethod__)
True
Обходной путь, описанный в отчете о проблеме, должен быть установлен вручную host.__isabstractmethod__ = False
на вашем объекте. В последнем комментарии к проблеме предлагается исправление для pymongo 3.0.
Укороченная версия
mongo.MongoClient
возвращает объект, который представляется (является?) абстрактным методом, который вы затем назначаете dbh
поле в Storage
, Это делает Storage
абстрактный класс, поэтому создание его экземпляра поднимает TypeError
,
Обратите внимание, что у меня нет pymongo
так что я не могу рассказать вам больше о MongoClient
чем это лечится ABCMeta
,
Длинная версия
ABCMeta.__new__
Метод ищет внутри каждого поля нового класса, который он создает. Любое поле, которое само по себе имеет True
(или "правда") __isabstractmethod__
Поле считается абстрактным методом. Если у класса есть какие -либо не переопределенные абстрактные методы, весь класс считается абстрактным, поэтому любая попытка его создания является ошибкой.
Из более ранней версии стандартной библиотекиabc.py
:
def __new__(mcls, name, bases, namespace):
cls = super().__new__(mcls, name, bases, namespace)
# Compute set of abstract method names
abstracts = {name
for name, value in namespace.items()
if getattr(value, "__isabstractmethod__", False)}
# ...
cls.__abstractmethods__ = frozenset(abstracts)
# ...
Это не упоминается вabc.ABCMeta
класс документов, но немного ниже, под@abc.abstractmethod
декоратор:
Чтобы правильно взаимодействовать с механизмом абстрактного базового класса, дескриптор должен идентифицировать себя как абстрактный, используя
__isabstractmethod__
, В общем, этот атрибут должен бытьTrue
если какой-либо из методов, использованных для составления дескриптора, является абстрактным.
пример
Я создал фиктивный "абстрактный" класс с __isabstractmethod__
атрибут и два предположительно конкретных подкласса AbstractStorage
, Вы увидите, что выдает именно ту ошибку, которую вы получаете:
#!/usr/bin/env python3
import abc
# I don't have pymongo, so I have to fake it. See CounterfeitAbstractMethod.
#import pymongo as mongo
class CounterfeitAbstractMethod():
"""
This class appears to be an abstract method to the abc.ABCMeta.__new__
method.
Normally, finding an abstract method in a class's namespace means
that class is also abstract, so instantiating that class is an
error.
If a class derived from abc.ABCMeta has an instance of
CounterfeitAbstractMethod as a value anywhere in its namespace
dictionary, any attempt to instantiate that class will raise a
TypeError: Can't instantiate abstract class <classname> with
abstract method <fieldname>.
"""
__isabstractmethod__ = True
class AbstractStorage(metaclass=abc.ABCMeta):
def __init__(self):
"""
Do-nothing initializer that prints the name of the (sub)class
being initialized.
"""
print(self.__class__.__name__ + ".__init__ executing.")
return
class ConcreteStorage(AbstractStorage):
"""
A concrete class that also _appears_ concrete to abc.ABCMeta. This
class can be instantiated normally.
"""
whatever = "Anything that doesn't appear to be an abstract method will do."
class BogusStorage(AbstractStorage):
"""
This is (supposedly) a concrete class, but its whatever field appears
to be an abstract method, making this whole class abstract ---
abc.ABCMeta will refuse to construct any this class.
"""
#whatever = mongo.MongoClient('localhost', 27017)
whatever = CounterfeitAbstractMethod()
def main():
"""
Print details of the ConcreteStorage and BogusStorage classes.
"""
for cls in ConcreteStorage, BogusStorage:
print(cls.__name__ + ":")
print(" whatever field: " + str(cls.whatever))
print(" abstract methods: " + str(cls.__abstractmethods__))
print(" Instantiating...")
print(" ", end="")
# KABOOM! Instantiating BogusStorage will raise a TypeError,
# because it appears to be an _abstract_ class.
instance = cls()
print(" instance: " + str(instance))
print()
return
if "__main__" == __name__:
main()
Запуск этого дает:
$ ./storage.py
ConcreteStorage:
whatever field: Anything that doesn't appear to be an abstract method will do.
abstract methods: frozenset()
Instantiating...
ConcreteStorage.__init__ executing.
instance: <__main__.ConcreteStorage object at 0x253afd0>
BogusStorage:
whatever field: <__main__.CounterfeitAbstractMethod object at 0x253ad50>
abstract methods: frozenset({'whatever'})
Instantiating...
Traceback (most recent call last):
File "./storage.py", line 75, in <module>
main()
File "./storage.py", line 68, in main
instance = cls()
TypeError: Can't instantiate abstract class BogusStorage with abstract methods whatever