Python的协议(Protocol): 为什么你需要他们

2023-01-12  本文已影响0人  进击的原点

Python 3.8有一个伟大但鲜为人知的新增功能是协议(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的优势:

上一篇下一篇

猜你喜欢

热点阅读