“对接口编程,而非对对象编程”的Python版本是什么?

51 投票
4 回答
11338 浏览
提问于 2025-04-16 09:08

受到一个很棒的问题(和一堆很棒的回答)的启发,来自这里。

“针对接口编程,而不是对象”这句话在Python中有什么重要性吗?

我希望得到的回答像原问题中的那样,但希望能有Python的代码示例和一些想法。

4 个回答

19

接口的意思是你希望某些方法在不同的对象中都是存在的,并且是标准化的;这就是接口或抽象基类的意义所在,不管你想用什么样的实现方式。

举个例子(Java),你可能会有一个用于对称加密的接口,像这样:

public interface cipher 
{
    public void encrypt(byte[] block, byte[] key); 
    public void decrypt(byte[] block, byte[] key);    
}

然后你可以去实现它:

public class aes128 implements cipher
{ 
    public void encrypt(byte[] block, byte[] key)
    {
        //...
    }
    public void decrypt(byte[] block, byte[] key)
    {
        //...
    }
}

接下来,你可以这样声明一个对象:

cipher c;

我们在这里做了什么呢?我们创建了一个对象 c,它的类型必须和接口匹配。c 可以指向任何符合这个接口的东西,所以下一步就是:

c = new aes128();

现在你可以调用你期望 cipher 具备的方法了。

这就是 Java。现在看看在 Python 中该怎么做:

class aes128(Object):

    def __init__(self):
        pass

    def encrypt(self, block, key):
        # here I am going to pass, but you really 
        # should check what you were passed, it could be 
        # anything. Don't forget, if you're a frog not a duck
        # not to quack!
        pass

当你想使用这个对象,但不确定你得到的对象是什么时,可以直接尝试使用它:

c = aes128()
try:
    c.encrypt(someinput, someoutput)
except:
    print "eh? No encryption method?!"

在这里,你依赖于 c.encrypt 的实现,如果它无法处理传入的内容,就会 raise 错误,前提是这个方法存在。当然,如果 c 是一个字符串类型,而不是你需要的正确类型,它也会自动抛出错误,你会捕获到这个错误(希望如此)。

简单来说,一种编程方式是有类型的,你必须遵循接口规则,另一种方式则是说你甚至不需要把这些规则写下来,你只需相信如果没有错误,那就说明它工作正常。

我希望这能让你明白两者之间的实际区别。

31

要理解Python中的接口,首先得明白“鸭子类型”这个概念。根据Python的官方解释:

鸭子类型:这是一种编程风格,它不关注一个对象的具体类型来判断它是否有合适的接口;相反,它只是直接调用或使用这个对象的方法或属性(“如果它看起来像鸭子,叫声也像鸭子,那它就一定是鸭子。”)通过强调接口而不是具体类型,设计良好的代码可以提高灵活性,允许多态替换。鸭子类型避免使用type()或isinstance()这样的类型检查。不过,鸭子类型可以和抽象基类一起使用。通常,它会使用hasattr()测试或者EAFP(“先尝试,后处理”)的编程风格。

Python鼓励根据接口来编写代码,但这些接口并不是强制的,而是通过约定来实现的。像可迭代对象、可调用对象或者文件接口这样的概念在Python中非常普遍,还有一些内置函数,比如map、filter和reduce,它们也依赖于这些接口。

69

“针对接口编写代码,而不是对象”在Python中并不完全成立,因为Python没有接口这个特性。Python中类似的概念是“使用鸭子类型”。换句话说,如果你想判断一个对象是不是鸭子,你应该检查它是否有一个quack()的方法,或者更好的是,直接尝试调用quack()并处理可能出现的错误,而不是去测试它是否是Duck类的实例。

在Python中,常见的“鸭子类型”包括文件(其实是类似文件的对象)、映射(像dict的对象)、可调用对象(像函数的对象)、序列(像list的对象)和可迭代对象(可以遍历的东西,可能是容器或生成器)。

举个例子,Python中的一些功能如果需要文件,通常会接受任何实现了所需file方法的对象;这个对象不一定要是file类的子类。例如,要把一个对象用作标准输出,最重要的是它需要有一个write()方法(可能还需要flush()close(),但这些方法不一定要真的执行什么操作)。同样,一个可调用对象是指任何有__call__()方法的对象;它不需要是函数类型的子类(实际上,你不能从函数类型派生)。

你也应该采取类似的方法。检查你需要的对象的方法和属性。更好的是,记录下你期望的内容,并假设调用你代码的人不是完全的傻瓜。(如果他们给你一个你无法使用的对象,他们很快就会从错误中发现问题。)只有在必要时才测试特定类型。确实有时候是必要的,这就是为什么Python提供了type()isinstance()issubclass(),但使用这些时要小心。

Python的鸭子类型在某种意义上等同于“针对接口编写代码,而不是对象”,因为它建议你不要过于依赖对象的类型,而是看看它是否具备你需要的接口。不同的是,在Python中,“接口”只是指一个对象的属性和方法的非正式集合,这些属性和方法提供了某种行为,而不是一个专门命名为interface的语言构造。

你可以在一定程度上使用abc模块来规范化Python的“接口”,这个模块允许你声明某个类是某个“抽象基类”(接口)的子类,使用你想要的任何标准,比如“它有属性colortail_lengthquack,并且quack是可调用的。”但这仍然比静态语言中的接口特性要宽松得多。

撰写回答