Python学习教程(七)——抽象之类和对象

  前几章介绍了Python主要的内建对象类型(数字、字符串、列表、元组和字典),以及内建函数和标准库的用法,还有定义的函数方法。本章我们主要学习自己创建对象。同时Python也称为是“面向对象”的语言。

1. 类和类型

1.1. 什么是类

  “类”我们可以理解为种类类型的同义词。由这个类创建的所有的对象都属于这类,或者说所以的对象都属于某一个类,所有对象也称为类的实例(instance)
  为了说清楚类和实例的关系,我们举一个实现世界中的例子,我们说“鸟”类,就是一种生物的类型,抽象出了具有羽毛、会飞等特性的一类生物的总称,我们眼睛所看到的具体的一只鸟,比如窗外树上的那只麻雀,它就是“鸟”类的具体的一个实例,我们把它称为“对象”。那么世界上所有的具体的一只一只的各种鸟,都是这个“鸟”类的一个一个的实例对象,这些实例对象都具有“鸟”类所抽象出来的所有特征。这就是类和对象的关系。理解了这个关系,我们也就进一步理解了抽象以及更好的发挥我们的抽象能力。
  借着上面的例子,我们简单描述一下如何对事物进行抽象,我们假设一个场景,这个世界就你一个人,万事万物还都未定义,你站在这个陌生的世界,眼睛看到的是各式各样的事物,假如你是一个具有抽象能力的人,那么下面要做的事情,就是观察这个世界的一个一个的具体的事物(对象),然后定义出一个一个的类,具有飞行能力的、具有羽毛的特征的生物定义为鸟类,具有鳞片的、在水里游的特征的生物定义为鱼类,以此类推,你便抽象出了整个世界的种种类型。回到我们的具体工作中依然如此,假如我们要做一个购物网站,网站要卖各种具体的商品,利用我们的抽象能力我们马上可以抽象出一个商品类,这个类具有的属性包括唯一编号、价格、颜色等等,每一个具体的商品就是这个类的一个实例对象。
  下面我们再说一个“子类”,我们的抽象也分层次,还是以上面商品类举例,商品类作为最高层次的抽象,描述了所有商品实例的共同特征,图书类、电子产品类继承了商品类,成了它的子类,图书类除了继承了商品类的一切属性外,又定义了作者、页数、出版社等属性,那么我们说图书类是商品类的子类,商品类是图书类的超类
  子类一般来讲是对超类定义属性或方法的扩展,当然也可能是重写父类的方法,比如图书类继承了商品的所有的属性和方法,假如商品类的color属性有一个对应的getColor的方法,图书类继承了这个方法后并没有什么用处,因此我们可以在子类图书类中重写了这个getColor方法,返回一个“”字符串。

1.2. 类的创建

  先来看一个简单的例子:

#coding=UTF-8
__metaclass__ = type    # 确定使用新式类

class Person:
    def setName(self,name):
        self.userName = name

    def getName(self):
        return self.userName

    def greet(self):
        print "Helo World! I'm %s." % self.userName

所谓的旧式类和新式类之间是有区别的。除非是Python3.0之前版本中默认附带的代码,否则再继续使用旧式类已无必要。新式类的语法中,需要在模块或者脚本开始的地方放置赋值语句__metaclass__ = type,下面的实例代码中如果没有写,我们可以默认认为已经加上了这一行。

  上面的例子包含3个方法的定义,除了他们是写在class语句里面的外,一切看起来都像是函数的定义。Person是类的名字。Class语句会在函数定义的地方创建自己的命名空间。一切看起来都很自然,但是那个self参数看起来有点奇怪。它是对对象自身的引用。看一个例子:

foo = Person()
bar = Person()
foo.setName("yangliehui")
bar.setName("zhaonan")
foo.greet()
bar.greet()

程序输出:

Helo World! I'm yangliehui.
Helo World! I'm zhaonan.

  上面的例子能说明self的用处,在调用foo的setName和greet方法时,foo自动将自己作为第一个参数传入函数中,因此形象的命名为self。显然这就是self的用处和存在的必要性,没有它的话,成员方法就没法访问他们要对其特性进行操作的对象本身了。
  特性是可以在外部访问的:

foo = Person()
bar = Person()
bar.setName("zhaonan")
foo.userName = "yangliehui"
foo.greet()
print bar.userName

程序输出:

Helo World! I'm yangliehui.
zhaonan

  对象调用的另一种写法:

bar = Person()
bar.setName("zhaonan")

bar.greet()
Person.greet(bar)

程序输出:

Helo World! I'm zhaonan.
Helo World! I'm zhaonan.

  我们看到bar.greet()Person.greet(bar)是等价的。

1.3. 特性、函数和方法

  self参数事实上正是方法和函数的区别。方法(更专业一点可以称为绑定方法)将他们的第一个参数绑定到所属的实例上,因此我们无需显示提供该参数。当然也可以将特性绑定到一个普通的函数上,这样就不会有特殊的self参数了:

class Class:
    def method(self):
        print "I have a self!"

def function():
    print "I don't"

instance = Class()
instance.method()

instance.method = function
instance.method()

程序输出:

