目录 | 上一节 (4.4 异常) | 下一节 (5.2 封装)

5.1 再谈字典

Python 对象系统主要基于字典实现。本节将对此进行讨论。

字典

字典是命名值(named values)的集合。

stock = {
'name' : 'GOOG',
'shares' : 100,
'price' : 490.1
}

虽然字典常用于简单的数据结构,但是字典也用于解释器的关键部分。字典可能是 Python 中最重要的数据类型

字典和模块

在模块内,字典存储所有的全局变量和函数。

# foo.py

x = 42
def bar():
... def spam():
...

可以通过 foo.__dict__globals() 查看该字典。

{
'x' : 42,
'bar' : <function bar>,
'spam' : <function spam>
}

字典和对象

用户定义对象的时候也使用到了实例字典和类字典。事实上,整个对象系统主要是基于字典实现的。

字典存储实例数据,如 __dict__

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG', 'shares' : 100, 'price': 490.1 }

当给 self 赋值的时候,你将填充该字典(和实例)。

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

实例数据 self.__dict__ 看起来像下面这样:

{
'name': 'GOOG',
'shares': 100,
'price': 490.1
}

每一个实例都拥有自己的私有字典。

s = Stock('GOOG', 100, 490.1)     # {'name' : 'GOOG','shares' : 100, 'price': 490.1 }
t = Stock('AAPL', 50, 123.45) # {'name' : 'AAPL','shares' : 50, 'price': 123.45 }

如果你创建了某个类的 100 个实例,那么就会有 100 个存储数据的字典。

类成员

一个单独的字典也存储方法:

class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price def cost(self):
return self.shares * self.price def sell(self, nshares):
self.shares -= nshares

使用 Stock.__dict__ 可以查看该字典:

{
'cost': <function>,
'sell': <function>,
'__init__': <function>
}

实例和类

实例和类是链接在一起的。实例通过 __class__ 属性指向类。

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>

实例字典存储的数据对每个实例而言是唯一的。但是,类字典存储的数据被该类的所有实例共享。

属性访问

使用对象时,可以通过 . 运算符访问数据和方法。

x = obj.name          # Getting
obj.name = value # Setting
del obj.name # Deleting

这些操作直接与字典绑定到一起。

修改实例

修改对象的操作会更新底层字典:

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.shares = 50 # Setting
>>> s.date = '6/7/2007' # Setting
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares # Deleting
>>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>

读取属性

假设你要读取实例上的属性:

x = obj.name

该属性可能位于两个地方:

  • 局部实例字典
  • 类字典

两种字典都会被检查到。首先,检查局部实例字典 __dict__。如果没有找到,通过 __class__ 查找类字典 __dict__

>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>

通过这样的查找模式,类成员被所有实例共享。

继承的工作原理

一个类可能继承自其它类:

class A(B, C):
...

在每个类中,父类存储在一个元组中:

>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>

子类通过 __bases__ 属性可以链接到父类。

多继承中的属性查找

从逻辑上讲,查找属性的过程如下:首先,检查局部字典 __dict__。如果没有找到,检查类字典 __dict__。如果在类中还是没有找到,通过 __bases__ 属性在父类中查找。这里面有一些小细节,我们接下来讨论。

单继承中的属性查找

在继承层级结构中,通过按顺序遍历继承树来找到属性。

class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass

在单继承中,因为到达上层父类的路径只有一条,所以当找到第一个匹配的属性时即可停止。

方法解析顺序(MRO)

Python 会预先计算继承链并将其存储到类的 MRO 属性中。你可以像这样查看:

>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
<class '__main__.B'>, <class '__main__.A'>,
<type 'object'>)
>>>

