shan

在Python中实现接口

2020-02-22

原文:Implementing an Interface in Python

在软件工程中,接口扮演者一个重要的角色。随着应用的发展,代码库升级和更改变得越来越难以管理。通常,你会遇到一些看上去很相似但却不相关的类,这可能会导致一些困惑。在这篇文档中,你会看到如何使用Python的接口去决定对于解决当前问题什么样的类是你需要的。

在这篇文章中,你能够

  • 懂得接口是如何工作的以及Python接口创建的注意事项
  • 理解在像Python这样的动态语言中接口的有用性
  • 实现一个非正式的Python接口
  • 使用 abc.ABCMeta@abc.abstractmethod 去实现一个正式的Python接口

Python中的接口与大多数其他语言的处理方式不同,并且它们的设计复杂度可能会有所不同。在本篇文档结束时,你将会更好地理解Python数据模型的某些方面,以及Python中的接口与Java,C ++和Go等语言的接口相比有什么不同。

Python Interface Overview

在较高的层次上,接口充当设计类的蓝图。像类一样,接口定义方法。与类不同的是,这些方法是抽象的。抽象方法是接口简单定义的一种方法。它不会实现这些方法。这是由类完成的,然后类实现接口并为接口的抽象方法赋予具体含义。

与Java,Go和C ++等语言相比,Python的接口设计方法是有所不同的。这些语言都有一个 interface 关键字,而Python却没有。 Python在另一个方面更进一步的偏离了其他语言。不需要实现接口的类来定义接口的所有抽象方法。

Informal Interfaces

在某些情况下,你可能不需要正式的Python接口的严格规则。 Python的动态特性使你可以实现非正式接口。非正式的Python接口是一个类,它定义了可以覆盖的方法,但没有严格的强制要求。

在下面的示例中,以数据工程师的角度,你需要从各种不同的非结构化的文件类型中提取文本(例如PDF和电子邮件)。你将会创建一个非正式的接口,该接口定义将在PdfParserEmlParser具体类中使用的方法:

1
2
3
4
5
6
7
8
class InformalParserInterface:
def load_data_source(self, path: str, file_name: str) -> str:
"""Load in the file for extracting text."""
pass

def extract_text(self, full_file_name: str) -> dict:
"""Extract text from the currently loaded file."""
pass

InformalParserInterface 定义了两个方法 .load_data_source().extract_text()。这些方法已定义但是并未实现。一旦创建从 InformalParserInterface 类继承的具体类,那么该实现就有了相关的方法。

如你所见,InformalParserInterface 看起来与标准Python类相同。依靠 duck typing, 你可以告诉用户这是一个接口,应该相应地使用它。

注意:没听说过duck typing吗?这个术语的意思是,如果你有一个看起来像鸭子的物体,像鸭子一样走路,像鸭子一样嘎嘎叫,那么它一定是鸭子!如果需要了解更多的相关信息,可以看看这篇文章:Duck Typing

牢记鸭子类型,现在,你已经定义了两个实现 InformalParserInterface 的类了。要使用你的接口,你必须创建一个具体的类。 具体类是接口类的子类,提供接口方法的具体实现。创建两个具体的类来实现你的接口。第一个是PdfParser,用来来解析PDF文件中的文本:

1
2
3
4
5
6
7
8
9
class PdfParser(InformalParserInterface):
"""Extract text from a PDF"""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides InformalParserInterface.load_data_source()"""
pass

def extract_text(self, full_file_path: str) -> dict:
"""Overrides InformalParserInterface.extract_text()"""
pass

现在InformalParserInterface 的具体实现允许你从PDF文件中提取文本。

第二个具体的类是EmlParser,用来解析电子邮件中的文本:

1
2
3
4
5
6
7
8
9
10
11
class EmlParser(InformalParserInterface):
"""Extract text from an email"""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides InformalParserInterface.load_data_source()"""
pass

def extract_text_from_email(self, full_file_path: str) -> dict:
"""A method defined only in EmlParser.
Does not override InformalParserInterface.extract_text()
"""
pass

现在,InformalParserInterface 的具体实现允许你可以从电子邮件文件中提取文本了。

到目前为止,你已经定义了InformalPythonInterface的两个具体实现。但是请注意,EmlParser 没有适当的定义 .extract_text()。如果你要检查EmlParser是否实现了InformalParserInterface,那么会得到下面的结果:

