目录 | 上一节 (5.1 再谈字典) | 下一节 (6 生成器)

5.2 类和封装

创建类时,通常会尝试将类的内部细节进行封装。本节介绍 Python 编程中有关封装的习惯用法(包括私有变量和私有属性)。

Public vs Private

虽然类的主要作用之一是封装对象的属性和内部实现细节。但是,类还定义了外界用来操作该对象的公有接口(public interface)。实现细节与公有接口之间的区别很重要。

问题

在 Python 中,几乎所有与类和对象有关的东西都是开放(open)的。

  • 可以轻松地查看对象的内部细节。
  • 可以随意地修改。
  • 没有访问控制的概念(例如:私有类成员)。

如何隔离内部实现的细节,这是一个问题。

Python 封装

Python 依赖编程约定来指示某些东西的用途。这就约定基于命名。有一种普遍的态度是,程序员应该遵守规则,而不是让语言来强制执行规则。

私有属性

以下划线 _ 开头的任何属性被认为是私有的(private)。

  1. class Person(object):
  2. def __init__(self, name):
  3. self._name = 0

如前所述,这这是一种编程风格。你仍然可以对这些私有属性进行访问和修改。

  1. >>> p = Person('Guido')
  2. >>> p._name
  3. 'Guido'
  4. >>> p._name = 'Dave'
  5. >>>

一般来说,一个以下划线 _ 开头的名称被认为是内部实现,无论该名称是变量名、函数名还是模块名。如果你发现自己直接使用这些名称,那么你可能在做一些错误的事情。你应该寻找更高级的功能。

简单属性

考虑下面这个类:

  1. class Stock:
  2. def __init__(self, name, shares, price):
  3. self.name = name
  4. self.shares = shares
  5. self.price = price

这里有一个让人惊讶的特性,你可以给属性设置任何值:

  1. >>> s = Stock('IBM', 50, 91.1)
  2. >>> s.shares = 100
  3. >>> s.shares = "hundred"
  4. >>> s.shares = [1, 0, 0]
  5. >>>

你可能会想要对此进行检查(译注:例如 shares 表示的是股份数目,值应该是整数。所以给 shares 赋值时应该对值进行检查。如果检查发现给 shares 赋的值不是整数,那么应该触发一个 TypeError 异常):

  1. s.shares = '50' # Raise a TypeError, this is a string

这时候你会怎么做?

托管属性

方法一:引进访问方法(accessor methods)。

  1. class Stock:
  2. def __init__(self, name, shares, price):
  3. self.name = name
  4. self.set_shares(shares)
  5. self.price = price
  6. # Function that layers the "get" operation
  7. def get_shares(self):
  8. return self._shares
  9. # Function that layers the "set" operation
  10. def set_shares(self, value):
  11. if not isinstance(value, int):
  12. raise TypeError('Expected an int')
  13. self._shares = value

糟糕的是,这破坏了我们的已有代码。例如:之前是通过 s.shares = 50shares 赋值的,那么现在就要改成s.set_shares(50)shares 赋值,这很不好。

特征属性(Properties)

方法二:

  1. class Stock:
  2. def __init__(self, name, shares, price):
  3. self.name = name
  4. self.shares = shares
  5. self.price = price
  6. @property
  7. def shares(self):
  8. return self._shares
  9. @shares.setter
  10. def shares(self, value):
  11. if not isinstance(value, int):
  12. raise TypeError('Expected int')
  13. self._shares = value

现在,普通属性(normal attribute)的访问触发了 @property@shares.setter 下的 getter 方法和 setter 方法。

  1. >>> s = Stock('IBM', 50, 91.1)
  2. >>> s.shares # Triggers @property
  3. 50
  4. >>> s.shares = 75 # Triggers @shares.setter
  5. >>>

使用该方法,不需要对源代码做任何修改。在类内(包括在 __init__() 方法内)有赋值的时候,直接调用新的 setter:

  1. class Stock:
  2. def __init__(self, name, shares, price):
  3. ...
  4. # This assignment calls the setter below
  5. self.shares = shares
  6. ...
  7. ...
  8. @shares.setter
  9. def shares(self, value):
  10. if not isinstance(value, int):
  11. raise TypeError('Expected int')
  12. self._shares = value

特征属性和私有名称( private names)的使用之间经常会出现混淆。尽管特征属性内部使用的是私有名称,如 _shares。类的其它地方(不是特征属性),仍可以继续使用诸如 shares 这样的名称。

特征属性对于计算数据属性也非常有用。

  1. class Stock:
  2. def __init__(self, name, shares, price):
  3. self.name = name
  4. self.shares = shares
  5. self.price = price
  6. @property
  7. def cost(self):
  8. return self.shares * self.price
  9. ...