I have a self!
I don't

  注意,self参数并不依赖于调用方法的方式,前面我们使用的是instance.method(实例.方法)的形式,可以随意使用其他变量引用同一个方法。

class Class:
    def method(self):
        print "I have a self!"


instance = Class()
instance.method()

instance_method = instance.method
instance_method()

程序输出:

I have a self!
I have a self!

  尽管最后一个方法调用看起来与函数调用十分相似,但是变量instance_method引用绑定方法instance.method上,也就意味着这还是会对self参数进行访问(也就是说,它仍然绑定到类的形同实例上)。


私有化
  假如我们定义的类中,有个别方法不希望类的外部调用,只能由内部的其他方法调用,我们称这类方法为私有方法,私有化的作用起到了对类的封装作用,屏蔽了不希望对外开放的方法调用。
  Python并不直接支持私有方式,而是靠程序员自己把握在外部进行特性修改的时机。毕竟在使用对象前应该知道如何使用。但是,可以用一些小技巧达到私有特性的效果。
  为了让方法或特性变为私有(从外部无法访问),只要在它的名字前面加上双下划线即可,如:

class Person:
    def __setName(self,name):
        self.name = name

    def getName(self):
        return self.name

foo = Person()
foo.__setName("yangliehui")

程序输出:

Traceback (most recent call last):
  File "D:\workspace_oxygen\APythonTest\com\test\test.py", line 10, in <module>
    foo.__setName("yangliehui")
AttributeError: Person instance has no attribute '__setName'

  现在我们看__setName方法在外部是无法访问的,而只能在类的内部使用和访问。尽管双下划线有些奇怪,但是看起来像是其他语言中的标准的私有方法。真正发生的事情才是不标准的。类的内部定义中,所有以下划线开始的名字都被“翻译”成前面加上单下划线和类名的形式。所以我们实际上还是能访问这些私有方法的,尽管不应该这么做:

class Person:
    def __setName(self,name):
        self.name = name

    def getName(self):
        return self.name

foo = Person()
foo._Person__setName("yangliehui")

  上面的程序可以正常访问了,因此确保他们不会访问对象的方法和特性是不可能的,但是这一类方法或属性的定义方式就是他们不应该访问的强有力的信号。
  如果不需要使用这种方法但是又想让其他对象不要访问内部数据,那么可以使用单下划线。这不过是个习惯,但是的确有实际效果。例如,前面有下划线的名字都不会被带星号的import语句(from module import *)导入。

1.4. 类的命名空间

  下面的两个语句(几乎)等价:

def foo(x): return x * x
foo = lambda x: x*x

  两者都创建了返回参数平方的函数,而且都将变量foo绑定到函数上。变量foo可以在全局(模块)范围内进行定义,也可以处于局部的函数或方法内。定义类时,同样的事情也会发生,所以位于class语句中的代码都在特殊的命名空间中执行——类命名空间(class namespace)。这个命名空间可由类内所有成员访问。并不是所有Python程序员都知道类的定义其实就是执行代码块,这一点非常有用,比如,在类的定义区并不只限定只能使用def语句:

class C:
    print 'Class C being defined'

程序输出:

Class C being defined

  再来看一下例子:

class MemberCounter:
    members = 0
    def init(self):
        MemberCounter.members += 1

m1 = MemberCounter()
m1.init()
print  MemberCounter.members

m2 = MemberCounter()
m2.init()
print MemberCounter.members

程序输出:

1
2

  上面的代码中,在类作用域内定义了一个可供所有成员(实例)访问的变量,当实例化一个对象后,调用init方法来计算类的成员数量。
  类作用域内的变量也可以被实例访问:

print m1.members
print m2.members

程序输出:

2
2

  那么在实例中重绑定members变量呢?

class MemberCounter:
    members = 0
    def init(self):
        MemberCounter.members += 1


m1 = MemberCounter()
m1.init()
print  MemberCounter.members

m2 = MemberCounter()
m2.init()
print MemberCounter.members
print '-----'
m1.members = 100
print MemberCounter.members
print m1.members
print m2.members

程序输出:

1
2
-----
2
100
2

  我们看到当新的members值被写到m1的特性中以后,屏蔽了类范围的变量。这个跟函数内的局部和全局变量的行为十分类似。在上面的例子,在类中我们定义了全局的类变量,那么当m1对象调用init后变量的值加为了1,当m2再调用init后,由于members变量是属于类的,所有会再1的基础上再累加1,我们看到打印出了2。然后当我们m1.members=100执行后,相当于我们在m1的对象里设置了一个只属于这个对象的变量,这个属于对象的变量在用对象访问时屏蔽了类变量的值,因此print MemberCounter.members打印出的还是2。而用对象访问时print m1.members打印出了属于对象变量的100 。print m2.members打印出的还是2,是因为我们并没有对m2这个对象提供一个属于这个对象的变量,因此它访问的还是那个属于类的全局变量。我们再看一个例子:

class MemberCounter:
    members = 0
    def init(self):
        MemberCounter.members += 1
        self.members += 1


m1 = MemberCounter()
m1.init()
print MemberCounter.members
print m1.members

print '------'

