你如何在python中验证duck-typed接口?

时间:2012-02-10 06:38:24

标签: python

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返回true,为1和3返回false。

是否有另一种方法可以使用issubclass或其他方法进行接口测试,允许1 2通过,但拒绝3?

能够使用duck typing而不是将类显式绑定到抽象类型对我来说非常有帮助,但是当duck-typed对象通过特定接口时也允许对象检查。

是的,我知道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 个答案:

答案 0 :(得分:9)

查看ABC module。您可以定义一个抽象基类,该基类提供__subclasshook__方法,该方法根据您喜欢的任何条件定义特定类“是否是抽象基类的子类” - 例如“它有方法X,Y和Z“或其他什么。然后,您可以使用issubclass()isinstance()来检测类和实例上的接口。

答案 1 :(得分: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

如果CMetaClass的尝试类,那么C.__abstractmethods__只有在C实现所有抽象方法时才会为空。

请参阅此处了解详细信息:https://www.python.org/dev/peps/pep-3119/#the-abc-module-an-abc-support-framework(它位于“实施”下,但搜索__abstractmethods__会使您找到正确的段落)

这对我有用:

我可以创建MetaClass。然后,我可以将BaseClassMetaClass子类化为创建SubClass,这需要一些额外的功能。但我需要通过更改BaseClass属性将SubClass的实例强制转换为__cls__,因为我不拥有BaseClass但是我得到了它的实例想要抛弃。

但是,如果我不正确地实现SubClass,我仍然可以抛弃,除非我使用上面的__subclasshook__并在我执行强制转换过程时添加一个子类检查(我应该这样做我想只尝试将父类强制转换。如果有人要求,我可以为此提供一个MWE。

ETA:这是一个MWE。我认为我之前提出的建议是不正确的,所以以下似乎可以按照我的意图行事。

目标是能够将BaseClass对象转换为SubClass并返回。从SubClassBaseClass很容易。但是从BaseClassSubClass并没有那么多。标准的做法是更新__class__属性,但是当SubClass实际上不是子类或从抽象元类派生但未正确实现时,它会留下空缺。

下面,转换是在convert实现的BaseMetaClass方法中完成的。但是,在那个逻辑中,我检查了两件事。一,为了转换为子类,我检查它是否确实是一个子类。其次,我检查属性__abstractmethods__以查看它是否为空。如果是,那么它也是一个正确实现的元类。失败会导致TypeError。否则转换对象。

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

答案 2 :(得分:1)

这里有一个在实践中也很有效的替代方案,没有在每个类实例创建时检查整个字典的麻烦。

(py2和py3兼容)

用法:

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