该继承链称为 方法解析顺序(Method Resolution Order)。为了找到属性,Python 按顺序遍历 MRO,第一个匹配的属性即是要找的属性。(译注:有关 MRO 的更多信息,请查看 https://www.python.org/download/releases/2.3/mro/)。

多继承中的方法解析顺序

使用多继承时,到达上层父类的路径有很多条,请看示例:

class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass

访问属性时会发生什么?

e = E()
e.attr

会执行属性查找,那么按什么顺序查找呢?这是个问题。

Python 使用的是 协作多重继承(cooperative multiple inheritance),协作多继承遵守的类排序规则如下:

  • 总是在检查父类之前检查子类
  • 父类(如果有多个)总是按照列出的顺序检查

根据该规则, 通过按层级结构对所有的类进行排序,然后计算出方法解析顺序。

>>> E.__mro__
(
<class 'E'>,
<class 'C'>,
<class 'A'>,
<class 'D'>,
<class 'B'>,
<class 'object'>)
>>>

底层算法称为“C3线性化算法(C3 Linearization Algorithm)”,确切的细节不重要,只要记住类层级结构遵守的排序规则与你家房子着火后必须撤离时遵守的规则相同:首先是孩子,其次是父母。

奇怪的代码重用(涉及多继承)

考虑以下两个完全不相关的对象:

class Dog:
def noise(self):
return 'Bark' def chase(self):
return 'Chasing!' class LoudDog(Dog):
def noise(self):
# Code commonality with LoudBike (below)
return super().noise().upper()

class Bike:
def noise(self):
return 'On Your Left' def pedal(self):
return 'Pedaling!' class LoudBike(Bike):
def noise(self):
# Code commonality with LoudDog (above)
return super().noise().upper()

LoudDog.noise() 方法和LoudBike.noise() 方法中有一些通用的代码。事实上,这些通用的代码是完全一样的。自然,这样的代码势必会吸引软件工程师。

"Mixin" 模式

Mixin 模式(pattern)是包含一部分代码片段的类。

class Loud:
def noise(self):
return super().noise().upper()

该类不能单独使用。通过继承和其它类混合使用。

class LoudDog(Loud, Dog):
pass class LoudBike(Loud, Bike):
pass

神奇的是,noise() 方法只实现了一次,却在两个完全不相关的类中使用。这种技巧是 Python 多继承的主要用途之一。

为什么使用 super()

当要覆盖一个方法的时候,总是使用 super() 函数。

class Loud:
def noise(self):
return super().noise().upper()

super() 函数代表 MRO 中的下一个类(译注:LoudDog 的 MRO 是 LoudDog>Loud>Dog>object。因为 Loud 的父类 object 没有定义 noise() 方法,所以 LoudDog 的实例在 Loud 中找不到 noise() 方法。然后 LoudDog 的实例就会到 MRO 中 Loud 的下一个类 Dog 中寻找)。

麻烦的是你不知道它是什么,尤其是使用多继承的时候。

注意事项

多继承是一种强大的机制。使用这种强大的机制时请牢记“权利越大,责任越大”。有时候,框架或者库使用多继承来实现一些高级特性,如组件组合。

练习

在第 4 节中,定义了一个表示股票持有信息的类 Stock。在本节练习中,我们将使用该类。请重新启动解释器并创建一些 Stock 类的实例:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm = Stock('IBM',50, 91.23)
>>>

练习 5.1:实例的表示

在交互式 shell 中,检查 googibm 两个实例的底层字典:

>>> goog.__dict__
... look at the output ...
>>> ibm.__dict__
... look at the output ...
>>>

练习 5.2:修改实例属性

尝试给上述其中一个实例添加新属性:

>>> goog.date = '6/11/2007'
>>> goog.__dict__
... look at output ...
>>> ibm.__dict__
... look at output ...
>>>

在上述输出中,你会发现 goog 实例具有 date 属性,但是 ibm 实例没有。重要的是要注意,Python 对实例属性确实没有任何限制。例如,实例属性不限于 __init__() 方法中设置的属性。

尝试直接添加一个新的值到 __dict__ 对象中:

>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>

在这里,你会发现一个事实,实例仅仅是字典顶部的一层。注意:应该强调的是,直接操作字典并不常见——你应该始终使用语法 (.) 编写代码。

练习 5.3:类的作用

类中的定义被类的所有实例所共享。所有的实例都有一个链接,指向它们的关联类:

>>> goog.__class__
... look at output ...
>>> ibm.__class__
... look at output ...
>>>

尝试在实例上调用方法:

>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>

名字 'cost' 既不在 goog.__dict__ 中定义,也不在 ibm.__dict__中定义。相反,而是由类字典提供的。请尝试以下代码:

>>> Stock.__dict__['cost']
... look at output ...
>>>

尝试直接通过字典调用 cost() 方法:

>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>

你是如何调用类中定义的函数,那么 self 就是怎么调用实例的。

尝试给 Stock 类添加新属性::

>>> Stock.foo = 42
>>>

该新属性会出现在所有实例中:

>>> goog.foo
42
>>> ibm.foo
42
>>>

但是,foo 并不属于实例字典:

>>> goog.__dict__
... look at output and notice there is no 'foo' attribute ...
>>>

你可以访问 foo 属性的原因是:当 Python 在实例字典中查找不到某个属性时,那么它就会到类字典中查找。

注意:本部分主要阐明什么是类变量。假设你有这样一个类:

class Foo(object):
a = 13 # Class variable
def __init__(self,b):
self.b = b # Instance variable

在 Foo 类中,因为变量 a 在类体(body of the class)中被赋值,所以 a 是“类变量(class variable)”。变量 a 可以被 Foo 类的所有实例所共享。示例:

>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a # Inspect the class variable (same for both instances)
13
>>> g.a
13
>>> f.b # Inspect the instance variable (differs)
10
>>> g.b
20
>>> Foo.a = 42 # Change the value of the class variable
>>> f.a
42
>>> g.a
42
>>>

练习 5.4:绑定方法

Python 有一个微妙的特性:调用方法实际上涉及两个步骤以及一个称为绑定方法的东西。示例:

>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>

实际上,绑定方法包含调用一个方法的所需的所有内容。例如,它们记录了实现方法的函数:

>>> s.__func__
<function sell at 0x10049af50>
>>>

这与在 Stock 字典中找到的值是一样的:

>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>

绑定方法还记录实例,即 self

>>> s.__self__
Stock('GOOG',75,490.1)
>>>

你可以使用 () 一起调用所有的函数。例如,调用 s(25) 实际是这样做的:

>>> s.__func__(s.__self__, 25)    # Same as s(25)
>>> goog.shares
50
>>>

练习 5.5:继承

创建一个继承自 Stock 的类:

>>> class NewStock(Stock):
def yow(self):
print('Yow!') >>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>

通过扩展属性的搜索过程来实现继承。__bases__ 属性是一个包含直接父类的元组:

>>> NewStock.__bases__
(<class 'stock.Stock'>,)
>>>

__mro__ 属性是一个包含所有父类的元组,父类按查找顺序排列。

>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class 'stock.Stock'>, <class 'object'>)
>>>

