06. Python-抽象2

目录

1. 对象魔法

1.1 多态

1.2 多态和方法

1.3 封装

1.4 继承

2. 类

2.1 类是什么

2.2 创建自定义类

2.3 属性、函数和方法

2.4 隐藏

2.5 类的命名空间

2.6 指定超类

2.7 深入了解继承

2.8 多个超类

2.9 接口和内省

2.10 抽象基类

3. 关于面向对象设计


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秒内读完并理解。对于其余的方法,尽可能将其篇幅控制在一页或一屏内。

确定需要哪些类以及这些类应包含哪些方法时,尝试像下面这样做。

  1. 将有关问题的描述(程序需要做什么)记录下来,并给所有的名词、动词和形容词加上标记。
  2. 在名词中找出可能的类。
  3. 在动词中找出可能的方法。
  4. 在形容词中找出可能的属性。
  5. 将找出的方法和属性分配给各个类。

有了面向对象模型 的草图后,还需考虑类和对象之间的关系(如继承或协作)以及它们的职责。为进一步改进模型,可像下面这样做。

  1. 记录(或设想)一系列用例 ,即使用程序的场景,并尽力确保这些用例涵盖了所有的功能。
  2. 透彻而仔细地考虑每个场景,确保模型包含了所需的一切。如果有遗漏,就加上;如果有不太对的地方,就修改。不断地重复这个过程,直到对模型满意为止。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南河Aure

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值