Использование abc.ABCMeta таким образом, чтобы оно было совместимо с Python 2.7 и Python 3.5
Я хотел бы создать класс, который имеет abc.ABCMeta
как метакласс и совместим как с Python 2.7, так и с Python 3.5. До сих пор мне удавалось делать это только на 2.7 или на 3.5 - но никогда на обеих версиях одновременно. Может ли кто-нибудь помочь мне?
Python 2.7:
import abc
class SomeAbstractClass(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def do_something(self):
pass
Python 3.5:
import abc
class SomeAbstractClass(metaclass=abc.ABCMeta):
@abc.abstractmethod
def do_something(self):
pass
тестирование
Если мы запустим следующий тест с использованием подходящей версии интерпретатора Python (Python 2.7 -> Пример 1, Python 3.5 -> Пример 2), он будет успешным в обоих сценариях:
import unittest
class SomeAbstractClassTestCase(unittest.TestCase):
def test_do_something_raises_exception(self):
with self.assertRaises(TypeError) as error:
processor = SomeAbstractClass()
msg = str(error.exception)
expected_msg = "Can't instantiate abstract class SomeAbstractClass with abstract methods do_something"
self.assertEqual(msg, expected_msg)
проблема
При запуске теста с использованием Python 3.5 ожидаемого поведения не происходит (TypeError
не поднимается при создании экземпляра SomeAbstractClass
):
======================================================================
FAIL: test_do_something_raises_exception (__main__.SomeAbstractClassTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/tati/sample_abc.py", line 22, in test_do_something_raises_exception
processor = SomeAbstractClass()
AssertionError: TypeError not raised
----------------------------------------------------------------------
Принимая во внимание, что запуск теста с использованием Python 2.7 поднимает SyntaxError
:
Python 2.7 incompatible
Raises exception:
File "/home/tati/sample_abc.py", line 24
class SomeAbstractClass(metaclass=abc.ABCMeta):
^
SyntaxError: invalid syntax
4 ответа
Вы могли бы использовать six.add_metaclass
или же six.with_metaclass
:
import abc, six
@six.add_metaclass(abc.ABCMeta)
class SomeAbstractClass():
@abc.abstractmethod
def do_something(self):
pass
six
библиотека совместимости Python 2 и 3 Вы можете установить его, запустив pip install six
или загрузив последнюю версию six.py
в каталог вашего проекта.
Для тех из вас, кто предпочитает future
над six
соответствующая функция future.utils.with_metaclass
,
Использование abc.ABCMeta таким образом, чтобы оно было совместимо с Python 2.7 и Python 3.5
Если бы мы использовали только Python 3 (это ново в 3.4), мы могли бы сделать:
from abc import ABC
и наследовать от ABC
вместо object
, То есть:
class SomeAbstractClass(ABC):
...etc
Вам все еще не нужна дополнительная зависимость (модуль из шести) - вы можете использовать метакласс для создания родителя (это, по сути, то, что делает модуль из шести в with_metaclass):
import abc
# compatible with Python 2 *and* 3:
ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()})
class SomeAbstractClass(ABC):
@abc.abstractmethod
def do_something(self):
pass
Или вы можете просто сделать это на месте (но это более грязно и не способствует повторному использованию):
# use ABCMeta compatible with Python 2 *and* 3
class SomeAbstractClass(abc.ABCMeta('ABC', (object,), {'__slots__': ()})):
@abc.abstractmethod
def do_something(self):
pass
Обратите внимание, что подпись выглядит немного грязнее, чем six.with_metaclass
но это по сути та же семантика, без дополнительной зависимости.
Любое решение
и теперь, когда мы пытаемся создать экземпляр без реализации абстракции, мы получаем именно то, что ожидаем:
>>> SomeAbstractClass()
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
SomeAbstractClass()
TypeError: Can't instantiate abstract class SomeAbstractClass with abstract methods do_something
Обратите внимание на __slots__ = ()
Мы только что добавили пустой __slots__
к удобному классу ABC в стандартной библиотеке Python 3, и мой ответ обновлен, чтобы включить его.
Не имея __dict__
а также __weakref__
доступны в ABC
parent позволяет пользователям отрицать их создание для дочерних классов и экономить память - минусов нет, если только вы не используете __slots__
в дочерних классах уже и полагаясь на неявное __dict__
или же __weakref__
создание из ABC
Родитель.
Быстрое решение было бы объявить __dict__
или же __weakref__
в вашем детском классе в зависимости от обстоятельств. Лучше (для __dict__
) может быть явно объявить всех ваших членов.
Я предпочитаю ответ Аарона Холла, но важно отметить, что в этом случае комментарий, который является частью строки:
ABC = abc.ABCMeta('ABC', (object,), {}) # compatible with Python 2 *and* 3
... так же важен, как и сам код. Без комментария ничто не может помешать будущему ковбою в будущем удалить строку и изменить наследование класса на:
class SomeAbstractClass(abc.ABC):
... таким образом ломая все до Python 3.4.
Один твик, который может быть немного более явным / понятным для кого-то другого - в том, что он самодокументирован - относительно того, чего вы пытаетесь достичь:
import sys
import abc
if sys.version_info >= (3, 4):
ABC = abc.ABC
else:
ABC = abc.ABCMeta('ABC', (), {})
class SomeAbstractClass(ABC):
@abc.abstractmethod
def do_something(self):
pass
Строго говоря, этого делать не нужно, но совершенно ясно, даже без комментариев, что происходит.
Просто сказать, что вы должны явно пройти str('ABC')
в abc.ABCMeta
в Python 2, если вы используете from __future__ import unicode_literals
,
В противном случае Python поднимает TypeError: type() argument 1 must be string, not unicode
,
Смотрите исправленный код ниже.
import sys
import abc
from __future__ import unicode_literals
if sys.version_info >= (3, 4):
ABC = abc.ABC
else:
ABC = abc.ABCMeta(str('ABC'), (), {})
Это не требует отдельного ответа, но, к сожалению, я не могу прокомментировать ваш (нужно больше представителей).