How to enforce method signature for child classes?

后端 未结 5 1655
隐瞒了意图╮
隐瞒了意图╮ 2021-01-06 17:05

Languages like C#, Java has method overloads, which means if child class does not implement the method with exact signature will not overwrite the parent method.

How

相关标签:
5条回答
  • 2021-01-06 17:44

    I use meta classes for others purposes in my code so I rolled a version that uses a class decorator instead. The below version works with python3. and also supports decorated methods (yes, this creates a potential loophole but if you use decorators that changes the actual signature, shame on you). To make it work with python2, change inspect.isfunction to inspect.ismethod

    import inspect
    from functools import wraps
    
    class BadSignatureException(Exception):
        pass
    
    def enforce_signatures(cls):
        for method_name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
            if method_name == "__init__":
                continue
            for base_class in inspect.getmro(cls):
                if base_class is cls:
                    continue
    
                try:
                    base_method = getattr(base_class, method_name)
                except AttributeError:
                    continue
    
                if not inspect.signature(method) == inspect.signature(base_method):
                    raise BadSignatureException("%s.%s does not match base class %s.%s" % (cls.__name__, method_name,
                                                                                           base_class.__name__, method_name))
    
        return cls
    
    if __name__ == "__main__":
        class A:
            def foo(self, x):
                pass
    
        def test_decorator(f):
            @wraps(f)
            def decorated_function(*args, **kwargs):
                pass
            return decorated_function
    
        @enforce_signatures
        class B(A):
            @test_decorator
            def foo(self):
                """This override shouldn't work because the signature is wrong"""
                pass
    
    0 讨论(0)
  • 2021-01-06 17:45

    By design, the language doesn't support checking the signatures. For an interesting read, check out:

    http://grokbase.com/t/python/python-ideas/109qtkrzsd/abc-what-about-the-method-arguments

    From this thread, it does sound like you may be able to write a decorator to check the signature, with abc.same_signature(method1, method2), but I've never tried that.

    0 讨论(0)
  • 2021-01-06 17:47

    The reason it is being overridden is because they actually have the same method signature. What is written there is akin to doing something like this in Java:

    public class A
    {
        public void m(String p)
        {
            throw new Exception("Not implemented");
        }
    }
    
    public class B extends A
    {
        public void m(String p2)
        {
            System.out.println(p2);
        }
    }
    

    Note that even though the paramater names are different, the types are the same and thus they have the same signature. In strongly typed languages like this, we get to explicitly say what the types are going to be ahead of time.

    In python the type of the paramater is dynamically determined at run time when you use the method. This makes it impossible for the python interpreter to tell which method you actually wished to call when you say B().m('123'). Because neither of the method signatures specify which type of paramater they expect, they simply say I'm looking for a call with one parameter. So it makes sense that the deepest (and most relevent to the actual object you have) is called, which would be class B's method because it is an instance of class B.

    If you want to only process cetain types in a child class method, and pass along all others to the parent class, it can be done like this:

    class A(object):
        def m(self, p=None):
            raise NotImplementedError('Not implemented')
    
    class B(A):
        def m(self, p2=None):
            if isinstance(p2, int):
                print p2
            else:
                super(B, self).m(p2)
    

    Then using b gives you the desired output. That is, class b processes ints, and passes any other type along to its parent class.

    >>> b = B()
    >>> b.m(2)
    2
    >>> b.m("hello")
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 6, in m
      File "<stdin>", line 3, in m
    NotImplementedError: Not implemented
    
    0 讨论(0)
  • 2021-01-06 17:50

    Below is a complete running example showing how to use a metaclass to make sure that subclass methods have the same signatures as their base classes. Note the use of the inspect module. The way I'm using it here it makes sure that the signatures are exactly the same, which might not be what you want.

    import inspect
    
    class BadSignatureException(Exception):
        pass
    
    
    class SignatureCheckerMeta(type):
        def __new__(cls, name, baseClasses, d):
            #For each method in d, check to see if any base class already
            #defined a method with that name. If so, make sure the
            #signatures are the same.
            for methodName in d:
                f = d[methodName]
                for baseClass in baseClasses:
                    try:
                        fBase = getattr(baseClass, methodName).__func__
                        if not inspect.getargspec(f) == inspect.getargspec(fBase):
                            raise BadSignatureException(str(methodName))
                    except AttributeError:
                        #This method was not defined in this base class,
                        #So just go to the next base class.
                        continue
    
            return type(name, baseClasses, d)
    
    
    def main():
    
        class A(object):
            def foo(self, x):
                pass
    
        try:
            class B(A):
                __metaclass__ = SignatureCheckerMeta
                def foo(self):
                    """This override shouldn't work because the signature is wrong"""
                    pass
        except BadSignatureException:
            print("Class B can't be constructed because of a bad method signature")
            print("This is as it should be :)")
    
        try:
            class C(A):
                __metaclass__ = SignatureCheckerMeta
                def foo(self, x):
                    """This is ok because the signature matches A.foo"""
                    pass
        except BadSignatureException:
            print("Class C couldn't be constructed. Something went wrong")
    
    
    if __name__ == "__main__":
        main()
    
    0 讨论(0)
  • 2021-01-06 17:59

    Update of the accepted answer to work with python 3.5.

    import inspect
    from types import FunctionType
    
    class BadSignatureException(Exception):
        pass
    
    
    class SignatureCheckerMeta(type):
        def __new__(cls, name, baseClasses, d):
            #For each method in d, check to see if any base class already
            #defined a method with that name. If so, make sure the
            #signatures are the same.
            for methodName in d:
                f = d[methodName]
    
                if not isinstance(f, FunctionType):
                    continue
                for baseClass in baseClasses:
                    try:
                        fBase = getattr(baseClass, methodName)
                        if not inspect.getargspec(f) == inspect.getargspec(fBase):
                            raise BadSignatureException(str(methodName))
                    except AttributeError:
                        #This method was not defined in this base class,
                        #So just go to the next base class.
                        continue
    
            return type(name, baseClasses, d)
    
    
    def main():
        class A(object):
            def foo(self, x):
                pass
    
        try:
            class B(A, metaclass=SignatureCheckerMeta):
                def foo(self):
                    """This override shouldn't work because the signature is wrong"""
                    pass
        except BadSignatureException:
            print("Class B can't be constructed because of a bad method signature")
            print("This is as it should be :)")
    
        try:
            class C(A):
                __metaclass__ = SignatureCheckerMeta
                def foo(self, x):
                    """This is ok because the signature matches A.foo"""
                    pass
        except BadSignatureException:
            print("Class C couldn't be constructed. Something went wrong")
    
    
    if __name__ == "__main__":
        main()
    
    0 讨论(0)
提交回复
热议问题