Python如何提供可维护的方式在系统中传递数据结构?

6 投票
3 回答
1848 浏览
提问于 2025-04-17 22:04

我刚接触动态语言,发现像Python这样的语言更喜欢使用简单的数据结构,比如字典,来在系统的不同部分之间传递数据(比如在函数、模块之间)。

在C#的世界里,当系统的两个部分需要沟通时,开发者会定义一个类(可能是实现了某个接口的类),这个类里包含一些属性(比如一个Person类有名字、出生日期等属性)。发送数据的部分会创建这个类的实例,并给属性赋值。接收数据的部分则可以访问这些属性。这个类被称为DTO(数据传输对象),它是“定义明确”的和显式的。如果我从DTO的类中删除了一个属性,编译器会立刻警告我所有使用这个DTO的代码部分,告诉我它们在尝试访问一个现在不存在的属性。这样我就能清楚地知道我的代码哪里出错了。

而在Python中,产生数据的函数(发送者)通过构建字典并返回它们来创建隐式的DTO。作为一个来自编译语言世界的人,这让我感到害怕。我立刻想到,如果在一个大型代码库中,一个产生字典的函数的某个键名被改了(或者某个键被完全删除),那么就会出现很多潜在的KeyError错误,因为代码库中依赖这个字典并期待某个键的部分将无法访问它们原本期待的数据。如果没有单元测试,开发者就没有可靠的方法来知道这些错误会出现在什么地方。

也许我完全误解了。字典在传递数据时是最佳实践工具吗?如果是的话,开发者是如何解决这种问题的?隐式数据结构和使用它们的函数是如何维护的?我该如何减少对这种看似巨大不确定性的恐惧呢?

3 个回答

1

我觉得传递字典并不是在系统各个部分之间传递结构化数据的唯一方法。我见过很多人使用类来实现这个功能。实际上,namedtuple 也是一个很好的选择。

如果没有单元测试,开发者就无法可靠地知道这些错误会出现在什么地方。

那么,为什么你不写单元测试呢?

在Python中,你不能依赖编译器来捕捉你的错误。如果你真的需要对代码进行静态检查,可以使用一些静态分析工具(可以参考这个问题)。

2

在用Python做大型项目时,自动化测试是必不可少的。否则,你就不敢进行任何大规模的代码重构,代码库很快就会变得一团糟,因为你每次修改都不敢动太多,结果只能做一些糟糕的解决方案(因为你会太害怕去实现正确的解决方案)。

其实,这种情况在C++或者大型项目中混合使用多种语言时也同样存在。

举个例子,就在几个小时之前,我为了给一个特定客户修复一个四行的bug(其中一行只是一个大括号)而创建了一个分支。因为主干代码和他在生产环境中使用的版本差距太大,而负责发布流程的人告诉我,他的使用场景在当前版本中还没有经过手动测试,所以我不能升级他的安装。

编译器可以告诉你一些信息,但如果软件很复杂,它无法保证在重构后代码的稳定性。认为只要某段代码能编译就一定是正确的,这种想法是错误的(可能只有在hello_world.cpp这种简单的情况下例外)。

另外,通常情况下,你不会在Python中对所有东西都使用字典,除非你真的很在意动态特性(但在这种情况下,代码不会用字面量作为字典的键)。如果你的Python代码中有很多像d["foo"]这样的写法,而不是d[k],那么我会说这可能是设计上有问题的信号。

5

我来自一个编译型语言的世界,这让我有点害怕。我立刻想到一个大项目的场景:如果一个生成字典的函数的某个键的名字被改了(或者某个键完全被删除了),那么就会出现很多潜在的键错误(KeyError),因为代码中的其他部分依赖这个字典,期待能找到这个键的数据,但实际上却找不到了。

我想特别强调你问题中的这一部分,因为我觉得这是你想要理解的主要点。

Python的开发理念有点不同;在Python中,对象可以在不报错的情况下改变(比如,你可以给实例添加属性,而不需要在类中声明它们)。在Python中,一个常见的编程习惯是EAFP

EAFP

宁愿请求原谅,也不要请求许可。这种常见的Python编码风格假设有效的键或属性是存在的,如果这个假设不成立,就捕获异常。这个干净快速的风格的特点是有很多的try和except语句。这个方法与许多其他语言(如C语言)常用的LBYL风格形成对比。

上面提到的LBYL是“先看后跳”的意思:

LBYL

先看后跳。这种编码风格在进行调用或查找之前,明确测试前提条件。这种风格与EAFP方法形成对比,特点是有很多if语句。

在多线程环境中,LBYL方法可能会引入“查看”和“跳跃”之间的竞争条件。例如,代码if key in mapping: return mapping[key]可能会失败,因为在测试之后、查找之前,另一个线程可能已经从映射中删除了这个键。这个问题可以通过使用锁或者采用EAFP方法来解决。

所以我想说,这在Python中是比较常见的,你期待对象会表现良好,并且能够优雅地处理自己(主要是通过抛出很多异常)。传统的“对象隐藏”和“接口契约”并不是Python的核心理念。这就像学习其他任何东西一样,你需要适应这个编程环境及其规则。

你问题的另一部分:

字典是否是传递数据的最佳实践工具?如果是,开发者是如何解决这类问题的?

这里的答案是取决于你的问题领域。如果你的问题领域不适合自定义对象,那么你可以传递任何类型的容器(列表、元组、字典)。但是,如果你需要传递的“丰富”数据只有对象,那么你的代码就会充满那些不定义行为而只定义属性的类。

哦,顺便说一下,获取键并引发KeyError的问题已经解决了,因为Python字典有一个get方法,当某个键不存在时,它可以返回一个默认值(默认返回哨兵对象None):

>>> d = {'a': 'b'}
>>> d['b']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'b'
>>> d.get('b')  # None is returned, which is the only
                # object that is not printed by the
                # Python interactive interpreter.
>>> d.get('b','default')
'default'

撰写回答