Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
293 views
in Technique[技术] by (71.8m points)

python - Abstract attribute (not property)?

What's the best practice to define an abstract instance attribute, but not as a property?

I would like to write something like:

class AbstractFoo(metaclass=ABCMeta):

    @property
    @abstractmethod
    def bar(self):
        pass

class Foo(AbstractFoo):

    def __init__(self):
        self.bar = 3

Instead of:

class Foo(AbstractFoo):

    def __init__(self):
        self._bar = 3

    @property
    def bar(self):
        return self._bar

    @bar.setter
    def setbar(self, bar):
        self._bar = bar

    @bar.deleter
    def delbar(self):
        del self._bar

Properties are handy, but for simple attribute requiring no computation they are an overkill. This is especially important for abstract classes which will be subclassed and implemented by the user (I don't want to force someone to use @property when he just could have written self.foo = foo in the __init__).

Abstract attributes in Python question proposes as only answer to use @property and @abstractmethod: it doesn't answer my question.

The ActiveState recipe for an abstract class attribute via AbstractAttribute may be the right way, but I am not sure. It also only works with class attributes and not instance attributes.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

It's 2018, we deserve a bit better solution:

from better_abc import ABCMeta, abstract_attribute    # see below

class AbstractFoo(metaclass=ABCMeta):

    @abstract_attribute
    def bar(self):
        pass

class Foo(AbstractFoo):
    def __init__(self):
        self.bar = 3

class BadFoo(AbstractFoo):
    def __init__(self):
        pass

It will behave like this:

Foo()     # ok
BadFoo()  # will raise: NotImplementedError: Can't instantiate abstract class BadFoo
# with abstract attributes: bar

This answer uses same approach as the accepted answer, but integrates well with built-in ABC and does not require boilerplate of check_bar() helpers.

Here is the better_abc.py content:

from abc import ABCMeta as NativeABCMeta

class DummyAttribute:
    pass

def abstract_attribute(obj=None):
    if obj is None:
        obj = DummyAttribute()
    obj.__is_abstract_attribute__ = True
    return obj


class ABCMeta(NativeABCMeta):

    def __call__(cls, *args, **kwargs):
        instance = NativeABCMeta.__call__(cls, *args, **kwargs)
        abstract_attributes = {
            name
            for name in dir(instance)
            if getattr(getattr(instance, name), '__is_abstract_attribute__', False)
        }
        if abstract_attributes:
            raise NotImplementedError(
                "Can't instantiate abstract class {} with"
                " abstract attributes: {}".format(
                    cls.__name__,
                    ', '.join(abstract_attributes)
                )
            )
        return instance

The nice thing is that you can do:

class AbstractFoo(metaclass=ABCMeta):
    bar = abstract_attribute()

and it will work same as above.

Also one can use:

class ABC(ABCMeta):
    pass

to define custom ABC helper. PS. I consider this code to be CC0.

This could be improved by using AST parser to raise earlier (on class declaration) by scanning the __init__ code, but it seems to be an overkill for now (unless someone is willing to implement).

2021: typing support

You can use:

from typing import cast, Any, Callable, TypeVar


R = TypeVar('R')


def abstract_attribute(obj: Callable[[Any], R] = None) -> R:
    _obj = cast(Any, obj)
    if obj is None:
        _obj = DummyAttribute()
    _obj.__is_abstract_attribute__ = True
    return cast(R, _obj)

which will let mypy highlight some typing issues

class AbstractFooTyped(metaclass=ABCMeta):

    @abstract_attribute
    def bar(self) -> int:
        pass


class FooTyped(AbstractFooTyped):
    def __init__(self):
        # skipping assignment (which is required!) to demonstrate
        # that it works independent of when the assignment is made
        pass


f_typed = FooTyped()
_ = f_typed.bar + 'test'   # Mypy: Unsupported operand types for + ("int" and "str")


FooTyped.bar = 'test'    # Mypy: Incompatible types in assignment (expression has type "str", variable has type "int")
FooTyped.bar + 'test'    # Mypy: Unsupported operand types for + ("int" and "str")

and for the shorthand notation, as suggested by @SMiller in the comments:

class AbstractFooTypedShorthand(metaclass=ABCMeta):
    bar: int = abstract_attribute()


AbstractFooTypedShorthand.bar += 'test'   # Mypy: Unsupported operand types for + ("int" and "str")

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...