《Python语言及其应用》读书笔记(5)之第6章对象和类(关键词:对象/类)
2017-09-17 14:39
375 查看
第6章对象和类
6.1 什么是对象
对象既包含数据(变量,更习惯称之为特性, attribute),也包含代码(函数,也称为方法)。它是某一类具体事物的特殊实例。6.2 使用class定义类
>>> class Person(): ... def __init__(self, name): ... self.name = name ... >>>
现在,用 Person 类创建一个对象,为 name 特性传递一个字符串参数:
>>> hunter = Person('Elmer Fudd')
上面这短短的一行代码实际做了以下工作:
查看 Person 类的定义;
在内存中实例化(创建)一个新的对象;
调用对象的 init方法,将这个新创建的对象作为 self传入,并将另一个参数(’ElmerFudd’)作为 name 传入;
将 name 的值存入对象;
返回这个新的对象;
将名字 hunter 与这个对象关联。
在类的定义中,
__init__并不是必需的。只有当需要区分由该类创建的不同对象时,才需要指定
__init__方法。
6.3 继承
利用类的继承:从已有类中衍生出新的类, 添加或修改部分功能。这是代码复用的一个绝佳的例子。 使用继承得到的新类会自动获得旧类中的所有方法,而不需要进行任何复制。你只需要在新类里面定义自己额外需要的方法, 或者按照需求对继承的方法进行修改即可。修改得到的新方法会覆盖原有的方法。 我们习惯将原始的类称为父类、 超类或基类,将新的类称作孩子类、 子类或衍生类。这些术语在面向对象的编程中不加以区分。
>>> class Car(): ... def exclaim(self): ... print("I'm a Car!") ... >>> class Yugo(Car): ... pass ...
最后,为每一个类各创建一个对象,并调用刚刚声明的 exclaim 方法:
>>> give_me_a_car = Car() >>> give_me_a_yugo = Yugo() >>> give_me_a_car.exclaim() I'm a Car! >>> give_me_a_yugo.exclaim() I'm a Car!
我们不需要进行任何特殊的操作, Yugo 就自动从 Car 那里继承了exclaim() 方法。但事实上,我们并不希望 Yugo 在 exlaim() 方法里宣称它是一个 Car,这可能会造成身份危机(无法区分 Car 和 Yugo)。让我们来看看怎么解决这个问题。
6.4 覆盖方法
就像上面的例子展示的一样,新创建的子类会自动继承父类的所有信息。接下来将看到子类如何替代——更习惯说覆盖(override) ——父类的方法。在子类中,可以覆盖任何父类的方法,包括
__init__()。下面的例子使用了之前创建过的 Person 类。我们来创建两个子类,分别代表医生(MDPerson)和律师(JDPerson):
>>> class Person(): ... def __init__(self, name): ... self.name = name ... >>> class MDPerson(Person): ... def __init__(self, name): ... self.name = "Doctor " + name ... >>> class JDPerson(Person): ... def __init__(self, name): ... self.name = name + ", Esquire" ...
在上面的例子中,子类的初始化方法 init() 接收的参数和父类 Person 一样,但存储到对象内部 name 特性的值却不尽相同:
>>> person = Person('Fudd') >>> doctor = MDPerson('Fudd') >>> lawyer = JDPerson('Fudd')112 | 第 6 章 >>> print(person.name) Fudd >>> print(doctor.name) Doctor Fudd >>> print(lawyer.name) Fudd, Esquire
6.5 添加新方法
子类还可以添加父类中没有的方法。6.6 使用super方法从父类得到帮助
我们已经知道如何在子类中覆盖父类的方法,但如果想要调用父类的方法怎么办?使用super()。
下面的例子将定义一个新的类EmailPerson,用于表示有电子邮箱的 Person。首先,来定义熟悉的 Person 类:
>>> class Person(): ... def __init__(self, name): ... self.name = name ...
下面是子类的定义。注意,子类的初始化方法
__init__()中添加了一个额外的 email参数:
>>> class EmailPerson(Person): ... def __init__(self, name, email): ... super().__init__(name) ... self.email = email
在子类中定义
__init__()方法时,父类的
__init__()方法会被覆盖。因此,在子类中,父类的初始化方法并不会被自动调用, 我们必须显式调用它。以上代码实际上做了这样几件事情。
通过 super() 方法获取了父类 Person 的定义。
子类的
__init__()调用了
Person.__init__()方法。它会自动将 self 参数传递给父类。因此,你只需传入其余参数即可。在上面的例子中, Person() 能接受的其余参数指的是name。
self.email = email 这行新的代码才真正起到了将 EmailPerson 与 Person 区分开的作用。接下来,创建一个 EmailPerson 类的对象:
>>> bob = EmailPerson('Bob Frapples', 'bob@frapples.com')
我们既可以访问 name 特性,也可以访问 email 特性:
>>> bob.name 'Bob Frapples' >>> bob.email 'bob@frapples.com'
为什么不像下面这样定义 EmailPerson 类呢?
>>> class EmailPerson(Person): ... def __init__(self, name, email): ... self.name = name ... self.email = email
确实可以这么做,但这有悖我们使用继承的初衷。我们应该使用 super() 来让 Person 完成它应该做的事情, 就像任何一个单纯的 Person 对象一样。除此之外,不这么写还有另一个好处:如果 Person 类的定义在未来发生改变,使用 super() 可以保证这些改变会自动反映
到 EmailPerson 类上,而不需要手动修改。子类可以按照自己的方式处理问题, 但如果仍需要借助父类的帮助,使用 super() 是最佳的选择(就像现实生活中孩子与父母的关系一样)。
6.7 self的自辩
Python 中经常被争议的一点就是必须把 self 设置为实例方法(前面例子中你见到的所有方法都是实例方法)的第一个参数。 Python 使用self 参数来找到正确的对象所包含的特性和方法。 通过下面的例子,我会告诉你调用对象方法背后 Python 实际做的工作。还记得前面例子中的 Car 类吗?再次调用 exclaim() 方法:
>>> car = Car() >>> car.exclaim() I'm a Car!
Python 在背后做了以下两件事情:
- 查找 car 对象所属的类(Car);
- 把 car 对象作为 self 参数传给 Car 类所包含的 exclaim() 方法。
- 了解调用机制后, 为了好玩,我们甚至可以像下面这样进行调用,这与普通的调用语法car.exclaim()效果完全一致:
>>> Car.exclaim(car) I'm a Car!
6.8 使用属性对特性进行访问和设置
Python 不需要 getter 和 setter 方法,因为 Python 里所有特性都是公开的,使用时全凭自觉。如果你不放心直接访问对象的特性, 可以为对象编写 setter 和 getter方法。但更具 Python风格的解决方案是使用属性(property) 5。
下面的例子中, 首先定义一个 Duck 类,它仅包含一个 hidden_name 特性。(下一节会告诉你命名私有特性的一种更好的方式。)我们不希望别人能够直接访问这个特性,因此需要定义两个方法: getter 方法(get_name())和 setter 方法(set_name())。我们在每个方法中
都添加一个 print() 函数,这样就能方便地知道它们何时被调用。最后,把这些方法设置为 name 属性:
>>> class Duck(): ... def __init__(self, input_name): ... self.hidden_name = input_name ... def get_name(self): ... print('inside the getter') ... return self.hidden_name ... def set_name(self, input_name): ... print('inside the setter') ... self.hidden_name = input_name ... name = property(get_name, set_name)
这两个新方法在最后一行之前都与普通的 getter 和 setter 方法没有任何区别,而最后一行则把这两个方法定义为了 name 属性。 property() 的第一个参数是 getter 方法,第二个参数是 setter 方法。现在,当你尝试访问 Duck 类对象的 name 特性时, get_name() 会被自动调用:
>>> fowl = Duck('Howard') >>> fowl.name inside the getter 'Howard'
当然,也可以显式调用 get_name() 方法,它就像普通的 getter 方法一样:
>>> fowl.get_name() inside the getter 'Howard'
当对 name 特性执行赋值操作时, set_name() 方法会被调用:
>>> fowl.name = 'Daffy' inside the setter >>> fowl.name inside the getter 'Daffy'
也可以显式调用 set_name() 方法:
>>> fowl.set_name('Daffy') inside the setter >>> fowl.name inside the getter 'Daffy'
另一种定义属性的方式是使用修饰符(decorator)。下一个例子会定义两个不同的方法,它们都叫 name(),但包含不同的修饰符:
- @property,用于指示 getter 方法;
- @name.setter,用于指示 setter 方法。
实际代码如下所示:
>>> class Duck(): ... def __init__(self, input_name): ... self.hidden_name = input_name ... @property ... def name(self): ... print('inside the getter') ... return self.hidden_name ... @name.setter ... def name(self, input_name): ... print('inside the setter') ... self.hidden_name = input_name
你仍然可以像之前访问特性一样访问 name,但这里没有了显式的 get_name() 和 set_name()方法:
>>> fowl = Duck('Howard') >>> fowl.name inside the getter 'Howard'
>>> fowl.name = 'Donald'
inside the setter
>>> fowl.name
inside the getter
'Donald'
在前面几个例子中,我们都使用 name 属性指向类中存储的某一特性(在我们的例子中是hidden_name)。除此之外,属性还可以指向一个计算结果值。我们来定义一个 Circle 类,它包含 radius 特性以及一个计算属性 diameter:
>>> class Circle(): ... def __init__(self, radius): ... self.radius = radius ... @property ... def diameter(self): ... return 2 * self.radius ...
创建一个 Circle 对象,并给 radius 赋予一个初值:
>>> c = Circle(5) >>> c.radius 5
可以像访问特性(例如 radius)一样访问属性 diameter:
>>> c.diameter 10
真正有趣的还在后面。我们可以随时改变 radius 特性的值,计算属性 diameter 会自动根据新的值更新自己:
>>> c.radius = 7 >>> c.diameter 14
如果你没有指定某一特性的 setter 属性(@diameter.setter),那么将无法从类的外部对它的值进行设置。这对于那些只读的特性非常有用:
>>> c.diameter = 20 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: can't set attribute
与直接访问特性相比,使用 property 还有一个巨大的优势:如果你改变了某个特性的定义,只需要在类定义里修改相关代码即可,不需要在每一处调用修改。
6.9 使用名称重整保护私有特性
前面的 Duck 例子中,为了隐藏内部特性,我们曾将其命名为hidden_name。其实, Python对那些需要刻意隐藏在类内部的特性有自己的命名规范:由连续的两个下划线开头(__)。我们来把 hidden_name 改名为 __name,如下所示:
>>> class Duck(): ... def __init__(self, input_name): ... self.__name = input_name ... @property ... def name(self): ... print('inside the getter') ... return self.__name ... @name.setter ... def name(self, input_name): ... print('inside the setter') ... self.__name = input_name ...
看看代码是否还能正常工作:
>>> fowl = Duck('Howard') >>> fowl.name inside the getter 'Howard'
>>> fowl.name = 'Donald'
inside the setter
>>> fowl.name
inside the getter
'Donald'
看起来不错!现在,你无法在外部访问 __name 特性了:
>>> fowl.__name Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'Duck' object has no attribute '__name'
这种命名规范本质上并没有把特性变成私有,但 Python 确实将它的名字重整了,让外部的代码无法使用。 如果你实在好奇名称重整是怎么实现的,我可以偷偷地告诉你其中的奥秘,但不要告诉别人哦:
>>> fowl._Duck__name 'Donald'
发现了吗?我们并没有得到 inside the getter,成功绕过了 getter 方法。尽管如我们所见,这种保护特性的方式并不完美, 但它确实能在一定程度上避免我们无意或有意地对特性进行直接访问。
注:如前面例子中,假如我们需要把特性 hidden_name 的名字改成 in_class_name。不设置属性(property)的话,我们需要在每一处访问 hidden_name 的地方将它替换成 in_class_name;而设置了属性的话,仅需在类的内部修改,其余部分的访问仍直接通过属性 name 即可。——译者注
6.10 方法的类型
有些数据(特性)和函数(方法)是类本身的一部分,还有一些是由类创建的实例的一部分。在类的定义中, 以 self 作为第一个参数的方法都是实例方法instance method)。它们在创建自定义类时最常用。 实例方法的首个参数是 self,当它被调用时, Python 会把调用该方法的对象作为 self 参数传入。
与之相对, 类方法(class method)会作用于整个类,对类作出的任何改变会对它的所有实例对象产生影响。 在类定义内部,用前缀修饰符 @classmethod 指定的方法都是类方法。与实例方法类似, 类方法的第一个参数是类本身。在 Python 中,这个参数常被写作 cls,因为全称 class 是保留字,在这里我们无法使用。下面的例子中,我们为类 A 定义一个类方法来记录一共有多少个类 A 的对象被创建:
>>> class A(): ... count = 0 ... def __init__(self): ... A.count += 1 ... def exclaim(self): ... print("I'm an A!") ... @classmethod ... def kids(cls): ... print("A has", cls.count, "little objects.") ... >>> >>> easy_a = A() >>> breezy_a = A() >>> wheezy_a = A() >>> A.kids() A has 3 little objects.
注意,上面的代码中,我们使用的是 A.count(类特性),而不是self.count(可能是对象的特性)。在 kids() 方法中,我们使用的是 cls.count,它与 A.count 的作用一样。
类定义中的方法还存在着第三种类型, 它既不会影响类也不会影响类的对象。它们出现在类的定义中仅仅是为了方便, 否则它们只能孤零零地出现在代码的其他地方,这会影响代码的逻辑性。 这种类型的方法被称作静态方法(static method),用 @staticmethod 修饰,它既不需要 self 参数也不需要 class 参数。下面例子中的静态方法是一则 CoyoteWeapon的广告:
>>> class CoyoteWeapon(): ... @staticmethod ... def commercial(): ... print('This CoyoteWeapon has been brought to you by Acme') ... >>> >>> CoyoteWeapon.commercial() This CoyoteWeapon has been brought to you by Acme
注意,在这个例子中,我们甚至都不用创建任何 CoyoteWeapon 类的对象就可以调用这个方法,句法优雅不失风格!
6.11 鸭子类型
详细见书上。没有看的很明白。
6.12 特殊方法
特殊方法(special method),有时也被称作魔术方法(magic method)。>>> class Word(): ... def __init__(self, text): ... self.text = text ... def __eq__(self, word2): ... return self.text.lower() == word2.text.lower() ... 修改就此结束,来看看新的版本能否正常工作: >>> first = Word('ha') >>> second = Word('HA') >>> third = Word('eh') >>> first == second True >>> first == third False
详细见书上。
6.13 组合
如果你想要创建的子类在大多数情况下的行为都和父类相似的话(子类是父类的一种特殊情况,它们之间是 is-a 的关系),使用继承是非常不错的选择。建立复杂的继承关系确实很吸引人, 但有些时候使用组合(composition) 或聚合(aggregation) 更加符合现实的逻辑(x 含有 y,它们之间是 has-a 的关系)。一只鸭子是鸟的一种(is-a),它有一条尾巴(has-a)。尾巴并不是鸭子的一种,它是鸭子的组成部分。代码例子见书上。
6.14 何时使用类和对象而不是模块
有一些方法可以帮助你决定是把你的代码封装到类里还是模块里。当你需要许多具有相似行为(方法)但不同状态(特性)的实例时,使用对象是最好的选择。
类支持继承,但模块不支持。
如果你想要保证实例的唯一性, 使用模块是最好的选择。不管模块在程序中被引用多少次,始终只有一个实例被加载。(对 Java 和 C++ 程序员来说,如果读过 Erich Gamma 的《设计模式:可复用面向对象软件的基础》,可以把 Python 模块理解为单例。)
如果你有一系列包含多个值的变量, 并且它们能作为参数传入不同的函数,那么最好将它们封装到类里面。 举个例子,你可能会使用以 size 和 color 为键的字典代表一张彩色图片。你可以在程序中为每张图片创建不同的字典,并把它们作为参数传递给像scale() 或者transform() 之类的函数。但这么做的话,一旦你想要添加其他的键或者函数会变得非常麻烦。 为了保证统一性,应该定义一个 Image 类,把 size 和 color 作为特性,把 scale() 和 transform() 定义为方法。这么一来,关于一张图片的所有数据和可执行的操作都存储在了统一的位置。
用最简单的方式解决问题。 使用字典、列表和元组往往要比使用模块更加简单、简洁且快速。而使用类则更为复杂。
命名数组(详细见书上)
本书将 property 译作属性,而将 attribute 译作特性,请读者注意区分。——译者注
参考文献:
1. 《Python语言及其应用》。
相关文章推荐
- 《Python语言及其应用》读书笔记(1)之第2章Python基本元素:数字、字符串和变量(关键词:数字/字符串/变量)
- 《Python语言及其应用》读书笔记(2)之第3章Python容器:列表、元组、字典与集合(关键词:列表/元组/字典/集合)
- 《Python语言及其应用》读书笔记(3)之第4章Python外壳:代码结构(关键词:)
- 《JS高级程序设计》第6章读书笔记:对象继承(三)原型式继承和寄生式继承
- 《Python语言及其应用》读书笔记(6)之第7章像高手一样玩转数据(关键词:数据)
- 《Python语言及其应用》读书笔记(11)之第12章成为真正的Python开发者(关键词:测试/调试/优化)
- [置顶] 《JS高级程序设计》第6章读书笔记:创建对象(一)之工场模式和构造函数模式
- 《JS高级程序设计》第6章读书笔记:创建对象(二)原型模式和组合模式
- 《Python语言及其应用》读书笔记(7)之第8章数据的归宿(关键词:数据)
- 快学scala 第6章 对象 读书笔记及习题答案代码
- 《JS高级程序设计》第6章读书笔记:创建对象(三)再探原型
- 《JS高级程序设计》第6章读书笔记:继承对象(四)寄生组合式继承
- 《Python语言及其应用》读书笔记(10)之第11章并发和网络(关键词:并发/网络)
- 《Flask Web开发》读书笔记(6)第6章电子邮件(关键词:Flask/电子邮件)
- 《Python语言及其应用》读书笔记(8)之第9章剖析Web(关键词:Web)
- 《Python学习手册》学习笔记(6)之第6章动态类型简介(关键词:编程语言/Python/变量/对象/引用)
- 《JS高级程序设计》第6章读书笔记:继承对象(二)借用构造函数和组合继承
- 《JS高级程序设计》第6章读书笔记:对象继承(一)原型链
- 《Python基础教程》读书笔记(1)之第6章抽象(关键词:Python/抽象/函数/参数/作用域)
- 《Python语言及其应用》读书笔记(4)之第5章Python盒子:模块、包和程序(关键词:模块/包/程序)