问题
Suppose I define an abstract base class like this:
from abc import abstractmethod, ABCMeta
class Quacker(object):
__metaclass__ = ABCMeta
@abstractmethod
def quack(self):
return "Quack!"
This ensures any class deriving from Quacker
must implement the quack
method. But if I define the following:
class PoliteDuck(Quacker):
def quack(self, name):
return "Quack quack %s!" % name
d = PoliteDuck() # no error
I'm allowed to instantiate the class because I've provided the quack
method, but the function signatures don't match. I can see how this might be useful in some situations, but I'm in interested in ensuring I can definitely call the abstract methods. This might fail if the function signature is different!
So: how can I enforce a matching function signature? I would expect an error when creating the object if the signatures don't match, just like if I hadn't defined it at all.
I know that this is not idiomatic, and that Python is the wrong language to be using if I want these sorts of guarantees, but that's beside the point - is it possible?
回答1:
It's worse than you think. Abstract methods are tracked by name only, so you don't even have to make quack
a method in order to instantiate the child class.
class SurrealDuck(Quacker):
quack = 3
d = SurrealDuck()
print d.quack # Shows 3
There is nothing in the system that enforces that quack
is even a callable object, let alone one whose arguments match the abstract method's original. At best, you could subclass ABCMeta
and add code yourself to compare type signatures in the child to the originals in the parent, but this would be nontrivial to implement.
(Currently, marking something as "abstract" essentially just adds the name to a frozen set attribute in the parent (Quacker.__abstractmethods__
). Making a class instantiable is as simple as setting this attribute to an empty iterable, which is useful for testing.)
回答2:
I recommend you look at pylint. I ran this code through it's static analysis, and on the line where you defined the quack() method, it reported:
Argument number differs from overridden method (arguments-differ)
(https://en.wikipedia.org/wiki/Pylint)
回答3:
I don't think this has changed in the base python
language, but I did find one workaround that might be useful. The mypy
package does seem to enforce signature conformity on abstract base classes and their concrete implementation. So basically if you define a signature on the abstract base class, all concrete classes have to follow the same exact signature.
Here is an example that will break in mypy
. The code is taken from the mypy
website, but I adapted it for this answer.
The first example is code that will pass. Note that the signatures for the eat
method are the same and mypy
does not complain.
from abc import ABCMeta, abstractmethod
class Animal(metaclass=ABCMeta):
@abstractmethod
def eat(self, food: str) -> None: pass
@property
@abstractmethod
def can_walk(self) -> bool: pass
class Cat(Animal):
def eat(self, food: str) -> None:
pass # Body omitted
@property
def can_walk(self) -> bool:
return True
y = Cat() # OK
But let's adapt this code a bit and now mypy
throws an error:
from abc import ABCMeta, abstractmethod
class Animal(metaclass=ABCMeta):
@abstractmethod
def eat(self, food: str) -> None: pass
@property
@abstractmethod
def can_walk(self) -> bool: pass
class Cat(Animal):
def eat(self, food: str, drink: str) -> None:
pass # Body omitted
@property
def can_walk(self) -> bool:
return True
y = Cat() # Error
Mypy
is still very much a work-in-progress but it does work in this case. There are some corner cases where some signature variants might not get caught, but otherwise seems to work for most practical applications.
来源:https://stackoverflow.com/questions/25183424/can-a-python-abstract-base-class-enforce-function-signatures