跳至主要内容

Python 封装:全面指南

学习在 Python 面向对象编程中实现封装的基础知识。
· 11分钟 阅读

使用 AI 探索

封装是 Python 中面向对象的基本原则之一。它可以保护您的类不被意外更改或删除,并促进代码的可复用性与可维护性。来看一个简单的类定义:

class Smartphone:
   def __init__(self, brand, os):
       self.brand = brand
       self.os = os

iphone = Smartphone("Apple", "iOS 17")

许多 Python 程序员都会这样定义类。然而,这与专业 Python 开发者所遵循的最佳实践相去甚远。这个类的问题在您尝试修改其数据时就显现出来了:

iphone.os = "Android"
print(iphone.os)
Android

想象一下,一台 iPhone 跑着 Android——太离谱了!显然,我们需要在类的内部设置一些边界,使用户不能随心所欲地更改其属性。

如果他们确实要更改,那么必须在我们的规则之下进行。但我们仍希望 .os 或 .brand 这些属性在表面上保持不变。

以上这些目标都可以通过封装来实现,在本教程中,我们将全面学习它。让我们开始吧!

Python 如何实现封装?

Python 倾向于通过约定与编程实践来支持封装,而非强制性的访问修饰符。您看,Python 很多代码设计背后的基本理念是“大家都是成年人”。我们只能将封装作为一种约定来实现,并期望其他 Python 开发者信任并尊重我们的代码。

在 Java、C++ 等其他 OOP 语言中,封装通过 public、private 或 protected 等访问修饰符被严格强制执行,但 Python 并没有这些:

特性PythonJavaC++
访问修饰符无强制修饰符;使用命名约定(_protected、__private)通过 public、protected、private 关键字强制执行通过 public、protected、private 关键字强制执行
Getter/Setter 方法可选,常与 @property 装饰器配合用于受控访问常见做法,通常以方法形式实现常见做法,通常以方法形式实现
数据访问通过命名约定访问;依赖开发者自律由访问修饰符控制;由编译器强制由访问修饰符控制;由编译器强制
理念“大家都是成年人”——依赖约定与信任严格强制访问控制严格强制访问控制

因此,我接下来将展示的封装技术大多(如果不是全部)都是 Python 的约定。若您愿意,它们很容易被打破。但我相信您会在自己的开发项目中尊重并遵循它们。

Python 中的访问修饰符

假设我们有这样一个简单的类:

class Tree:
   def __init__(self, height):
       self.height = height

pine = Tree(20)
print(pine.height)
20

它只有一个可打印的 height 属性。问题在于我们也可以把它改成任意值:

pine.height = 50
pine.height
50
pine.height = "Grandma"
pine.height
'Grandma'

那么,如何告知用户禁止更改 height 呢?我们可以通过在前面加一个下划线把它变成受保护成员:

class Tree:
   def __init__(self, height):
       self._height = height

pine = Tree(20)
pine._height
20

现在,了解此约定的人会知道他们只能访问该属性,而我们强烈不鼓励他们使用和修改它。但如果他们愿意,仍然可以修改,没错。

那我们如何也阻止这一点?用另一种约定——通过在前面加双下划线,将属性变成私有成员:

class Tree:
   def __init__(self, height):
       self.__height = height


pine = Tree(20)
pine.__height
AttributeError: 'Tree' object has no attribute '__height'

现在,如果有人尝试访问该属性,Python 会抛出错误,更别提修改它了。

但您注意到我们刚刚做了什么吗?我们把与对象相关的唯一信息对用户隐藏了。我们的类变得毫无用处,因为它没有任何公共属性。

那么,如何既向用户公开树高,又能控制其访问和修改方式?例如,我们希望树高在特定范围内,且只能是整数。如何强制执行呢?

这时,使用 Java 的朋友可能会建议使用getter 和 setter 方法。那我们先试试:

