目录
1. 对象魔法
1.1 多态
可对不同类型的对象执行相同的操作,而这些操作就像“被施了魔法”一样能够正常运行。
1.2 多态和方法
每当无需知道对象是什么样的就能对其执行操作时,都是多态在起作用。
很多函数和运算符都是多态的,你编写的大多数函数也可能如此,即便你不是有意为之。每当你使用多态的函数和运算符时,多态都将发挥作用。事实上,要破坏多态,唯一的办法是使用诸如type 、issubclass 等函数显式地执行类型检查,但你应尽可能避免以这种方式破坏多态。重要的是,对象按你希望的那样行事,而非它是否是正确的类型(类)。
1.3 封装
封装 (encapsulation)指的是向外部隐藏不必要的细节。这听起来有点像多态(无需知道对象的内部细节就可使用它)。这两个概念很像,因为它们都是抽象的原则 。它们都像函数一样,可帮助你处理程序的组成部分,让你无需关心不必要的细节。
但封装不同于多态。多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封装让你无需知道对象的构造就能使用它。
1.4 继承
可基于通用类创建出专用类。
2. 类
2.1 类是什么
类:一种对象。每个对象都属于 特定的类,并被称为该类的实例 。
2.2 创建自定义类
class Person:
def set_name(self, name):
self.name = name
def get_name(self):
return self.name
def greet(self):
print("Hello, world! I'm {},".format(self.name))
# 创建两个实例
>>> 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.
self是什么。对foo 调用set_name 和greet 时,foo 都会作为第一个参数自动传递给它们。将这个参数命名为self ,这非常贴切。实际上,可以随便给这个参数命名,但鉴于它总是指向对象本身,因此习惯上将其命名为self 。
self 很有用,甚至必不可少。如果没有它,所有的方法都无法访问对象本身——要操作的属性所属的对象。也可以从外部访问这些属性。
>>> foo.name
'Luke Skywalker'
>>> bar.name = 'Yoda'
>>> bar.greet()
Hello, world! I'm Yoda.
如果foo 是一个Person 实例,可将foo.greet() 视为Person.greet(foo) 的简写,但后者的多态性更低。
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
)调用方法。完全可以让另一个变量指向同一个方法。虽然这个方法调用看起来很像函数调用,但变量birdsong
指向的是关联的方法bird.sing
,这意味着它也能够访问参数self
(即它也被关联到类的实例)。
>>> class Bird:
... song = 'Squaawk!'
... def sing(self):
... print(self.song)
...
>>> bird = Bird()
>>> bird.sing()
Squaawk!
>>> birdsong = bird.sing
>>> birdsong()
Squaawk!
2.4 隐藏
默认情况下,可从外部访问对象的属性。
将属性定义为私有 。私有属性不能从对象外部访问,而只能通过存取器 方法(如get_name 和set_name )来访问。
要让方法或属性成为私有的(不能从外部访问),只需让其名称以两个下划线打头即可。
class Secretive:
def __inaccessible(self):
print("Bet you can't see me ...")
def accessible(self):
print("The secret message is:")
self.__inaccessible()
# 现在从外部不能访问__inaccessible ,但在类中(如accessible 中)依然可以使用它。
>>> s = Secretive()
>>> s.__inaccessible()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: Secretive instance has no attribute '__inaccessible'
>>> s.accessible()
The secret message is:
Bet you can't see me ...
虽然以两个下划线打头有点怪异,但这样的方法类似于其他语言中的标准私有方法。然而,幕后的处理手法并不标准:在类定义中,对所有以两个下划线打头的名称都进行转换,即在开头加上一个下划线和类名。
>>> Secretive._Secretive__inaccessible
<unbound method Secretive.__inaccessible>
# 只要知道这种幕后处理手法,就能从类外访问私有方法,然而不应这样做。
>>> s._Secretive__inaccessible()
Bet you can't see me ...
如果你不希望名称被修改,又想发出不要从外部修改属性或方法的信号,可用一个下划线打头。这虽然只是一种约定,但也有些作用。例如,from module import * 不会导入以一个下划线打头的名称。
2.5 类的命名空间
# 这两条语句大致等价
def foo(x):
return x * x
foo = lambda x: x * x
它们都创建一个返回参数平方的函数,并将这个函数关联到变量foo 。可以在全局(模块)作用域内定义名称foo ,也可以在函数或方法内定义。定义类时情况亦如此:在class 语句中定义的代码都是在一个特殊的命名空间(类的命名空间 )内执行的,而类的所有成员都可访问这个命名空间。类定义其实就是要执行的代码段。在类定义中,并非只能包含def 语句。
>>> class C:
... print('Class C being defined...')
...
Class C being defined...
下面这些代码在类作用域内定义了一个变量,所有的成员(实例)都可访问它,这里使用它来计算类实例的数量。注意到这里使用了init 来初始化所有实例,也就是将init 转换为合适的构造函数。
每个实例都可访问这个类作用域内的变量,就像方法一样。
class MemberCounter:
members = 0
def init(self):
MemberCounter.members += 1
>> m1 = MemberCounter()
>>> m1.init()
>>> MemberCounter.members
1
>>> m2 = MemberCounter()
>>> m2.init()
>>> MemberCounter.members
2
# 每个实例都可访问这个类作用域内的变量,就像方法一样。
>>> m1.members
2
>>> m2.members
2
# 如果你在一个实例中给属性members 赋值,结果将如何呢
# 新值被写入m1 的一个属性中,这个属性遮住了类级变量。
>>> m1.members = 'Two'
>>> m1.members
'Two'
>>> m2.members
2
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']
>>> f = Filter()
>>> f.init()
>>> f.filter([1, 2, 3])
[1, 2, 3]
# 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 。这就是懒惰的好处。)
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>
对于新式类(无论是通过使用__metaclass__ = type 还是通过从object 继承创建的)的实例,还可使用type(s) 来获悉其所属的类。对于所有旧式类的实例,type 都只是返回instance 。
2.8 多个超类
如何继承多个类:
子类TalkingCalculator 本身无所作为,其所有的行为都是从超类那里继承的。关键是通过从Calculator 那里继承calculate ,并从Talker 那里继承talk ,它成了会说话的计算器。
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
>>> tc = TalkingCalculator()
>>> tc.calculate('1 + 2 * 3')
>>> tc.talk()
Hi, my value is 7
这被称为多重继承 ,是一个功能强大的工具。然而,除非万不得已,否则应避免使用多重继承,因为在有些情况下,它可能带来意外的“并发症”。
使用多重继承时,有一点务必注意:如果多个超类以不同的方式实现了同一个方法(即有多个同名方法),必须在class 语句中小心排列这些超类,因为位于前面的类的方法将覆盖位于后面的类的方法。因此,在前面的示例中,如果Calculator 类包含方法talk ,那么这个方法将覆盖Talker 类的方法talk (导致它不可访问)。
将导致Talker 的方法talk 是可以访问的。多个超类的超类相同时,查找特定方法或属性时访问超类的顺序称为方法解析顺序 (MRO),它使用的算法非常复杂。所幸其效果很好。
2.9 接口和内省
接口这一概念与多态相关。处理多态对象时,你只关心其接口(协议)——对外暴露的方法和属性。在Python中,不显式地指定对象必须包含哪些方法才能用作参数。
通常,你要求对象遵循特定的接口(即实现特定的方法),但如果需要,也可非常灵活地提出要求:不是直接调用方法并期待一切顺利,而是检查所需的方法是否存在;如果不存在,就改弦易辙。
>>> hasattr(tc, 'talk')
True
>>> hasattr(tc, 'fnord')
False
tc 包含属性talk (指向一个方法),但没有属性fnord 。还可以检查属性talk 是否是可调用的。
getattr (可以指定属性不存在时使用的默认值,这里为None ),然后对返回的对象调用callable 。
>>> callable(getattr(tc, 'talk', None))
True
>>> callable(getattr(tc, 'fnord', None))
False
setattr 与getattr 功能相反,可用于设置对象的属性:
>>> setattr(tc, 'name', 'Mr. Gumby')
>>> tc.name
'Mr. Gumby'
要查看对象中存储的所有值,可检查其__dict__ 属性。如果要确定对象是由什么组成的,应研究模块inspect 。这个模块主要供高级用户创建对象浏览器(让用户能够以图形方式浏览Python对象的程序)以及其他需要这种功能的类似程序。
2.10 抽象基类
一般而言,抽象类是不能(至少是不应该 )实例化的类,其职责是定义子类应实现的一组抽象方法。
from abc import ABC, abstractmethod
class Talker(ABC):
@abstractmethod
def talk(self):
pass
形如@this 的东西被称为装饰器,这里的要点是你使用@abstractmethod 来将方法标记为抽象的——在子类中必须实现的方法。
抽象类(即包含抽象方法的类)最重要的特征是不能实例化。
>>> Talker()
# 报错
# 派生出一个子类
class Knigget(Talker):
pass
由于没有重写方法talk ,因此这个类也是抽象的,不能实例化。实例化会报错。
可重新编写这个类,使其实现要求的方法。
class Knigget(Talker):
def talk(self):
print('Ni!')
现在实例化它没有任何问题。这是抽象基类的主要用途,而且只有在这种情形下使用isinstance 才是妥当的:如果先检查给定的实例确实是Talker 对象,就能相信这个实例在需要的情况下有方法talk 。
>>> k = Knigget()
>>> isinstance(k, Talker)
True
>>> k.talk()
Ni!
3. 关于面向对象设计
- 将相关的东西放在一起。如果一个函数操作一个全局变量,最好将它们作为一个类的属性和方法。
- 不要让对象之间过于亲密。方法应只关心其所属实例的属性,对于其他实例的状态,让它们自己去管理就好了。
- 慎用继承,尤其是多重继承。继承有时很有用,但在有些情况下可能带来不必要的复杂性。要正确地使用多重继承很难,要排除其中的bug更难。
- 保持简单。让方法短小紧凑。一般而言,应确保大多数方法都能在30秒内读完并理解。对于其余的方法,尽可能将其篇幅控制在一页或一屏内。
确定需要哪些类以及这些类应包含哪些方法时,尝试像下面这样做。
- 将有关问题的描述(程序需要做什么)记录下来,并给所有的名词、动词和形容词加上标记。
- 在名词中找出可能的类。
- 在动词中找出可能的方法。
- 在形容词中找出可能的属性。
- 将找出的方法和属性分配给各个类。
有了面向对象模型 的草图后,还需考虑类和对象之间的关系(如继承或协作)以及它们的职责。为进一步改进模型,可像下面这样做。
- 记录(或设想)一系列用例 ,即使用程序的场景,并尽力确保这些用例涵盖了所有的功能。
- 透彻而仔细地考虑每个场景,确保模型包含了所需的一切。如果有遗漏,就加上;如果有不太对的地方,就修改。不断地重复这个过程,直到对模型满意为止。
文章评论