再探泛型 API,感受 Python 对象的设计哲学

简介: 再探泛型 API,感受 Python 对象的设计哲学

之前我们提到了泛型 API,这类 API 的特点是可以处理任意类型的对象,举个例子。

// 返回对象的长度
PyObject_Size
// 返回对象的某个属性
PyObject_GetAttr
// 返回对象的哈希值
PyObject_Hash
// 将对象转成字符串返回
PyObject_Str

对应到 Python 代码中,就是下面这个样子。

# PyObject_Size
print(len("古明地觉"))
print(len([1, 2, 3]))
"""
4
3
"""
# PyObject_GetAttr
print(getattr("古明地觉", "lower"))
print(getattr([1, 2, 3], "append"))
print(getattr({}, "update"))
"""
<built-in method lower of str object at 0x7f081aa7e920>
<built-in method append of list object at 0x7f081adc1100>
<built-in method update of dict object at 0x7f081aa8fd80>
"""
# PyObject_Hash
print(hash("古明地觉"))
print(hash(2.71))
print(hash(123))
"""
8152506393378233203
1637148536541722626
123
"""
# PyObject_Str
print(str("古明地觉"))
print(str(object()))
"""
古明地觉
<object object at 0x7fdfa0209d10>
"""

这些 API 能处理任意类型的对象,这究竟是怎么办到的?要想搞清楚这一点,还是要从 PyObject 入手。

我们知道对象在 C 看来就是一个结构体实例,并且结构体嵌套了 PyObject。

# 创建一个列表,让变量 var 指向它
var = [1, 2, 3]
# 创建一个浮点数,让变量 var 指向它
var = 2.71

列表对应的结构体是 PyListObject,浮点数对应的结构体是 PyFloatObject,变量 var 是指向对象的指针。那么问题来了,凭啥一个变量可以指向不同类型的对象呢?或者说变量和容器里面为什么可以保存不同对象的指针呢?

原因在前面的文章中解释的很详细了,因为对象的指针会统一转成 PyObject * 之后再交给变量保存,以创建列表为例。

28ca165158635afadfa8d905637f975e.png

当然创建浮点数也是同理,因此变量和容器里的元素本质上就是一个泛型指针 PyObject *。而对象的指针在交给变量保存的时候,也都会先转成 PyObject *,因为不管什么对象,它底层的结构体都嵌套了 PyObject。正是因为这个设计,变量才能指向任意的对象。

d637a8d5cf967f9a016cb288feb08e7d.png

所以 Python 变量相当于一个便利贴,可以贴在任意对象上。

不过问题来了,由于对象的指针会统一转成 PyObject * 之后再交给变量保存,那么变量怎么知道自己指向的是哪种类型的对象呢?相信你肯定知道答案:通过 ob_type 字段。

e90f85147fd637f437a423bdbf3b827d.png

对象对应的结构体可以有很多个字段,比如 PyListObject,但变量能看到的只有前两个字段。至于之后的字段是什么,则取决于对象的类型,总之对变量来说是不可见的,因为它是 PyObject *。

所以变量会先通过 ob_type 字段获取对象的类型,如果 ob_type 字段的值为 &PyList_Type,那么变量指向的就是 PyListObject。如果 ob_type 字段的值为 &PyFloat_Type,那么变量指向的就是 PyFloatObject,其它类型同理。

得到了对象的类型,那么再转成相应的指针即可,假设 ob_type 是 &PyList_Type,那么变量会再转成 PyListObject *,这样就可以操作列表的其它字段了。

所以我们再总结一下:

1852419b6c69e02e9d57f0309b7490b5.png

变量和容器里的元素只能保存相同的指针类型,而不同类型的对象,其底层的结构体是不同的。但这些结构体无一例外都嵌套了 PyObject,因此它们的指针会统一转成 PyObject * 之后再交给变量保存。

然后变量在操作对象时,会先通过 ob_type 判断对象的类型,假如是 &PyList_Type,那么会再转成 PyListObject *,其它类型同理。

d412cadc737d77d7adc65228951d6dba.png

相信你已经知道为什么泛型 API 可以处理任意类型的对象了,以 PyObject_GetAttr 为例,它内部会调用类型对象的 tp_getattro。

// 等价于 getattr(v, name)
PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{   
    // 获取对象 v 的类型对象
    PyTypeObject *tp = Py_TYPE(v);
    PyObject* result = NULL;
    // 如果类型对象实现了 tp_getattro,那么进行调用
    // 等价于 Python 中的 type(v).__getattr__(v, name)
    if (tp->tp_getattro != NULL) {
        result = (*tp->tp_getattro)(v, name);
    }
    // 否则会退化为 tp_getattr,它要求属性名称必须是 C 字符串
    // 不过 tp_getattr 已经废弃,应该使用 tp_getattro
    else if (tp->tp_getattr != NULL) {
        const char *name_str = PyUnicode_AsUTF8(name);
        if (name_str == NULL) {
            return NULL;
        }
        result = (*tp->tp_getattr)(v, (char *)name_str);
    }
    // 否则说明对象 v 没有指定属性
    else {
        PyErr_Format(PyExc_AttributeError,
                    "'%.100s' object has no attribute '%U'",
                    tp->tp_name, name);
    }
    return result;
}

函数先通过 ob_type 找到对象的类型,然后通过类型对象的 tp_getattro 调用对应的属性查找函数。所以 PyObject_GetAttr 会根据对象的类型,调用不同的属性查找函数,因此这就是泛型 API 能处理任意对象的秘密。

我们再以 Python 为例:

class A:
    def __getattr__(self, item):
        return f"class:A,item:{item}"
class B:
    def __getattr__(self, item):
        return f"class:B,item:{item}"