1
2
3
4
5
6
7
>>>
>>> # Check if both PdfParser and EmlParser implement InformalParserInterface
>>> issubclass(PdfParser, InformalParserInterface)
True

>>> issubclass(EmlParser, InformalParserInterface)
True

这会返回 True,由于违反了接口的定义,因此带来了一个问题!

现在检查 PdfParserEmlParser方法解析顺序(MRO)。这将告诉你所涉及类的超类,以及搜索它们从而执行方法的顺序。你可以使用特殊方法cls.__mro__查看类的MRO:

1
2
3
4
5
>>> PdfParser.__mro__
(__main__.PdfParser, __main__.InformalParserInterface, object)

>>> EmlParser.__mro__
(__main__.EmlParser, __main__.InformalParserInterface, object)

这样的非正式接口适用于小型项目,在小型项目中,只有少数开发人员会在源代码上工作。但是,随着项目规模的扩大和团队的成长,这可能导致开发人员花费大量时间在代码库中寻找难以发现的逻辑错误!

Using Metaclasses

理想情况下,当实现类未定义接口类的所有抽象方法时,你希望issubclass(EmlParser,InformalParserInterface)返回False。为此,你可以创建一个 metaclass 称为 ParserMeta,覆盖两个特殊方法:

  1. .__instancecheck__()
  2. .__subclasscheck__()

在下面的代码块中,你将创建一个名为UpdatedInformalParserInterface的类,该类是基于ParserMeta元类构建的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ParserMeta(type):
"""A Parser metaclass that will be used for parser class creation.
"""
def __instancecheck__(cls, instance):
return cls.__subclasscheck__(type(instance))

def __subclasscheck__(cls, subclass):
return (hasattr(subclass, 'load_data_source') and
callable(subclass.load_data_source) and
hasattr(subclass, 'extract_text') and
callable(subclass.extract_text))

class UpdatedInformalParserInterface(metaclass=ParserMeta):
"""This interface is used for concrete classes to inherit from.
There is no need to define the ParserMeta methods as any class
as they are implicitly made available via .__subclasscheck__().
"""
pass

现在,既然已经创建了ParserMetaUpdatedInformalParserInterface,就可以创建具体的实现了。

首先,创建一个用于解析PDF的新类,称为PdfParserNew

1
2
3
4
5
6
7
8
9
class PdfParserNew:
"""Extract text from a PDF."""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides UpdatedInformalParserInterface.load_data_source()"""
pass

def extract_text(self, full_file_path: str) -> dict:
"""Overrides UpdatedInformalParserInterface.extract_text()"""
pass

在这里,PdfParserNew会覆盖.load_data_source().extract_text(),因此issubclass(PdfParserNew,UpdatedInformalParserInterface)应该返回True

在下一个代码块中,你将使用名为EmlParserNew的电子邮件解析器的新实现:

1
2
3
4
5
6
7
8
9
10
11
class EmlParserNew:
"""Extract text from an email."""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides UpdatedInformalParserInterface.load_data_source()"""
pass

def extract_text_from_email(self, full_file_path: str) -> dict:
"""A method defined only in EmlParser.
Does not override UpdatedInformalParserInterface.extract_text()
"""
pass

这里,你有一个用于创建UpdatedInformalParserInterface的元类。通过使用元类,你无需显式定义子类。相反,子类必须定义所需的方法。如果没有,则issubclass(EmlParserNew,UpdatedInformalParserInterface)将返回False

在具体类上运行issubclass()会产生以下结果:

1
2
3
4
5
>>> issubclass(PdfParserNew, UpdatedInformalParserInterface)
True

>>> issubclass(EmlParserNew, UpdatedInformalParserInterface)
False

不出所料,由于EmlParserNew中未定义.extract_text(),因此EmlParserNew不是UpdatedInformalParserInterface的子类。

现在,让我们看一下MRO:

1
2
>>> PdfParserNew.__mro__
(<class '__main__.PdfParserNew'>, <class 'object'>)

如你所见,UpdatedInformalParserInterfacePdfParserNew的超类,但并未在MRO中显示。这种不正常的行为是由以下事实引起的:UpdatedInformalParserInterfacePdfParserNew虚拟基类

Using Virtual Base Classes

在前面的示例中,即使UpdatedInformalParserInterface没有出现在EmlParserNew MRO中,issubclass(EmlParserNew,UpdatedInformalParserInterface)也返回了True。这是因为UpdatedInformalParserInterfaceEmlParserNew虚拟基类

