如何验证Python中的鸭子类型接口?

8 投票
3 回答
3297 浏览
提问于 2025-04-17 12:44
class ITestType(object):
  """ Sample interface type """

  __metaclass__ = ABCMeta

  @abstractmethod
  def requiredCall(self):
    return

class TestType1(object):
  """ Valid type? """
  def requiredCall(self):
    pass

class TestType2(ITestType):
  """ Valid type """
  def requiredCall(self):
    pass

class TestType3(ITestType):
  """ Invalid type """
  pass

在上面的例子中,issubclass(TypeType*, ITestType) 对于 2 会返回真,而对于 1 和 3 则返回假。

有没有其他的方法可以使用 issubclass,或者有其他的接口测试方法,可以让 1 2 通过,但拒绝 3 呢?

对我来说,能够使用鸭子类型(duck typing)而不是明确将类绑定到抽象类型是非常有帮助的,同时也能在鸭子类型的对象通过特定接口时进行对象检查。

是的,我知道 Python 的人不太喜欢接口,标准的方法论是“在失败时找到问题,并用异常包裹一切”,但这和我的问题完全无关。不,我不能在这个项目中简单地不使用接口。

编辑:

太好了!对于其他看到这个问题的人,这里有一个如何使用 subclasshook 的例子:

class ITestType(object):
  """ Sample interface type """

  __metaclass__ = ABCMeta

  @abstractmethod
  def requiredCall(self):
    return

  @classmethod
  def __subclasshook__(cls, C):
    required = ["requiredCall"]
    rtn = True
    for r in required:
      if not any(r in B.__dict__ for B in C.__mro__):
        rtn = NotImplemented
    return rtn

3 个回答

1

这里有一个替代方案,在实际使用中效果也很好,不需要在每次创建类实例时都检查整个字典,省去了很多麻烦。

(兼容Python 2和Python 3)

用法:

class Bar():
  required_property_1 = ''

  def required_method(self):
    pass

# Module compile time check that Foo implements Bar
@implements(Bar)
class Foo(UnknownBaseClassUnrelatedToBar):
  required_property_1

   def required_method(self):
     pass

# Run time check that Foo uses @implements or defines its own __implements() member
def accepts_bar(self, anything):
  if not has_api(anything, Bar):
    raise Exception('Target does not implement Bar')
  ...

你还可以做一些显而易见的事情,比如 @implements(Stream, Folder, Bar),当它们都需要一些相同的方法时,这样在实际应用中比继承更有用。

代码:

import inspect


def implements(*T):
  def inner(cls):
    cls.__implements = []
    for t in T:

      # Look for required methods
      t_methods = inspect.getmembers(t, predicate=lambda x: inspect.isfunction(x) or inspect.ismethod(x))
      c_methods = inspect.getmembers(cls, predicate=lambda x: inspect.isfunction(x) or inspect.ismethod(x))
      sig = {}
      for i in t_methods:
        name = 'method:%s' % i[0]
        if not name.startswith("method:__"):
          sig[name] = False
      for i in c_methods:
        name = 'method:%s' % i[0]
        if name in sig.keys():
          sig[name] = True

      # Look for required properties
      t_props = [i for i in inspect.getmembers(t) if i not in t_methods]
      c_props = [i for i in inspect.getmembers(cls) if i not in c_methods]
      for i in t_props:
        name = 'property:%s' % i[0]
        if not name.startswith("property:__"):
          sig[name] = False
      for i in c_props:
        name = 'property:%s' % i[0]
        if name in sig.keys():
          sig[name] = True

      missing = False
      for i in sig.keys():
        if not sig[i]:
          missing = True
      if missing:
        raise ImplementsException(cls, t, sig)
      cls.__implements.append(t)
    return cls
  return inner


def has_api(instance, T):
  """ Runtime check for T in type identity """
  rtn = False
  if instance is not None and T is not None:
    if inspect.isclass(instance):
      if hasattr(instance, "__implements"):
        if T in instance.__implements:
          rtn = True
    else:
      if hasattr(instance.__class__, "__implements"):
        if T in instance.__class__.__implements:
          rtn = True
  return rtn


class ImplementsException(Exception):
  def __init__(self, cls, T, signature):
    msg = "Invalid @implements decorator on '%s' for interface '%s': %r" % (cls.__name__, T.__name__, signature)
    super(ImplementsException, self).__init__(msg)
    self.signature = signature
2

这事儿虽然晚了几年,但我来分享一下我的做法:

import abc

class MetaClass(object):
    __metaclass__ = abc.ABCMeta

    [...]

    @classmethod
    def __subclasshook__(cls, C):
        if C.__abstractmethods__:
            print C.__abstractmethods__
            return False
        else:
            return True

如果 C 是一个尝试创建的 MetaClass 类,那么只有当 C 实现了所有的抽象方法时,C.__abstractmethods__ 才会是空的。

详细信息可以查看这里:https://www.python.org/dev/peps/pep-3119/#the-abc-module-an-abc-support-framework(在“实现”部分,但搜索 __abstractmethods__ 应该能找到相关段落)

我在哪些地方用过这个:

我可以创建 MetaClass。然后我可以从 BaseClassMetaClass 继承,创建一个需要额外功能的 SubClass。但是我需要把一个 BaseClass 的实例转换成 SubClass,因为我不拥有 BaseClass,但我有它的实例想要进行转换。

