Python的协议(Protocol): 为什么你需要他们
Python 3.8有一个伟大但鲜为人知的新增功能是协议(Protocol),或静态鸭子类型。那是什么,它有什么用?
为了让你更好地了解协议的适用范围以及它们为何有用,我将首先讨论以下主题:
- 动态 vs. 静态类型(Dynamic vs Static typing)
- 类型提示 (Type hints)
- 抽象基类(ABCs)
- 协议 (Protocol)
动态 vs.静态类型
动态类型 (Dynamic typing)
我们常说python是一种动态类型语言,这代表的是什么意思?
首先,python的类型声明不是必须的,下面这个函数的传入参数和返回值我们都可以不用定义类型:
def my_function(a,b,c):
return a+b-c
其次:类型是在运行时处理和检查的。my_function可以使用整数、浮点数或两者的混合作为输入来运行。返回类型取决于输入:
result = my_function(5, 3, 2)
# type(result) -> int
result = my_function(5.1, 3, 2)
# type(result) -> float
静态类型(Static typing)
而相比较于C语言,我们不得不定义类型声明:
int my_function(int a, int b, int c)
{
return a+b-c;
}
int result = my_function(5.1, 3, 2);
如果声明的类型不对就会报错。 这个就是静态类型语言的好处。在编译期间类型就会被检查,所以在运行时你不会收到任何的错误消息。而python在运行时间里你就会得到在静态语言里得不到的那些错误。
鸭子类型
鸭子类型(duck typing)是编程语言中动态类型的一种设计风格,一个对象的特征不是由父类决定,而是通过对象的方法决定的。
If it walks like a duck and it quacks like a duck, then it must be a duck.
(如果它走路像鸭子,叫起来像鸭子,那么它一定是鸭子。)
例如,假设我们有一个名为 的类Duck,这个类可以走路(walk)和嘎嘎叫(quack):
class Duck:
def walk(self):
...
def quack(self):
...
我们可以创建Duck的实例并让他走路和嘎嘎:
duck = Duck()
duck.walk()
duck.quack()
现在,如果我们有一个Donkey可以走路但不能嘎嘎叫的类:
class Donkey:
def walk(self):
...
然后,如果我们尝试制作Donkey走路和嘎嘎的实例:
duck = Donkey()
duck.walk()
duck.quack()
我们会得到一个>> AttributeError: 'Donkey' object has no attribute 'quack’. 。请注意,我们只在运行时得到它!
但是,我们可以用任何其他可以走路和嘎嘎的类来代替Duck类。例如:
class ImpostorDuck:
def walk(self):
...
def quack(self):
not_quite_quacking()
类型提示(Type hints)
Python 3.5 中引入了类型提示或可选的静态类型来克服这一缺点。它允许您有选择地指定参数类型和返回值,然后可以通过静态类型检查器(例如mypy )进行检查。当然python得IDE(pycharm)也会有类似检查提醒。
例如,假设我们有一个Duck类然后定义会eat_bread和swim的方法:
class Duck:
def eat_bread(self):
...
def swim(self):
...
然后我们可以定义一个feed_bread的函数。我们可以将输入参数的类型指定为Duck:
def feed_the_duck(duck: Duck):
duck.eat_bread()
duck = Duck()
feed_the_duck(duck)
例如,现在尝试feed_the_duck函数里传递monkey的实例:
class Monkey:
def eat_bananas(self):
...
def climb_tree(self):
...
monkey = Monkey()
feed_the_duck(monkey)
在运行时,这会给你>> AttributeError: 'Monkey' object has no attribute 'eat_bread’
但是 mypy 可以在您运行代码之前发现此类问题。在这种情况下,它会告诉您:
error: Argument 1 to "feed_the_duck" has incompatible type "Monkey"; expected "Duck"
这些类型提示可以让您作为开发人员的生活更轻松,但它们并不完美。例如,如果我们想要feed_bread更通用以便它也可以接受其他类型的动物,我们需要明确列出所有接受的类型:
from typing import Union
class Pig:
def eat_bread(self):
pass
def feed_bread(animal: Union[Duck, Pig]):
animal.eat_bread()
但是它存在一个缺点:如果其他类也有eat_bread方法,你不能将它用于自己的类型。比如以下定义了一个Mees类,并且也包含eat_bread方法:
class Mees:
def eat_bread(self):
pass
mees = Mees()
feed_bread(mees)
在运行时,上面的代码可以很好地工作,但是pycharm会有提示:
image.png
所以,总结一下:类型提示很棒,因为它们使您可以选择进行静态类型检查。仍然没有添加类型声明的义务,但如果您这样做,您将获得静态类型语言的一些好处。但是无法适应导入代码的类型提示导致 Python 的动态类型特性与静态类型提示之间存在冲突。
抽象基类(ABCs)
抽象基类(Abstract base class)消除了上述冲突的一些痛苦。顾名思义,它们是基类——您应该从中继承的类——但它们不能被实例化。它们用于定义 ABC 的子类应该是什么样子的接口。
例如:
class Animal(metaclass=ABCMeta):
@abstractmethod
def walk(self):
pass # Needs implementation by subclass
实例化此类是不可能的:>>TypeError: Can't instantiate abstract class Animal with abstract methods walk
但是,如果我们定义一个子类,我们可以实例化它:
class Duck(Animal):
def walk(self):
...
duck = Duck()
assert isinstance(duck, Animal) # <-- True
举一个更实际的例子,可以创建一个名为 EatsBread抽象基类并且定义它的抽象方法eat_bread。以下我们单独建一个名为animals的py文件并写入如下:
from abc import ABCMeta, abstractmethod
class EatsBread(metaclass=ABCMeta):
@abstractmethod
def eat_bread(self):
pass
class Duck(EatsBread):
def eat_bread(self):
...
class Pig(EatsBread):
def eat_bread(self):
...
def feed_bread(animal: EatsBread):
animal.eat_bread()
现在,如果我在我的代码中使用这个实现Mees——我可以创建Mees子类,EatsBread一切都会好起来的:
from animals import EatsBread, feed_bread
class Mees(EatsBread):
def eat_bread(self):
...
def drink_milk(self):
...
feed_bread(Mees()) # <-- OK at runtime and for mypy
虽然这样好多了——但这仍然不完美。通常基类不容易公开,这意味着我必须进行丑陋的导入才能获得我需要的东西:
from animals import feed_bread,EatsBread
此外,你必须从基类继承或显式地将你的类注册为子类,例如EatsBread.register(Mees)才能使其工作——这不如鸭子类型的隐式行为好。
再次总结:抽象基类为您的类型提供结构这很棒。这意味着类型提示不需要为新的子类更新。但是我们可能仍然会遇到合并来自多个包的类的问题。所有这些类型——无论是继承还是作为虚拟子类——都需要显式发生,这与动态类型的隐式转换相冲突。
Protocols(协议)
这就是协议的用武之地。协议是 ABC 的一种特殊情况,它隐含地工作:
from typing import Protocol
class EatsBread(Protocol):
def eat_bread(self):
pass
def feed_bread(animal: EatsBread):
animal.eat_bread()
class Duck:
def eat_bread(self):
...
feed_bread(Duck()) # <-- OK
在上面的代码中,Duck隐含地被认为是EatsBread. 无需显式继承协议。任何实现协议中定义的所有属性和方法(具有匹配签名)的类都被视为该协议的子类型。
因此,如果我们要使用feed_bread package 中的函数animals:
from animals import feed_bread
class Mees:
def eat_bread(self):
...
def drink_milk(self):
...
feed_bread(Mees()) # <-- OK
这里Mees也是隐含的子类型EatsBread。同样,无需明确指定:只要签名匹配,它就可以工作!这就是Protocol也被称为静态鸭子类型的原因。
但是请注意,所有这些仅在类型检查时有效,在运行时无效!如果这里用isinstance函数检测Duck是否是EatsBread类型时,得到的结果是False。
from typing import Protocol
class EatsBread(Protocol):
def eat_bread(self):
pass
class Duck:
def eat_bread(self):
...
isinstance(Duck(),EatsBread) # <-- False
如果你确实希望它在运行时工作,你可以使用runtime_checkable的装饰器。如下所示:
from typing import Protocol
@runtime_checkable
class EatsBread(Protocol):
def eat_bread(self):
pass
class Duck:
def eat_bread(self):
...
isinstance(Duck(),EatsBread) # <-- True
最后一次总结, Python的Protocol的优势:
- 无需显式继承协议或将您的类注册为虚拟子类。
- 组合包不再困难:只要签名匹配,它就可以工作。
- 我们现在拥有两全其美:动态类型的静态类型检查。