m2 = MemberCounter()
m2.init()
print MemberCounter.members
print m2.members

程序输出:

1
2
------
2
3

  上面的例子也是说明类变量和对象变量之间的关系。请自行分析。

1.5. 指定超类

  子类可以扩展超类(也称为父类或基类),将其他类名写在class语句后的圆括号内可以指定超类:

#coding=UTF-8
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):             # 子类重写了超类init方法
        self.blocked = ['SPAM']

  Filter是个用于过滤序列的通用类,事实上它不能过滤任何东西:

f = Filter()
f.init()
print f.filter([1,2,3])

程序输出:

[1, 2, 3]

  Filter类的用处在于它可以用作其他类的超类,比如SPAMFilter类,可以将序列中的’SPAM’过滤出去。

f = SPAMFilter()
f.init()
print f.filter(['SPAM','SPAM','SPAM','SPAM','yangliehui'])

程序输出:

['yangliehui']

注意:SPAMFilter定义的两个要点:
1. 这里用提供的新定义的方式重写了Filter的init定义。
2. filter方法的定义是从Filter类中继承过来的,所以不用重写它的定义。
第二个要点揭示了继承的用处:我可以写一大堆不同的过滤类,全都从Filter继承,每一个我们都可以使用已经实现的filter方法。

1.6. 检查继承

  如果要查看一个类是否是另一个的子类,可以使用内建的issubclass函数:

print issubclass(SPAMFilter, Filter)
print issubclass(Filter, SPAMFilter)

程序输出:

True
False

  如果想要知道已知类的基类(们),可以直接使用它的特殊特性__bases__:

print SPAMFilter.__bases__
print Filter.__bases__

程序输出:

(<class __main__.Filter at 0x0000000001E7C828>,)
()

  同样还能使用isinstance方法检查一个对象是否是同一个类的实例:

s = SPAMFilter()
print isinstance(s, SPAMFilter)
print isinstance(s, Filter)
print isinstance(s, str)

程序输出:

True
True
False

  可以看到,s是SPAMFilter类的(直接)实例,但是它也是Filter类的间接实例,因为SPAMFilter是Filter的子类。另外一种说法就是SPAMFilter类就是Filter类。

注意:使用isinstance并不是一个好习惯,使用多态会更好一些。关于多态的概念我们很快会讲到。
  如果只想知道一个对象属于哪一个类,可以使用__class__特性:

print s.__class__

程序输出:

__main__.SPAMFilter

注意:如果使用__metaclass__=type或从object继承的方式来定义新式类,那么可以使用type(s)查看实例所属的类,代码如下。

__metaclass__ = type
class Filter:
    def init(self):
        self.name = []

f = Filter()
print type(f)

程序输出:

<class '__main__.Filter'>

1.7. 多个超类

  Python是多继承模式的,意味着一个子类可以有多个超类,例子如下:

__metaclass__ = type

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)自己不做任何事,它从自己的超类继承了所有的方法。

tc = TalkingCalculator()
tc.calculate('1+2*3')
tc.talk()

程序输出:

Hi,my value is 7

  当使用多重继承时,有个需要注意的地方。如果一个方法从多个超类继承(也就是说你有两个具有相同名字的不同方法),那么必须要注意一下超类的顺序,先继承的类的方法会重写后继承的类中的方法。

2. 对象概述

  在面向对象程序设计中,术语对象(object)基本上可以看做数据(特性)以及由一系列可以存取、操作这些数据的方法所组成的集合,是根据类的定义实例化出的一种数据的结构。
  对象的优点主要包括以下几个方面:
  多态:意味着可以对不同类的对象使用同样的操作。
  封装:对外部世界隐藏对象的工作细节。
  继承:以通用的类为基础建立专门的类对象。

2.1. 多态

  对于封装和继承,我们前面的内容已经有所体会,现在重点对多态的概念做一个了解和认识。
  多态,意思是“有多种形式”。多态意味着就算不知道变量所引用的对象类型是什么,还能对它进行操作,而它会根据对象(或类)类型的不同而表现出不同的行为。我们来看一个例子:

def fun_count(object,ch):
    return object.count(ch)

print fun_count('abc', 'a')
print fun_count([1,2,'a'], 'a')

程序输出:

1
1

  我们看上面的代码,对于函数fun_count里的object参数来说,其本身不需要知道它具体的类型是一个字符串还是一个列表,只要传进去的具体的参数类型含有count方法即可。上面的例子中我们是用的Python内置的count函数,现在我们用自定义类的方式,再举一个例子:


__metaclass__ = type

class A:
    def func(self):
        print 'A'

class B:
    def func(self):
        print 'B'

def print_obj(object):
    object.func()

print_obj(A())
print_obj(B())

程序输出:

A
B

  上例中,定义了两个类A和B,这两个类都有func方法,现在我们看print_obj函数,其多态体现在这个函数的参数是一个object,这个object关键字代表了所有的类型,这表示print_obj函数可以传进去任何类型的对象,那么所传进去的对象只要其内有func方法,都会被正确执行,print_obj也就自然的实现了多态,使用者只关心传进去的参数实现了所需的方法即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值