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

5.2 类和封装

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

Public vs Private

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

问题

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

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

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

Python 封装

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

私有属性

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

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

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

>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>

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

简单属性

考虑下面这个类:

class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price

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

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

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

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

这时候你会怎么做?

托管属性

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

class Stock:
def __init__(self, name, shares, price):
self.name = name
self.set_shares(shares)
self.price = price # Function that layers the "get" operation
def get_shares(self):
return self._shares # Function that layers the "set" operation
def set_shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected an int')
self._shares = value

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

特征属性(Properties)

方法二:

class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price @property
def shares(self):
return self._shares @shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value

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

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares # Triggers @property
50
>>> s.shares = 75 # Triggers @shares.setter
>>>

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

class Stock:
def __init__(self, name, shares, price):
...
# This assignment calls the setter below
self.shares = shares
... ...
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value

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

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

class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price @property
def cost(self):
return self.shares * self.price
...

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

>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares # Instance variable
100
>>> s.cost # Computed Value
49010.0
>>>

统一访问

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

>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() # Method
49010.0
>>> b = s.shares # Data attribute
100
>>>

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

装饰器语法

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

...
@property
def cost(self):
return self.shares * self.price

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

插槽属性(__slots__

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

class Stock:
__slots__ = ('name','_shares','price')
def __init__(self, name, shares, price):
self.name = name
...

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

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

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

关于封装的最终说明

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

练习

练习 5.6:简单特征属性

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

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>

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

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

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>

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

>>> s.cost()
... fails ...
>>>

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

练习 5.7:特征属性和 Setters

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

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

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

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

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... see what happens ...
>>>

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

>>> s.__dict__
... see what happens ...
>>>

应当指出, __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. MySQL 多实例及其主从复制

    目录 Mysql 实例 Mysql 多实例 创建多实例目录 编辑配置文件 初始化多实例数据目录 授权目录 启动多实例 连接多实例并验证 Mysql 多实例设置密码 设置密码后连接 Mysql 多实例主 ...

  2. vscode remote Development ssh targets 连接腾讯云

    配置 本地安装 插件 remote development 点击左侧,选择ssh 点击设置按钮,选择ssh配置文件 配置举例 ``` Host tencentcloud     HostName    ...

  3. Celery&Flower文档笔记

    1.Celery # tasks.py from celery import Celery app = Celery('tasks', broker='redis://localhost:6379', ...

  4. python阿里云api查询域名是否可以注册(CheckDomain)

    import requests from fun import * from urllib import parse url ='http://domain.aliyuncs.com/?' acces ...

  5. vue watch route params change

    vue watch route params change watch: { '$route.params.menuKey' (val, oldVal) { console.log('new rout ...

  6. Linux shell script All In One

    Linux shell script All In One refs xgqfrms 2012-2020 www.cnblogs.com 发布文章使用:只允许注册用户才可以访问!

  7. CSS ::marker All In One

    CSS ::marker All In One CSS pseudo element / CSS 伪元素 /* user agent stylesheet */ ::marker { unicode- ...

  8. js function call hacker

    js function call hacker you don't know javascript function https://developer.mozilla.org/en-US/docs/ ...

  9. taro ENV & NODE_ENV & process.env

    taro ENV & NODE_ENV & process.env https://github.com/NervJS/taro-ui/blob/dev/src/common/util ...

  10. 小程序 in action

    小程序 in action https://github.com/xgqfrms/xcx-taro taro https://taro-docs.jd.com/taro/docs/README.htm ...