这些和标准子类之间的主要区别在于,虚拟基类使用.__subclasscheck__() 特殊方法隐式检查类是否为超类的虚拟子类。此外,虚拟基类不会出现在子类MRO中。

看一下以下代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PersonMeta(type):
"""A person metaclass"""
def __instancecheck__(cls, instance):
return cls.__subclasscheck__(type(instance))

def __subclasscheck__(cls, subclass):
return (hasattr(subclass, 'name') and
callable(subclass.name) and
hasattr(subclass, 'age') and
callable(subclass.age))

class PersonSuper:
"""A person superclass"""
def name(self) -> str:
pass

def age(self) -> int:
pass

class Person(metaclass=PersonMeta):
"""Person interface built from PersonMeta metaclass."""
pass

下面是创建虚拟基类的步骤:

  1. 元类PersonMeta
  2. 基类PersonSuper
  3. Python接口Person

现在已经完成了创建虚拟基类的步骤,定义两个具体的类,即EmployeeFriendEmployee类继承自PersonSuper,而Friend隐式继承自Person

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Inheriting subclasses
class Employee(PersonSuper):
"""Inherits from PersonSuper
PersonSuper will appear in Employee.__mro__
"""
pass

class Friend:
"""Built implicitly from Person
Friend is a virtual subclass of Person since
both required methods exist.
Person not in Friend.__mro__
"""
def name(self):
pass

def age(self):
pass

尽管Friend没有显式继承自Person,但它实现了 .name().age(),因此Person成为Friend虚拟基类。当运行issubclass(Friend, Person)时,它应该返回True,这意味着FriendPerson的子类。

下面的UML图显示了当你在Friend类上调用issubclass()时会发生什么:

virtual base class

看一下 PersonMeta,你会注意到还有另一种特殊方法,称为 .__instancecheck__()。该方法用于检查是否从Person接口创建了Friend的实例。当你使用isinstance(Friend, Person)时,你的代码将调用.__instancecheck__()

Formal Interfaces

非正式接口对于代码量少且程序员数量有限的项目很有用。但是对于大型应用程序,使用非正式接口是错误的方法。为了创建正式的Python接口,你将需要Python的abc模块中的一些其他工具。

Using abc.ABCMeta

为了实施抽象方法的子类实例化,你将使用Python内置模块abc中的ABCMeta。回到UpdatedInformalParserInterface接口,你使用覆盖的特殊方法.__instancecheck__().__subclasscheck__()创建了自己的元类ParserMeta

与其创建自己的元类,不如使用abc.ABCMeta作为元类。然后,使用.__subclasshook__()方法重写.__instancecheck__().__subclasscheck__()方法,因为它创建了特殊方法更可靠的实现。

Using .__subclasshook__()

这是使用abc.ABCMeta作为元类的FormalParserInterface的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import abc

class FormalParserInterface(metaclass=abc.ABCMeta):
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'load_data_source') and
callable(subclass.load_data_source) and
hasattr(subclass, 'extract_text') and
callable(subclass.extract_text))

class PdfParserNew:
"""Extract text from a PDF."""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides FormalParserInterface.load_data_source()"""
pass

def extract_text(self, full_file_path: str) -> dict:
"""Overrides FormalParserInterface.extract_text()"""
pass

class EmlParserNew:
"""Extract text from an email."""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides FormalParserInterface.load_data_source()"""
pass

def extract_text_from_email(self, full_file_path: str) -> dict:
"""A method defined only in EmlParser.
Does not override FormalParserInterface.extract_text()
"""
pass

如果在PdfParserNewEmlParserNew上运行issubclass(),则issubclass()将分别返回TrueFalse

Using abc to Register a Virtual Subclass

一旦你导入abc模块后,你可以使用.register()元方法直接注册虚拟子类。在下一个示例中,将接口Double注册为内置__float__类的虚拟子类:

1
2
3
4
5
class Double(metaclass=abc.ABCMeta):
"""Double precision floating point number."""
pass

Double.register(float)

你可以查看使用.register()的效果:

1
2
3
4
5
>>> issubclass(float, Double)
True

>>> isinstance(1.2345, Double)
True

通过使用.register()元方法,你已成功将Double登记为float的虚拟子类。

