Подтвердить что ты не робот

Используя 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 → Example 1, Python 3.5 → Example 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
4b9b3361

Ответ 1

Вы можете использовать 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.

Ответ 2

  Использование 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 позволяет пользователям отрицать их создание для дочерних классов и экономить память - никаких минусов нет, если вы уже не использовали __slots__ в дочерних классах и не полагались на неявные __dict__ или __weakref__ создание от родителя ABC.

Быстрое решение проблемы - объявить __dict__ или __weakref__ в вашем дочернем классе в зависимости от ситуации. Лучше (для __dict__) явным образом объявить всех ваших участников.

Ответ 3

Я предпочитаю ответ Аарона Холла, но важно отметить, что в этом случае комментарий, который является частью строки:

ABC = abc.ABCMeta('ABC', (object,), {}) # compatible with Python 2 *and* 3 

... каждый бит так же важен, как и сам код. Без комментариев нет ничего, что помешало бы будущему ковбою по дороге удалять линию и менять наследование класса на:

class SomeAbstractClass(abc.ABC):

... таким образом, сломав все pre 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

Строго говоря, этого не нужно делать, но совершенно ясно, даже без комментариев, что происходит.

Ответ 4

Просто скажем, что вы должны явно передать 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'), (), {})

Это не требует отдельного ответа, но, к сожалению, я не могу комментировать ваши (нужно больше rep).