class Tree:
   def __init__(self, height):
       self.__height = height

   def get_height(self):
       return self.__height

   def set_height(self, new_height):
       if not isinstance(new_height, int):
           raise TypeError("Tree height must be an integer")
       if 0 < new_height <= 40:
           self.__height = new_height
       else:
           raise ValueError("Invalid height for a pine tree")


pine = Tree(20)
pine.get_height()
20

这样,您创建了私有属性 __height,但允许用户通过 get_height 和 set_height 方法以受控方式访问和修改它。

pine.set_height(25)

pine.get_height()
25

在设置新值之前,set_height 会确保新高度在某个范围内且为数值。

pine.set_height("Password")
TypeError: Tree height must be an integer

但这些方法对于一个简单操作来说似乎有些小题大做。而且,像这样写代码也不够优雅:

# Increase height by 5
pine.set_height(pine.get_height() + 5)

如果我们能这样写:

pine.height += 5

同时仍能强制高度的数据类型与范围正确,不是更优雅、更易读吗?答案是肯定的,我们将在下一节学习如何做到这一点。

在 Python 类中使用 @property 装饰器

我们引入一种新技巧——为属性创建属性(properties):

class Tree:
   def __init__(self, height):
       # First, create a private or protected attribute
       self.__height = height

   @property
   def height(self):
       return self.__height

pine = Tree(17)
pine.height
17

我们希望用户能像访问普通属性 height 那样访问隐藏的 __height。为此,我们定义一个名为 height 的方法,返回 self.__height,并用 @property 装饰它。

现在,我们可以调用 height 并访问该私有属性:

pine.height
17

但最妙的是,用户无法修改它:

pine.height = 15
AttributeError: can't set attribute 'height'

因此,我们再添加一个名为 height(self, new_height) 的方法,并用 height.setter 装饰器包装。在该方法内,我们实现强制高度数据类型与范围的逻辑:

class Tree:
   def __init__(self, height):
       self.__height = height

   @property
   def height(self):
       return self.__height

   @height.setter
   def height(self, new_height):
       if not isinstance(new_height, int):
           raise TypeError("Tree height must be an integer")
       if 0 < new_height <= 40:
           self.__height = new_height
       else:
           raise ValueError("Invalid height for a pine tree")

现在,当用户尝试修改 height 属性时,会调用 @height.setter,从而确保传入的值正确:

pine = Tree(10)

pine.height = 33  # Calling @height.setter
pine.height = 45  # An error is raised
ValueError: Invalid height for a pine tree

我们还可以通过 @height.getter 自定义通过点号访问 height 属性的方式:

class Tree:
   def __init__(self, height):
       self.__height = height

   @property
   def height(self):
       return self.__height

   @height.getter
   def height(self):
       # You can return a custom version of height
       return f"This tree is {self.__height} meters"


pine = Tree(33)

pine.height
'This tree is 33 meters'

即使我们用整数高度创建了 pine,也可以通过 @height.getter 修改返回值的形式。

以上就是在 Python 类中促进封装的一些示例。请记住,封装仍然是一种约定,因为我们依然可以破坏内部的 __height 私有成员:

pine._Tree__height = "Gotcha!"

pine.height
'This tree is Gotcha! meters'

Python 类中的一切都是公开的,私有方法也是如此。这并非设计缺陷,而是“大家都是成年人”理念的一种体现。

下面是我们刚刚回顾过的 Python 访问“修饰符”概览:

修饰符/约定说明示例
Public默认访问级别;属性与方法可从类外访问self.attribute
Protected以下划线开头表示;用于指示部分受限访问的约定self._attribute
Private以下双下划线开头表示;名称改写提供有限的访问限制self.__attribute
Properties使用 @property decorator 为私有属性提供受控访问@property def attribute(self):
只读属性无 @attribute.setter;允许只读访问属性@property def attribute(self):

实现封装的最佳实践