a = A()
b = B()
print(getattr(a, "some_attr"))
print(getattr(b, "some_attr"))
"""
class:A,item:some_attr
class:B,item:some_attr
"""
# 以上等价于
print(type(a).__getattr__(a, "some_attr"))
print(type(b).__getattr__(b, "some_attr"))
"""
class:A,item:some_attr
class:B,item:some_attr
"""

在 Python 里的表现和源码是一致的,我们再举个 iter 的例子:

data = [1, 2, 3]
print(iter(data))
print(type(data).__iter__(data))
"""
<list_iterator object at 0x7fb8200f29a0>
<list_iterator object at 0x7fb8200f29a0>
"""

如果一个对象支持迭代器操作,那么它的类型对象一定实现了 __iter__,通过 type(data) 可以获取到类型对象,然后将 data 作为参数调用 __iter__ 即可。

所以通过 ob_type 字段,这些泛型 API 实现了类似多态的效果,一个函数,多种实现。

相关文章
|
7天前
|
机器学习/深度学习 设计模式 API
Python 高级编程与实战:构建 RESTful API
本文深入探讨了使用 Python 构建 RESTful API 的方法,涵盖 Flask、Django REST Framework 和 FastAPI 三个主流框架。通过实战项目示例,详细讲解了如何处理 GET、POST 请求,并返回相应数据。学习这些技术将帮助你掌握构建高效、可靠的 Web API。
|
8天前
|
存储 监控 API
1688平台API接口实战:Python实现店铺全量商品数据抓取
本文介绍如何使用Python通过1688开放平台的API接口自动化抓取店铺所有商品数据。首先,开发者需在1688开放平台完成注册并获取App Key和App Secret,申请“商品信息查询”权限。接着,利用`alibaba.trade.product.search4trade`接口,构建请求参数、生成MD5签名,并通过分页机制获取全量商品数据。文中详细解析了响应结构、存储优化及常见问题处理方法,还提供了竞品监控、库存预警等应用场景示例和完整代码。
|
16天前
|
数据采集 供应链 API
实战指南:通过1688开放平台API获取商品详情数据(附Python代码及避坑指南)
1688作为国内最大的B2B供应链平台,其API为企业提供合法合规的JSON数据源,直接获取批发价、SKU库存等核心数据。相比爬虫方案,官方API避免了反爬严格、数据缺失和法律风险等问题。企业接入1688商品API需完成资质认证、创建应用、签名机制解析及调用接口四步。应用场景包括智能采购系统、供应商评估模型和跨境选品分析。提供高频问题解决方案及安全合规实践,确保数据安全与合法使用。立即访问1688开放平台,解锁B2B数据宝藏!
|
2月前
|
数据采集 供应链 API
Python爬虫与1688图片搜索API接口:深度解析与显著收益
在电子商务领域,数据是驱动业务决策的核心。阿里巴巴旗下的1688平台作为全球领先的B2B市场,提供了丰富的API接口,特别是图片搜索API(`item_search_img`),允许开发者通过上传图片搜索相似商品。本文介绍如何结合Python爬虫技术高效利用该接口,提升搜索效率和用户体验,助力企业实现自动化商品搜索、库存管理优化、竞品监控与定价策略调整等,显著提高运营效率和市场竞争力。
118 3
|
3月前
|
数据采集 JSON API
如何利用Python爬虫淘宝商品详情高级版(item_get_pro)API接口及返回值解析说明
本文介绍了如何利用Python爬虫技术调用淘宝商品详情高级版API接口(item_get_pro),获取商品的详细信息,包括标题、价格、销量等。文章涵盖了环境准备、API权限申请、请求构建和返回值解析等内容,强调了数据获取的合规性和安全性。
|
3月前
|
存储 数据处理 Python
Python如何显示对象的某个属性的所有值
本文介绍了如何在Python中使用`getattr`和`hasattr`函数来访问和检查对象的属性。通过这些工具,可以轻松遍历对象列表并提取特定属性的所有值,适用于数据处理和分析任务。示例包括获取对象列表中所有书籍的作者和检查动物对象的名称属性。
47 2
|
2月前
|
数据采集 JavaScript 前端开发
京东商品详情 API 接口指南(Python 篇)
本简介介绍如何使用Python抓取京东商品详情数据。首先,需搭建开发环境并安装必要的库(如requests、BeautifulSoup和lxml),了解京东反爬虫机制,确定商品ID获取方式。通过发送HTTP请求并解析HTML,可提取价格、优惠券、视频链接等信息。此方法适用于电商数据分析、竞品分析、购物助手及内容创作等场景,帮助用户做出更明智的购买决策,优化营销策略。
|
3月前
|
缓存 监控 算法
Python内存管理:掌握对象的生命周期与垃圾回收机制####
本文深入探讨了Python中的内存管理机制,特别是对象的生命周期和垃圾回收过程。通过理解引用计数、标记-清除及分代收集等核心概念,帮助开发者优化程序性能,避免内存泄漏。 ####
73 3
|
5月前
|
存储 XML API
探索后端开发中的RESTful API设计哲学
【10月更文挑战第21天】在数字化时代,后端开发是构建强大、可靠和可扩展应用程序的基石。本文将深入探讨RESTful API的设计原则,并展示如何通过这些原则来提升API的质量和性能。我们将从基础概念出发,逐步深入到实际案例分析,揭示高效API设计的秘诀。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用的技巧,帮助你在后端开发的道路上更进一步。
|
5月前
|
存储 缓存 Java
深度解密 Python 虚拟机的执行环境:栈帧对象
深度解密 Python 虚拟机的执行环境:栈帧对象
110 13

热门文章

最新文章