一旦注册了Double,你就可以将其用作类decorator,将装饰后的类设置为虚拟子类:

1
2
3
4
5
6
@Double.register
class Double64:
"""A 64-bit double-precision floating-point number."""
pass

print(issubclass(Double64, Double)) # True

装饰器注册方法可帮助你创建自定义虚拟类继承的层次结构。

Using Subclass Detection With Registration

.__subclasshook__().register()结合使用时必须小心,因为.__subclasshook__()优先于虚拟子类注册。为了确保考虑已注册的虚拟子类,必须在.__subclasshook__()特殊方法中添加NotImplementedFormalParserInterface将更新为以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class FormalParserInterface(metaclass=abc.ABCMeta):
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'load_data_source') and
callable(subclass.load_data_source) and
hasattr(subclass, 'extract_text') and
callable(subclass.extract_text) or
NotImplemented)

class PdfParserNew:
"""Extract text from a PDF."""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides FormalParserInterface.load_data_source()"""
pass

def extract_text(self, full_file_path: str) -> dict:
"""Overrides FormalParserInterface.extract_text()"""
pass

@FormalParserInterface.register
class EmlParserNew:
"""Extract text from an email."""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides FormalParserInterface.load_data_source()"""
pass

def extract_text_from_email(self, full_file_path: str) -> dict:
"""A method defined only in EmlParser.
Does not override FormalParserInterface.extract_text()
"""
pass

print(issubclass(PdfParserNew, FormalParserInterface)) # True
print(issubclass(EmlParserNew, FormalParserInterface)) # True

由于你已使用了注册,因此可以看到EmlParserNew被视为你FormalParserInterface接口的虚拟子类。
这不是你想要的,因为EmlParserNew不会覆盖.extract_text()请谨慎使用虚拟子类注册!

Using Abstract Method Declaration

抽象方法是Python接口声明的方法,但可能没有有用的实现。抽象方法必须由实现所讨论接口的具体类覆盖。

要在Python中创建抽象方法,你可以在接口的方法中添加@abc.abstractmethod装饰器。在下一个示例中,你将更新FormalParserInterface,使其包含抽象方法.load_data_source().extract_text()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class FormalParserInterface(metaclass=abc.ABCMeta):
@classmethod
def __subclasshook__(cls, subclass):
return (hasattr(subclass, 'load_data_source') and
callable(subclass.load_data_source) and
hasattr(subclass, 'extract_text') and
callable(subclass.extract_text) or
NotImplemented)

@abc.abstractmethod
def load_data_source(self, path: str, file_name: str):
"""Load in the data set"""
raise NotImplementedError

@abc.abstractmethod
def extract_text(self, full_file_path: str):
"""Extract text from the data set"""
raise NotImplementedError