这允许你删除 cost 后面的括号,隐藏 cost 是一个方法的事实:

  1. >>> s = Stock('GOOG', 100, 490.1)
  2. >>> s.shares # Instance variable
  3. 100
  4. >>> s.cost # Computed Value
  5. 49010.0
  6. >>>

统一访问

最后一个例子展示了如何在对象上放置一个更加统一的接口。如果不这样做,对象使用起来可能会令人困惑。

  1. >>> s = Stock('GOOG', 100, 490.1)
  2. >>> a = s.cost() # Method
  3. 49010.0
  4. >>> b = s.shares # Data attribute
  5. 100
  6. >>>

为什么 cost 后面需要加上括号 (),但是 shares 却不需要? 特征属性可以解决这个问题。

装饰器语法

@ 语法称为“装饰(decoration)”。它指定了一个修饰符(modifier),应用于紧接其后的函数定义:

  1. ...
  2. @property
  3. def cost(self):
  4. return self.shares * self.price

更多细节在 第 7 节 中给到。

插槽属性(__slots__

你可以使用 __slots__ 限制属性名称集:

  1. class Stock:
  2. __slots__ = ('name','_shares','price')
  3. def __init__(self, name, shares, price):
  4. self.name = name
  5. ...

使用其它属性时,将会触发错误:

  1. >>> s.price = 385.15
  2. >>> s.prices = 410.2
  3. Traceback (most recent call last):
  4. File "<stdin>", line 1, in ?
  5. AttributeError: 'Stock' object has no attribute 'prices'

管这样可以防止错误和限制对象的使用,但实际上使用 __slots__ 是为了提高性能,提高 Python 利用内存的效率。

关于封装的最终说明

不要滥用私有属性(private attributes),特征属性(properties),插槽属性(slots)等。它们有特殊的用途,你在阅读其它 Python 代码时可能会看到。但是,对于大多数日常编码而言,它们不是必需的。

练习

练习 5.6:简单特征属性

使用特征属性是一种非常有用的给对象添加“计算属性”的方式。虽然你在 stock.py 文件中创建了 Stock 对象,但是请注意,在 Stock 对象上 ,对于不同类型的属性,获取方式稍微有点不同。

  1. >>> from stock import Stock
  2. >>> s = Stock('GOOG', 100, 490.1)
  3. >>> s.shares
  4. 100
  5. >>> s.price
  6. 490.1
  7. >>> s.cost()
  8. 49010.0
  9. >>>

具体来说,cost 后面之所以要添加括号,是因为 cost 是一个方法。

如果你想去掉 cost() 的括号,那么可以把该方法转为一个特征属性。请修改 Stock 类,使其像下面这样计算所持有股票的总价:

  1. >>> ================================ RESTART ================================
  2. >>> from stock import Stock
  3. >>> s = Stock('GOOG', 100, 490.1)
  4. >>> s.cost
  5. 49010.0
  6. >>>

尝试将 cost作为方法调用(s.cost()),你会发现,现在已经被定义为特征属性的 cost 无法作为方法被调用。

  1. >>> s.cost()
  2. ... fails ...
  3. >>>

这些更改很可能会破坏你之前的 pcost.py 程序,所以,你可能需要返回到 pcost.py 中去掉 cost() 方法后面的括号()

练习 5.7:特征属性和 Setters

请修改 shares 属性,以便将该值存储在私有属性中,并且使用属性函数(property functions)确保赋给 shares 的值总是整数。预期行为示例:

  1. >>> ================================ RESTART ================================
  2. >>> from stock import Stock
  3. >>> s = Stock('GOOG',100,490.10)
  4. >>> s.shares = 50
  5. >>> s.shares = 'a lot'
  6. Traceback (most recent call last):
  7. File "<stdin>", line 1, in <module>
  8. TypeError: expected an integer
  9. >>>

练习 5.8:添加插槽属性(slots)

请修改 Stock 类,以便 Stock 类拥有一个 __slots__ 属性。然后确认无法添加新属性:

  1. >>> ================================ RESTART ================================
  2. >>> from stock import Stock
  3. >>> s = Stock('GOOG', 100, 490.10)
  4. >>> s.name
  5. 'GOOG'
  6. >>> s.blah = 42
  7. ... see what happens ...
  8. >>>

使用 __slots__ 时,Python 使用更高效的对象内部表示。如果你尝试查看实例 s 的底层字典会发生什么?

  1. >>> s.__dict__
  2. ... see what happens ...
  3. >>>

应当指出, __slots__ 作为数据结构是类中最常用的一种优化。使用插槽属性使程序占用更少的内存,运行更快。但是,在其它大多数类中,你应该尽可能避免使用 __slots__

目录 | 上一节 (5.1 再谈字典) | 下一节 (6 生成器)

注:完整翻译见 https://github.com/codists/practical-python-zh

翻译:《实用的Python编程》05_02_Classes_encapsulation的更多相关文章

  1. 翻译:《实用的Python编程》InstructorNotes

    实用的 Python 编程--讲师说明 作者:戴维·比兹利(David Beazley) 概述 对于如何使用我的课程"实用的 Python 编程"进行教学的问题,本文档提供一些通用 ...

  2. 翻译:《实用的Python编程》README

    欢迎光临 大约 25 年前,当我第一次学习 Python 时,发现 Python 竟然可以被高效地应用到各种混乱的工作项目上,我立即被震惊了.15 年前,我自己也将这种乐趣教授给别人.教学的结果就是本 ...

  3. 翻译:《实用的Python编程》04_02_Inheritance

    目录 | 上一节 (4.1 类) | 下一节 (4.3 特殊方法) 4.2 继承 继承(inheritance)是编写可扩展程序程序的常用手段.本节对继承的思想(idea)进行探讨. 简介 继承用于特 ...

  4. 翻译:《实用的Python编程》01_02_Hello_world

    目录 | 上一节 (1.1 Python) | 下一节 (1.3 数字) 1.2 第一个程序 本节讨论有关如何创建一个程序.运行解释器和调试的基础知识. 运行 Python Python 程序始终在解 ...

  5. 翻译:《实用的Python编程》03_03_Error_checking

    目录 | 上一节 (3.2 深入函数) | 下一节 (3.4 模块) 3.3 错误检查 虽然前面已经介绍了异常,但本节补充一些有关错误检查和异常处理的其它细节. 程序是如何运行失败的 Python 不 ...

  6. 翻译:《实用的Python编程》03_04_Modules

    目录 | 上一节 (3.3 错误检查) | 下一节 (3.5 主模块) 3.4 模块 本节介绍模块的概念以及如何使用跨多个文件的函数. 模块和导入 任何一个 Python 源文件都是一个模块. # f ...

  7. 翻译:《实用的Python编程》03_05_Main_module

    目录 | 上一节 (3.4 模块) | 下一节 (3.6 设计讨论) 3.5 主模块 本节介绍主程序(主模块)的概念 主函数 在许多编程语言中,存在一个主函数或者主方法的概念. // c / c++ ...

  8. 翻译:《实用的Python编程》04_01_Class

    目录 | 上一节 (3.6 设计讨论) | 下一节 (4.2 继承) 4.1 类 本节介绍 class 语句以及创建新对象的方式. 面向对象编程(OOP) 面向对象编程是一种将代码组织成对象集合的编程 ...

  9. 翻译:《实用的Python编程》05_00_Overview

    目录 | 上一节 (4 类和对象) | 下一节 (6 生成器) 5. Python 对象的内部工作原理 本节介绍 Python 对象的内部工作原理.来自其它语言的程序员通常会发现 Python 的类概 ...

随机推荐

  1. redis键过期时间

    redis服务器中每个数据库都是一个redisDb,而redisDb实质上是一个字典的模型,数据库的每一个键都是一个字典的键值,对数据库的增删改查也就是对字典对象的增删改查. redis在维护带有过期 ...

  2. C++ part5

    为啥大三了课少了一点点,做作业的时间反而多了一大堆堆??? 关于protect 只能被本类或者子类的成员函数,或者友元函数访问. 友元函数: #include <iostream> #in ...

  3. apt 和 apt-get 之间有什么区别?

    使用ubuntu的朋友一定会接触一个命令就是apt-get . 使用该工具安装各种应用程序那叫一个爽. 在 Ubuntu 16.04 发行后,apt使用渐渐频繁起来. 那么,apt-get 与 apt ...

  4. 记一次FreeRTOS错误配置导致无法进入临界区

    最近项目用到FreeRTOS,在实际调试中发现我自己的一段代码本来好用的(在无RTOS的情况下),但是当我在带RTOS的情况下把代码放到一个单独的任务中运行时我发现本来好用的代码莫名其妙的出现问题,有 ...

  5. Graphviz - Graph Visualization Software 开源可视化绘图工具(visio 类)

    http://www.graphviz.org/Download_windows.php Welcome to Graphviz Available translations:  Romanian,  ...

  6. DLL & Dynamic-link library

    DLL & Dynamic-link library 动态链接库 .dll 动态链接库(英语:Dynamic-link library,缩写为 DLL)是微软公司在微软视窗操作系统中实现共享函 ...

  7. text to JSON

    text to JSON GeoLocaltion API https://www.cnblogs.com/xgqfrms/p/13283680.html https://repl.it/@xgqfr ...

  8. js animation & requestAnimationFrame

    js animation & requestAnimationFrame https://developer.mozilla.org/en-US/docs/Web/API/window/req ...

  9. js & regex & var & highlight

    js & regex & var & highlight let key = `ali`.toLocaleUpperCase(); let name = "阿里云计算 ...

  10. MacBook Pro 关闭触控板

    MacBook Pro 关闭触控板 https://support.apple.com/zh-cn/HT204895 https://support.apple.com/zh-cn/HT203171 ...