Python 面向协议编程
GenAI LIVE! | 2025年6月4日
Python 对象🐍
在 Python 中一切都是object
。
一个对象由以下部分组成:
- 由 内部标识的内存位置
id
。 - 确定对象协议的类型(类) 。
- 对象持有的一个值(或一组值)。
例子:
# Given an object identified by the variable name `a`
>>> a = 1
# We can access its memory location `id`
>>> id(a)
1407854654624
# We can access its type
>>> type(a)
'<class 'int'>'
# We can access its value
>>> a
1
# We can use the behavior defined on its protocol
>>> a + 1
2
>>> a.hex()
'0x1'
上述内容对于Python 中的每个对象都是类似的。
类型🔠
每个对象都基于一种类型,一种类型就是一种class
定义。
print
<- 是function
类型"Hello"
<- 是str
类型0
<- 是int
类型[0:5]
<- 是一种slice
类型str.upper
<- 是一种method_descriptor
类型
Function String Integer Slice
__⬆️__ __⬆️_ _⬆️_ __⬆️__
print("Hello"[0] + "World!"[0:5].upper())
___⬆️___ __⬆️___
Symbol Method Descriptor
协议📝
Python 是一种POP语言(面向协议编程)
对象的类型决定了它的实现,它暴露了行为,行为是对象可以做的事情或者可以用对象做的事情。
有些语言称之为对象的特征。
每组能力就是我们所说的协议,协议对于在对象之间设置契约很有用。
识别Hello World程序上的协议:
Callable Subscriptable Sliceable
__⬆️__ ______⬆️ __________⬆️
print("Hello"[0] + "World!"[0:5].upper())
________⬆️________ __⬆️__
Summable Callable
-
可调用🗣️可以使用()调用
当类型的协议包含方法时,该类型也是可调用的__call__
。 -
可订阅✍🏻 其元素可以通过订阅访问。
订阅可以是数字序数[0]
或命名键['name']
。当类型的协议包含该__getitem__
方法时,该类型即为可订阅类型。 -
可切片🔪 其元素集合可以被切片。
当一个类型可下标且其__getitem__
方法可以接受 而不是或 时,该类型即为可切片slice
类型。切片是元素的组合。index
name
[start:stop:step]
-
可求和➕ 可以通过
+
运算与其他对象组合。
组合后的乘积始终是新对象。对于数值类型,这指的是将sum
两个或多个数字合并成一个集合的能力。对于序列,这指的是将concatenation
其片段合并成一个。当类型的协议包含__add__
或__radd__
方法时,该类型即为可求和类型。 -
可打印🖨️可以使用
print
所有 Python 对象进行打印,这些对象print
都是可打印的,将查找__repr__
或__str__
用于打印对象的方法。
ℹ️ 还有更多,事实上,你可以定义自定义协议,协议非常通用,虽然在打字模块中有一些预定义的协议,但没有官方的协议列表。
from typing import Iterator, Iterable, Optional, Sequence, Awaitable, Generic
完整的协议和子类型列表可在https://mypy.readthedocs.io/en/stable/protocols.html上找到
🦆 协议支持一种称为“鸭子类型”的方法,即在Python中,如果一个对象看起来像鸭子、行为像鸭子并且具有鸭子的行为,那么它就被称为鸭子,不管这是否是一只学会了模仿鸭子嘎嘎叫的狗的情况🐶。
打字和协议检查
一些协议可以使用内置函数检查
callable(print) is True
callable("Hello") is False
某些协议必须根据其类型类进行检查
isinstance("Hello", str) is True
isinstance(0, slice) is False
在某些情况下,验证协议的唯一方法
是检查其属性。
hasattr("Hello", "__add__") is True # Summable, we can use `+` operator.
其他地方我们需要使用EAFP模式。
try:
"Hello" + 1
except TypeError: # Strong type checking
# we cannot `__add__` an `str` to an `int`
打字协议
Python3 提供了一种定义自定义协议的方法
from typing import Protocol, runtime_checkable
@runtime_checkable
class CallableSummableSubscriptable(Protocol):
def __call__(self) -> T:
...
def __add__(self, other: T) -> T:
...
def __getitem__(self, item: T) -> T:
...
ℹ️ 协议方法只是具有空体的签名,由 表示
...
。T
通常是表示泛型类型的类型别名。
协议对于定义契约、函数签名的界限很有用,例如定义一个只有当类型具有指定协议时才接受参数的函数。
def awesome_function(thing: CallableSummableSubscriptable):
# accepts only objects that implements that ⬆️ protocol.
# Protocols can be checked at runtime @runtime_checkable
# Or checked using static analysers e.g: mypy
Python 中有传统的 OOP 吗?
OOP Python 实际上是POP(面向协议编程)
- 有关协议和行为的更多信息。
- 较少涉及传统的 OOP 概念。
所有传统的概念和模式也都可用,但有些是对象和协议所固有的,最终程序员不必以同样的方式去处理它们。
遗产
从另一个基类型继承并采用其所有行为的能力以及使用自定义实现覆盖的能力。
class MyString(str)
def __str__(self):
return super().__str__().upper()
>>> x = MyString("Bruno")
>>> print(x)
"BRUNO"
封装
能够隐藏对象属性和方法并仅公开其中选定的一组或以更可控的方式公开它们。
# Descriptor is a protocol for getter/setter like approach.
class Field:
def __get__(...):
def __set__(...):
class Thing:
# Double underline means that the field is private
# but actually it is only a naming mangling convention.
__protected_attr = Field()
# Properties can also be used to define getter/setter
@property
def foo(self):
return self.__protected_attr
@foo.setter
def set_foo(self, value):
self.__protected_attr = value
多态性
无论对象的基本类型如何,对象都能够表现出不同的行为,并且过程可以将不同类型的对象作为参数。
len("Bruno")
len([1, 2, 3])
dict.get("key")
dict.get("key", default="other")
print("Hello")
print(123)
print(*["Hello", "World"])
def function(*args, **kwargs):
...
ℹ️ 在传统的 OOP 文献中,多态性通常仅用于定义重用相同名称但不同实现的方法的能力,但实际上它比这更深入。
结论
🧑🍳 厨师在为菜谱选择食材时,会查看每种食材上定义的协议,厨师可以去超市看到一组不同类型的洋葱🧅,即使菜谱上只说洋葱,厨师也会根据所需的行为知道需要特定类型的洋葱,白洋葱更甜,更适合煮熟的食谱,紫洋葱更酸,因此更适合沙拉和生食食谱。(但这也取决于口味)
🧑💻 软件工程师在选择算法中使用的数据结构时🤖必须查看定义的协议以及对象的行为,即使要求说它是文本的集合,例如,工程师必须分析程序以明智地在元组,列表甚至集合之间进行选择。
协议通常比类型更重要。
鏂囩珷鏉ユ簮锛�https://dev.to/rochacbruno/python-protocol-orient-programming-1m0g注:感谢@mathsppblog 启发了本文的第一段https://twitter.com/mathsppblog/status/1445148609977126914