class PdfParserNew(FormalParserInterface):
"""Extract text from a PDF."""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides FormalParserInterface.load_data_source()"""
pass

def extract_text(self, full_file_path: str) -> dict:
"""Overrides FormalParserInterface.extract_text()"""
pass

class EmlParserNew(FormalParserInterface):
"""Extract text from an email."""
def load_data_source(self, path: str, file_name: str) -> str:
"""Overrides FormalParserInterface.load_data_source()"""
pass

def extract_text_from_email(self, full_file_path: str) -> dict:
"""A method defined only in EmlParser.
Does not override FormalParserInterface.extract_text()
"""
pass

在上面的示例中,你最终创建了一个正式接口,当不重写抽象方法时,该接口将引发错误。 PdfParserNew实例pdf_parser不会引发任何错误,因为PdfParserNew正确覆盖了FormalParserInterface抽象方法。但是,EmlParserNew会引发错误:

1
2
3
4
5
6
>>> pdf_parser = PdfParserNew()
>>> eml_parser = EmlParserNew()
Traceback (most recent call last):
File "real_python_interfaces.py", line 53, in <module>
eml_interface = EmlParserNew()
TypeError: Can't instantiate abstract class EmlParserNew with abstract methods extract_text

如你所见,traceback消息告诉你尚未覆盖所有抽象方法。这是你在构建正式的Python接口时所希望的行为。

Interfaces in Other Languages

接口在许多编程语言中出现,并且它们的实现因语言而异。在接下来的文档中,你将会比较Python,Java,C++和Go的接口异同。

Java

与Python不同,Java包含一个interface关键字。与文件解析器示例相同,你可以使用Java声明一个接口,如下所示:

1
2
3
4
5
public interface FileParserInterface {
// Static fields, and abstract methods go here ...
public void loadDataSource();
public void extractText();
}

现在,你将创建两个具体的类,即PdfParserEmlParser,以实现FileParserInterface。为此,你必须在类定义中使用关键字implements,如下所示:

1
2
3
4
5
6
7
8
9
public class EmlParser implements FileParserInterface {
public void loadDataSource() {
// Code to load the data set
}

public void extractText() {
// Code to extract the text
}
}

继续你的文件解析示例,一个功能齐全的Java接口如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.*;
import java.io.*;

public class FileParser {
public static void main(String[] args) throws IOException {
// The main entry point
}

public interface FileParserInterface {
HashMap<String, ArrayList<String>> file_contents = null;

public void loadDataSource();
public void extractText();
}

public class PdfParser implements FileParserInterface {
public void loadDataSource() {
// Code to load the data set
}

public void extractText() {
// Code to extract the text
}

}

public class EmlParser implements FileParserInterface {
public void loadDataSource() {
// Code to load the data set
}

public void extractText() {
// Code to extract the text
}
}

}

如你所见,Python接口在创建过程中为你提供了比Java接口更大的灵活性。

C++

像Python一样,C++使用抽象基类来创建接口。在C++中定义接口时,使用关键字virtual来描述应在具体类中覆盖的方法:

1
2
3
4
5
6
class FileParserInterface {

public:
virtual void loadDataSource(std::string path, std::string file_name);
virtual void extractText(std::string full_file_name);
};

当你要实现该接口时,将给出具体的类名称,后跟冒号(:),然后是接口名称。以下示例演示了C++接口的实现:

1
2
3
4
5
6
7
8
9
10
11
class PdfParser : FileParserInterface {
public:
void loadDataSource(std::string path, std::string file_name);
void extractText(std::string full_file_name);
};

class EmlParser : FileParserInterface {
public:
void loadDataSource(std::string path, std::string file_name);
void extractText(std::string full_file_name);
};

Python接口和C++接口具有一些相似之处,因为它们都利用抽象基类来模拟接口。

Go

尽管Go的语法让人想起Python,但是Go编程语言包含一个interface关键字,更像Java。让我们在Go中创建fileParserInterface

1
2
3
4
type fileParserInterface interface {
loadDataSet(path string, filename string)
extractText(full_file_path string)
}

A big difference between Python and Go is that Go doesn’t have classes. Rather, Go is similar to C in that it uses the struct keyword to create structures. A structure is similar to a class in that a structure contains data and methods. However, unlike a class, all of the data and methods are publicly accessed. The concrete structs in Go will be used to implement the fileParserInterface.

Here’s an example of how Go uses interfaces:

Python和Go之间的最大区别是Go没有类。相反,Go与C相似,因为它使用了struct关键字来创建结构体。 结构体与类类似,因为结构包含数据和方法。但是,与类不同是,所有数据和方法都是可以公开访问的。 Go中的具体结构体将用于实现fileParserInterface

这是Go如何使用接口的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

type fileParserInterface interface {
loadDataSet(path string, filename string)
extractText(full_file_path string)
}

type pdfParser struct {
// Data goes here ...
}

type emlParser struct {
// Data goes here ...
}

func (p pdfParser) loadDataSet() {
// Method definition ...
}

func (p pdfParser) extractText() {
// Method definition ...
}

func (e emlParser) loadDataSet() {
// Method definition ...
}

func (e emlParser) extractText() {
// Method definition ...
}

func main() {
// Main entrypoint
}

与Python接口不同,Go接口是使用结构和显式关键字interface创建的。

Conclusion

创建接口时,Python提供了极大的灵活性。非正式的Python接口对于小型项目很有用,因为你不太可能对方法的返回类型感到困惑。随着项目的发展,对正式Python接口的需求变得越来越重要,因为推断返回类型变得更加困难。这确保了实现接口的具体类覆盖了抽象方法。

现在你可以:

  • 了解接口的工作方式以及创建Python接口的注意事项

  • 了解动态语言(例如Python)的接口的有用性

  • 在Python中实现正式和非正式接口

  • 比较Python接口与Java,C++和Go等语言的接口

既然你已经熟悉如何创建Python接口,请将Python接口添加到你的下一个项目中,以查看其实际作用!

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章