实例 n 是这样找到 cost() 方法的:

>>> for cls in n.__class__.__mro__:
if 'cost' in cls.__dict__:
break >>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>

目录 | 上一节 (4.4 异常) | 下一节 (5.2 封装)

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

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

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

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

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

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

  3. 翻译:《实用的Python编程》05_02_Classes_encapsulation

    目录 | 上一节 (5.1 再谈字典) | 下一节 (6 生成器) 5.2 类和封装 创建类时,通常会尝试将类的内部细节进行封装.本节介绍 Python 编程中有关封装的习惯用法(包括私有变量和私有属 ...

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

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

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

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

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

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

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

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

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

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

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

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

随机推荐

  1. zoj-3870 (二进制)

    For an upcoming programming contest, Edward, the headmaster of Marjar University, is forming a two-m ...

  2. Leetcode(868)-二进制间距

    给定一个正整数 N,找到并返回 N 的二进制表示中两个连续的 1 之间的最长距离. 如果没有两个连续的 1,返回 0 . 示例 1: 输入:22 输出:2 解释: 22 的二进制是 0b10110 . ...

  3. Swift private(set) All In One

    Swift private(set) All In One SwiftUI Getters and Setters https://docs.swift.org/swift-book/Language ...

  4. Smashing Conf 2020

    Smashing Conf 2020 https://smashingconf.com/online-workshops/ events https://smashingconf.com/ny-202 ...

  5. LeetCode & tree & binary tree

    LeetCode & tree & binary tree 树 & 二叉树 refs https://leetcode.com/problemset/all/?topicSlu ...

  6. macOS & timer & stop watch

    macOS & timer & stop watch https://matthewpalmer.net/blog/2018/09/28/top-free-countdown-time ...

  7. http cache & 浏览器缓存,存储位置的优先级,条件?

    http cache & 浏览器缓存,存储位置的优先级,条件? memory cache disk cache 浏览器缓存,存储位置的优先级,条件, 机制,原理是什么? from memory ...

  8. websockets & auto close & bug & solution

    websockets & auto close & bug & solution WS 连接总是被关闭 ??? refs https://wdd.js.org/websocke ...

  9. 安装 Angular Material UI

    文档 调色板 安装 ng add @angular/material ? Choose a prebuilt theme name, or "custom" for a custo ...

  10. NGK公链脱颖而出,成为值得期待的项目!

    当下2020年是动荡的一年,全世界经济危机汲汲可危,在这个特殊的时刻,有人抱怨说这是最坏的年代,也有人庆幸说这是最好的年代,历史不会重演,但总是惊人的相似,首先带你回顾一下上一次金融危机出现的2008 ...