不过,如果我不正确地实现 SubClass,我仍然可以进行转换,除非我使用上面提到的 __subclasshook__,并在转换过程中添加一个子类检查(我应该这样做,因为我只想尝试将父类转换为子类)。如果有人需要,我可以提供一个最小可重现示例(MWE)。

补充:这里是一个最小可重现示例。我之前的建议可能不正确,所以以下内容似乎实现了我想要的效果。

目标是能够将一个 BaseClass 对象转换为 SubClass,再转换回来。从 SubClass 转换到 BaseClass 很简单。但从 BaseClass 转换到 SubClass 就不那么容易了。通常的做法是更新 __class__ 属性,但这样会有风险,因为 SubClass 可能并不是真正的子类,或者是从一个抽象元类派生的,但没有正确实现。

下面的转换是在 BaseMetaClass 实现的 convert 方法中完成的。不过,在这个逻辑中,我检查了两个事情。首先,为了转换为子类,我检查它是否确实是一个子类。其次,我检查 __abstractmethods__ 属性,看它是否为空。如果为空,那么它也是一个正确实现的元类。如果不符合条件,就会引发一个类型错误。否则,对象就会被转换。

import abc


class BaseMetaClass(object):
    __metaclass__ = abc.ABCMeta

    @classmethod
    @abc.abstractmethod
    def convert(cls, obj):
        if issubclass(cls, type(obj)):
            if cls.__abstractmethods__:
                msg = (
                    '{0} not a proper subclass of BaseMetaClass: '
                    'missing method(s)\n\t'
                ).format(
                    cls.__name__
                )
                mthd_list = ',\n\t'.join(
                    map(
                        lambda s: cls.__name__ + '.' + s,
                        sorted(cls.__abstractmethods__)
                    )
                )
                raise TypeError(msg + mthd_list)

            else:
                obj.__class__ = cls
                return obj
        else:
            msg = '{0} not subclass of {1}'.format(
                cls.__name__,
                type(obj).__name__
            )
            raise TypeError(msg)

    @abc.abstractmethod
    def abstractmethod(self):
        return


class BaseClass(object):

    def __init__(self, x):
        self.x = x

    def __str__(self):
        s0 = "BaseClass:\n"
        s1 = "x: {0}".format(self.x)
        return s0 + s1


class AnotherBaseClass(object):

    def __init__(self, z):
        self.z = z

    def __str__(self):
        s0 = "AnotherBaseClass:\n"
        s1 = "z: {0}".format(self.z)
        return s0 + s1


class GoodSubClass(BaseMetaClass, BaseClass):

    def __init__(self, x, y):
        super(GoodSubClass, self).__init__(x)
        self.y = y

    @classmethod
    def convert(cls, obj, y):
        super(GoodSubClass, cls).convert(obj)
        obj.y = y

    def to_base(self):
        return BaseClass(self.x)

    def abstractmethod(self):
        print "This is the abstract method"

    def __str__(self):
        s0 = "SubClass:\n"
        s1 = "x: {0}\n".format(self.x)
        s2 = "y: {0}".format(self.y)
        return s0 + s1 + s2


class BadSubClass(BaseMetaClass, BaseClass):

    def __init__(self, x, y):
        super(BadSubClass, self).__init__(x)
        self.y = y

    @classmethod
    def convert(cls, obj, y):
        super(BadSubClass, cls).convert(obj)
        obj.y = y

    def __str__(self):
        s0 = "SubClass:\n"
        s1 = "x: {0}\n".format(self.x)
        s2 = "y: {0}".format(self.y)
        return s0 + s1 + s2


base1 = BaseClass(1)
print "BaseClass instance"
print base1
print


GoodSubClass.convert(base1, 2)
print "Successfully casting BaseClass to GoodSubClass"
print base1
print

print "Cannot cast BaseClass to BadSubClass"
base1 = BaseClass(1)
try:
    BadSubClass.convert(base1, 2)
except TypeError as e:
    print "TypeError: {0}".format(e.message)
    print


print "Cannot cast AnotherBaseCelass to GoodSubClass"
anotherbase = AnotherBaseClass(5)
try:
    GoodSubClass.convert(anotherbase, 2)
except TypeError as e:
    print "TypeError: {0}".format(e.message)
    print

print "Cannot cast AnotherBaseCelass to BadSubClass"
anotherbase = AnotherBaseClass(5)
try:
    BadSubClass.convert(anotherbase, 2)
except TypeError as e:
    print "TypeError: {0}".format(e.message)
    print


# BaseClass instance
# BaseClass:
# x: 1

# Successfully casting BaseClass to GoodSubClass
# SubClass:
# x: 1
# y: 2

# Cannot cast BaseClass to BadSubClass
# TypeError: BadSubClass not a proper subclass of BaseMetaClass: missing method(s)
#     BadSubClass.abstractmethod

# Cannot cast AnotherBaseCelass to GoodSubClass
# TypeError: GoodSubClass not subclass of AnotherBaseClass

# Cannot cast AnotherBaseCelass to BadSubClass
# TypeError: BadSubClass not subclass of AnotherBaseClass
9

可以看看ABC模块。这个模块让你可以定义一个抽象基类,它里面有一个叫__subclasshook__的方法。这个方法可以根据你设定的标准来判断某个类是否是这个抽象基类的子类,比如你可以规定“这个类有方法X、Y和Z”或者其他条件。然后你就可以用issubclass()或者isinstance()来检查类和实例是否符合这些接口。

撰写回答