您可以遵循以下若干最佳实践,以确保您的代码与其他经验丰富的 OOP 开发者的代码保持良好一致:

  1. 如果某些属性或方法仅供您使用,请创建受保护或私有属性/方法。受保护或私有成员通常会被排除在文档之外,并向他人发出信号:作为开发者,您可以不经通知就更改它们,从而不鼓励他人使用。
  2. 不必为每个类属性都创建属性(property)。对于具有大量属性的大型类,编写 attr.getter 和 attr.setter 方法会让人头大。
  3. 考虑在用户访问受保护成员(_)时发出警告。
  4. 谨慎使用私有成员,因为它们可能会让不熟悉该约定的人难以阅读代码。
  5. 优先保证清晰而非晦涩。由于封装旨在提升代码可维护性和数据保护,不要完全隐藏类的重要实现细节。
  6. 如果您想创建只读属性,不要实现 @attr.setter 方法。用户将能够访问该属性,但无法修改。
  7. 始终记住封装是一种约定,而非 Python 语法强制执行的内容。
  8. 对于简单的类,考虑使用数据类(dataclasses),它们可以用一行代码启用类的封装。不过,数据类适用于属性与方法可预期的简单类。参阅这篇数据类教程了解更多。

结语与延伸资源

在本教程中,我们学习了 Python 面向对象编程的核心支柱之一:封装。

封装允许您为类对象内部存储的数据定义受控访问方式。这使您能够编写干净、可读且高效的代码,并防止类数据被意外更改或删除。

以下是一些相关资源,帮助您深化 OOP 知识:

FAQs

在 Python 中,封装与抽象有何不同?

封装指的是将数据与对其进行操作的方法打包在一个单元(如类)中,并限制对某些组件的访问。相对地,抽象是隐藏复杂实现,只暴露关键部分。封装侧重于限制访问,抽象侧重于简化交互。

封装能用于函数式编程吗,还是只属于 OOP?

封装与面向对象编程(OOP)紧密相关,通常不用于函数式编程。函数式编程强调不可变性与无状态函数,而 OOP 则关注对象以及将数据封装在对象内部。

在 Python 中实现封装时,应避免哪些常见陷阱?

常见陷阱包括过度使用私有变量,导致代码难以维护;未提供足够的访问方法(getter/setter),使代码脆弱。此外,开发者可能会忘记 Python 的封装是基于约定的,从而可能被绕过。

封装如何提升代码安全性?

封装通过限制对对象内部状态的访问并防止未授权修改来提升代码安全性。通过控制数据的访问与修改方式,开发者可以确保数据保持有效且一致。

在 Python 中使用 @property 装饰器对性能有何影响?

使用 @property 装饰器会引入轻微的开销,因为多了一次方法调用,但在多数情况下影响可以忽略。改进代码可读性与可维护性的收益通常远大于这点性能成本。

能否在 Python 中像 Java 或 C++ 那样严格强制封装?

Python 不会像 Java 或 C++ 那样严格强制封装。其理念基于约定,并假定开发者会尊重私有和受保护成员的预期用法。不过,使用以下划线为前缀的命名约定可以在一定程度上抑制误用。

封装在 Python 项目中的真实应用有哪些?

封装常用于保护应用中的敏感数据,如银行软件会隐藏账户细节。它也用于库与框架中,以隐藏复杂实现细节,让开发者只需与简洁的接口交互。

我该如何高效测试 Python 中的封装代码?

测试封装代码时,应针对类的公共接口编写单元测试。通过测试对用户暴露的方法与属性,您可以确保内部状态变更得到正确管理,并验证封装逻辑的有效性。

封装能应用于 Python 模块吗,还是仅限于类?

虽然封装主要与类相关,但这一概念也可扩展到模块:可通过以下划线为前缀的函数或变量来表示模块内的私有用法。

封装与 Python 中的多态有何关系?

封装与多态同为 OOP 的原则,但目的不同。封装限制对象部分内容的访问;多态则允许将对象视为其父类的实例,便于扩展与维护代码。二者可结合使用,构建灵活且安全的代码结构。

Bexruz (Bex) Tuychiev

作者

Bexruz (Bex) Tuychiev

我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。 

主题