抽象"/>
Python基础教程(第3版)》笔记:第7章 再谈抽象
Python基础教程(第3版)》笔记:第7章 再谈抽象
创建自定义对象(尤其是对象类型或类)是一个Python核心概念。下面列出了使用对象的最重要的好处。
- 多态:可对不同类型的对象执行相同的操作,而这些操作就像“被施了魔法”一样能够
正常运行。 - 封装:对外部隐藏有关对象工作原理的细节。
- 继承:可基于通用类创建出专用类。
7.1.1 多态
术语**多态(polymorphism)**源自希腊语,意思是“有多种形态”。这大致意味着即便你不知道变量指向的是哪种对象,也能够对其执行操作,且操作的行为将随对象所属的类型(类)而异。
7.1.2 多态和方法
7.1.3 封装
**封装(encapsulation)**指的是向外部隐藏不必要的细节。
但封装不同于多态。多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封装让你无需知道对象的构造就能使用它。听起来还是有点像?下面来看一个使用了多态但没有使用封装的示例。假设你有一个名为OpenObject的类(如何创建类将在本章后面介绍)。
7.2 类
7.2.1 类到底是什么
每个对象都属于特定的类,并被称为该类的实例。
注意: 在python中,约定使用单数并将首字母大写命名类
7.2.2 创建自定义类
class Person:def set_name(self, name):self.name = namedef get_name(self):return self.namedef greet(self):print("Hello, world! I'm {}.".format(self.name))
class语句创建独立的命名空间,用于在其中定义函数。self指向对象本身。
>>> foo = Person()
>>> bar = Person()
>>> foo.set_name('Luke Skywalker')
>>> bar.set_name('Anakin Skywalker')
>>> foo.greet()
Hello, world! I'm Luke Skywalker.
>>> bar.greet()
Hello, world! I'm Anakin Skywalker.
从外部访问这些属性。
>>> foo.name
'Luke Skywalker'
>>> bar.name = 'Yoda'
>>> bar.greet()
Hello, world! I'm Yoda.
7.2.3 属性、函数和方法
实际上,方法和函数的区别表现在前一节提到的参数self上。方法(更准确地说是关联的方法)将其第一个参数关联到它所属的实例,因此无需提供这个参数。无疑可以将属性关联到一个普通函数,但这样就没有特殊的self参数了。
>>> class Class: def method(self): print('I have a self!') >>> def function(): print("I don't...") >>> instance = Class()
>>> instance.method()
I have a self!
>>> instance.method = function #将属性关联到一个普通函数
>>> instance.method()
I don't...
将属性关联到一个普通函数
请注意,有没有参数self并不取决于是否以刚才使用的方式(如instance.method)调用方法。
实际上,完全可以让另一个变量指向同一个方法。
>>> class Bird: song = 'Squaawk!' def sing(self): print(self.song) >>> bird = Bird()
>>> bird.sing()
Squaawk!
>>> birdsong = bird.sing
>>> birdsong()
Squaawk!
虽然最后一个方法调用看起来很像函数调用,但变量birdsong指向的是关联的方法bird.sing,这意味着它也能够访问参数self(即它也被关联到类的实例)。
7.2.4 再谈隐藏
可将属性定义为私有。私有属性不能从对象外部访问,而只能通过存取器方法(如get_name和set_name)来访问。
Python没有为私有属性提供直接的支持,而是要求程序员知道在什么情况下从外部修改属性是安全的。毕竟,你必须在知道如何使用对象之后才能使用它。然而,通过玩点小花招,可获得类似于私有属性的效果。
要让方法或属性成为私有的(不能从外部访问),只需让其名称以两个下划线打头即可。
class Secretive: def __inaccessible(self): print("Bet you can't see me ...") def accessible(self): print("The secret message is:") self.__inaccessible()
现在从外部不能访问__inaccessible,但在类中(如accessible中)依然可以使用它。
7.2.5 类的命名空间
定义类时:在class语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,而类的所有成员都可访问这个命名空间。类定义其实就是要执行的代码段,并非所有的Python程序员都知道这一点,但知道这一点很有帮助。例如,在类定义中,并非只能包含def语句。
class MemberCounter: members = 0 def init(self): MemberCounter.members += 1
>>> m1 = MemberCounter()
>>> m1.init()
>>> MemberCounter.members
1
>>> m2 = MemberCounter()
>>> m2.init()
>>> MemberCounter.members
2
上述代码在类作用域内定义了一个变量,所有的成员(实例)都可访问它,这里使用它来计算类实例的数量。注意到这里使用了init来初始化所有实例,第9章将把这个初始化过程自动化,也就是将init转换为合适的构造函数
每个实例都可访问这个类作用域内的变量,就像方法一样。
>>> m1.members
2
>>> m2.members
2
如果你在一个实例中给属性members赋值,结果将如何呢?
>>> m1.members = 'Two'
>>> m1.members
'Two'
>>> m2.members
2
新值被写入m1的一个属性中,这个属性遮住了类级变量。这类似于第6章的旁注“遮盖的问题”所讨论的,函数中局部变量和全局变量之间的关系。
7.2.6 指定超类
要指定超类,可在class语句中的类名后加上超类名,并将其用圆括号括起
class Filter: def init(self): self.blocked = [] def filter(self, sequence): return [x for x in sequence if x not in self.blocked]
class SPAMFilter(Filter): # SPAMFilter是Filter的子类def init(self): # 重写超类Filter的方法init self.blocked = ['SPAM']
Filter类的用途在于可用作其他类(如将’SPAM’从序列中过滤掉的SPAMFilter类)的基类(超类)。
>>> s = SPAMFilter()
>>> s.init()
>>> s.filter(['SPAM', 'SPAM', 'SPAM', 'SPAM', 'eggs', 'bacon', 'SPAM'])
['eggs', 'bacon']
请注意SPAMFilter类的定义中有两个要点。
- 以提供新定义的方式重写了Filter类中方法init的定义。
- 直接从Filter类继承了方法filter的定义,因此无需重新编写其定义。
第二点说明了继承很有用的原因:可以创建大量不同的过滤器类,它们都从Filter类派生而来,并且都使用已编写好的方法filter。这就是懒惰的好处。
7.2.7 深入探讨继承
要确定一个类是否是另一个类的子类,可使用内置方法issubclass。
>>> issubclass(SPAMFilter, Filter)
True
>>> issubclass(Filter, SPAMFilter)
False
如果你有一个类,并想知道它的基类,可访问其特殊属性__bases__。
>>> SPAMFilter.__bases__
(<class __main__.Filter at 0x171e40>,)
>>> Filter.__bases__
(<class 'object'>,)
同样,要确定对象是否是特定类的实例,可使用isinstance。
>>> s = SPAMFilter()
>>> isinstance(s, SPAMFilter)
True
>>> isinstance(s, Filter)
True
>>> isinstance(s, str)
False
注意 使用isinstance通常不是良好的做法,依赖多态在任何情况下都是更好的选择。一个重要的例外情况是使用抽象基类和模块abc时。
如你所见,s是SPAMFilter类的(直接)实例,但它也是Filter类的间接实例,因为SPAMFilter是Filter的子类。换而言之,所有SPAMFilter对象都是Filter对象。从前一个示例可知,isinstance也可用于类型,如字符串类型(str)。
如果你要获悉对象属于哪个类,可使用属性__class__。
>>> s.__class__
<class __main__.SPAMFilter at 0x1707c0>
7.2.8 多个超类
在前一节,你肯定注意到了一个有点奇怪的细节:复数形式的__bases__。前面说过,你可使用它来获悉类的基类,而基类可能有多个。为说明如何继承多个类,下面来创建几个类
class Calculator: def calculate(self, expression): self.value = eval(expression)
class Talker: def talk(self): print('Hi, my value is', self.value)
class TalkingCalculator(Calculator, Talker): pass
子类TalkingCalculator本身无所作为,其所有的行为都是从超类那里继承的。关键是通过从Calculator那里继承calculate,并从Talker那里继承talk,它成了会说话的计算器。
>>> tc = TalkingCalculator()
>>> tc.calculate('1 + 2 * 3')
>>> tc.talk()
Hi, my value is 7
这被称为多重继承,是一个功能强大的工具。然而,除非万不得已,否则应避免使用多重继承,因为在有些情况下,它可能带来意外的“并发症”。
使用多重继承时,有一点务必注意:如果多个超类以不同的方式实现了同一个方法(即有多个同名方法),必须在class语句中小心排列这些超类,因为位于前面的类的方法将覆盖位于后面的类的方法。因此,在前面的示例中,如果Calculator类包含方法talk,那么这个方法将覆盖Talker类的方法talk(导致它不可访问)。如果像下面这样反转超类的排列顺序:
class TalkingCalculator(Talker, Calculator): pass
将导致Talker的方法talk是可以访问的。多个超类的超类相同时,查找特定方法或属性时访问超类的顺序称为方法解析顺序(MRO),它使用的算法非常复杂。所幸其效果很好,你可能根本无需担心
7.2.9 接口和内省
接口这一概念与多态相关。处理多态对象时,你只关心其接口(协议)——对外暴露的方法和属性。
7.3 关于面向对象设计的一些思考
提供一些指南。
- 将相关的东西放在一起。如果一个函数操作一个全局变量,最好将它们作为一个类的属性和方法。
- 不要让对象之间过于亲密。方法应只关心其所属实例的属性,对于其他实例的状态,让它们自己去管理就好了。
- 慎用继承,尤其是多重继承。继承有时很有用,但在有些情况下可能带来不必要的复杂性。要正确地使用多重继承很难,要排除其中的bug更难。
- 保持简单。让方法短小紧凑。一般而言,应确保大多数方法都能在30秒内读完并理解。对于其余的方法,尽可能将其篇幅控制在一页或一屏内。
确定需要哪些类以及这些类应包含哪些方法时,尝试像下面这样做。
(1) 将有关问题的描述(程序需要做什么)记录下来,并给所有的名词、动词和形容词加上标记。
(2) 在名词中找出可能的类。
(3) 在动词中找出可能的方法。
(4) 在形容词中找出可能的属性。
(5) 将找出的方法和属性分配给各个类。
有了面向对象模型的草图后,还需考虑类和对象之间的关系(如继承或协作)以及它们的职责。为进一步改进模型,可像下面这样做。
(1) 记录(或设想)一系列用例,即使用程序的场景,并尽力确保这些用例涵盖了所有的功能。
(2) 透彻而仔细地考虑每个场景,确保模型包含了所需的一切。如果有遗漏,就加上;如果有不太对的地方,就修改。不断地重复这个过程,直到对模型满意为止。
更多推荐
Python基础教程(第3版)》笔记:第7章 再谈抽象
发布评论