简明Python3教程 13.面向对象编程
简介
(注: OOP代表面向对象编程,OO代表面向对象,以后全部使用英文缩写)
迄今为止我们编写的所有程序都是围绕函数创建的,函数即操纵数据的语句块。这称作面向过程编程。
除此之外还有另一种组织程序的方法,将数据与功能组合到一起封装进被称为对象的东西中。这叫做OOP。
大多数时候你可以使用过程性编程,但当编写大型程序或问题更倾向以OO方式解决时,你还可以使用OOP技术。
类和对象是OOP的两个重要特征。类用于创建新的类型,而对象是类的实例。这就象你创建int类型的变量,这些变量就是int类的实例(对象)
静态语言程序员请注意
注意在python中就算整数也被当作对象(int类的对象)。
这与C++和Java(版本1.5之前)是不同的,它们中的整数属于原始本地类型。具体详见help(int)。
而C#和java 1.5程序员会发现这与装箱(boxing)和拆箱(unboxing)的概念类似。
对象可以使用属于这个对象的变量存储数据。属于对象或类的变量称作字段(fields)。
对象同样可以利用属于类的函数实现所需的功能。这些函数被称作类的方法(method)。
这些术语非常重要,因为它们帮助我们将独立的函数和变量与属于类或对象的函数和变量区分开来。
字段与方法统称为类属性。
字段分为两种类型 – 它们既可以属于类的实例/对象也可以属于类本身,两者分别称为实例变量与类变量。
一个类通过关键字class创建。字段与方法被列在类的缩进块里。
self
类方法与普通函数只有一个特殊区别 – 类方法必须增加一个额外的形参,而且它必须处于第一个形参的位置,
但是在调用类方法时不要为这个额外的形参传值,python会自动代劳。这个特别的变量引用对象本身,按照惯例它被命名为self。
尽管你可以随便为这个形参取名字,但我强烈建议你使用self – 其它名字会让人皱眉头的。
使用标准名字是有很多好处的 – 任何你的代码的读者都会立即明白它代表什么,甚至当你使用标准名字时专业的IDE都会更好的帮助你。
写给C++/Java/C#程序员
python中的self相当于C++中的this指针和java/C#中的this引用
你一定感到疑惑python是如何为self赋值的,为什么你无需亲力亲为呢?举个例子可以让事情变的明朗。
假设你有一个叫做MyClass的类与一个它的实例myobject。 当你调用这个对象的方法时myobject.method(arg1, arg2),
python会自动将其转换为myobject.method(myobject, arg1, arg2) – 这就是关于self的内幕。
这也意味着如果你有一个无需参数的方法,你也仍然需要一个self实参。
类
下面的例子可以是一个最简单的类了。
#!/usr/bin/python
# Filename: simplestclass.py
class Person:
pass # 空语句块
p = Person()
print(p)
输出:
$ python simplestclass.py
<__main__.Person object at 0x019F85F0>
代码如何工作:
我们使用class语句和一个类名创建了一个新类,其后紧跟一个定义类体的缩进的语句块。本例中pass语句代表一个空语句块。
接下来,我们创建这个类的对象/实例,方法是类名后跟一对小括号(后节我们会学到更多关于实例的知识)。
为了验证变量类型,我们简单的打印它。它告诉我们在__main__模块我们拥有了一个Person类的实例。
注意存储这个对象的计算机内存地址也被打印出来了。而地址值在你的计算机中可能会不同,因为python只要找到可用空间就会把对象存进去。
对象方法
我们已经讨论过对象/类可以拥有类似函数一样的方法,只不过这些函数必须加上额外的self变量。现在我们就来看个例子。
#!/usr/bin/python
# Filename: method.py
class Person:
def sayHi(self):
print('Hello, how are you?')
p = Person()
p.sayHi()
# 在这个简短的例子中同样可以写成Person().sayHi()
输出:
$ python method.py
Hello, how are you?
代码如何工作:
本利中我们使用了self,注意方法sayHi虽无需参数但在函数中仍然要写上self。
__init__方法
在python的类中,有很多方法名拥有特殊意义。我们现在看看__init__方法的特别之处。
__init__方法在类对象被实例化时立即执行。此方法专用于初始化对象。另外注意方法名中的双下划线前后缀。
范例:
#!/usr/bin/python
# Filename: class_init.py
class Person:
def __init__(self, name):
self.name = name
def sayHi(self):
print('Hello, my name is', self.name)
p = Person('Swaroop')
p.sayHi()
# 在这个简短的例子中同样可以写成Person().sayHi()
输出:
$ python class_init.py
Hello, my name is Swaroop
范例如何工作:
这里我们定义一个带有名为name形参的__init__方法(除了寻常的self)。
然后我们又创建了一个名为name的字段。注意它们是两个不同的变量尽管都叫’name’。点号允许我们将它们区分开来。
重点是,我们并没有显式的调用__int__方法,而是在创建类实例时在类名后面的小括号中指定实参。这就是__init__的特殊之处。
初始化之后,我们就可以在方法中使用self.name字段了,方法sayHi演示了这点。
类和对象变量
我们已经讨论了类与对象的功能(方法)部分,现在就来学习数据部分。
数据即字段,只不过是绑定在类和对象的名字空间中的普通变量。这意味着这些名字只在其所属类和对象的上下文中合法有效。这就是它们被称作名字空间的原因。
有两种字段类型 – 类变量和对象变量,它们根据所有者是类还是对象而区分开来。
类对象是共享的 – 它们可以被一个类的所有实例存取。即一个类对象在类中只存在一个拷贝,任何对象对其的修改都会反映到所有对象上。
而每个独立的类对象/实例都拥有自己的对象变量。这样每个对象都有属于自己的字段拷贝即这些字段非共享,不同对象中的同名对象变量彼此没有任何关联。
举个例子将更容易理解:
#!/usr/bin/python
# Filename: objvar.py
class Robot:
'''Represents a robot, with a name.'''
# 一个类变量用于记录机器人的数量
population = 0
def __init__(self, name):
'''Initializes the data.'''
self.name = name
print('(Initializing {0})'.format(self.name))
# When this person is created, the robot
# adds to the population
Robot.population += 1
def __del__(self):
'''I am dying.'''
print('{0} is being destroyed!'.format(self.name))
Robot.population -= 1
if Robot.population == 0:
print('{0} was the last one.'.format(self.name))
else:
print('There are still {0:d} robots
working.'.format(Robot.population))
def sayHi(self):
'''Greeting by the robot.
Yeah, they can do that.'''
print('Greetings, my masters call me {0}.'.format(self.name))
def howMany():
'''Prints the current population.'''
print('We have {0:d} robots.'.format(Robot.population))
howMany = staticmethod(howMany)
droid1 = Robot('R2-D2')
droid1.sayHi()
Robot.howMany()
droid2 = Robot('C-3PO')
droid2.sayHi()
Robot.howMany()
print("/nRobots can do some work here./n")
print("Robots have finished their work. So let's destroy them.")
del droid1
del droid2
Robot.howMany()
输出:
(Initializing R2-D2)
Greetings, my masters call me R2-D2.
We have 1 robots.
(Initializing C-3PO)
Greetings, my masters call me C-3PO.
We have 2 robots.
Robots can do some work here.
Robots have finished their work. So let's destroy them.
R2-D2 is being destroyed!
There are still 1 robots working.
C-3PO is being destroyed!
C-3PO was the last one.
We have 0 robots.
代码如何工作:
这个例子很长但有助于演示类和对象变量的本质。
population属于Robot类因此它是个类变量。变量name属于对象(使用self赋值)因此它是个对象变量。
于是乎,我们使用Robot.population引用类变量populatin而不是self.population.。而在方法中引用对象变量name时使用self.name语法。
记住这个类变量和类对象中简单的差异吧。还要注意对象变量会隐藏同名的类变量!
howMany实际上是一个属于类的方法而不是对象。这意味着我们可以将及其定义为classmethod也可以定义为staticmethod,
这取决于我们是否需要知道我们是哪个类的一部分。因为我们无需类似信息,所以我们使用staticmethod。
另外我们还可以通过装饰符(decorators)达到同样的效果:
@staticmethod
def howMany():
'''Prints the current population.'''
print('We have {0:d} robots.'.format(Robot.population))
装饰符可以被想象成调用一条显式语句的捷径,就像在这个例中看到的一样。
观察__init__方法,它用于以一个指定的名字初始化Robot实例。其内部对population累加1,因为我们又添加了一个机器人。
同时观察self.name,它的值特定于每个对象,这也指出了类对象的本质。
记住,你只能使用self引用相同对象的变量和方法。这被称作属性引用。
本例中,我们还在类与方法中使用了文档字符串。我们可以在运行时使用Robot.__doc__和Robot.sayhi.__doc__分别访问类和方法的文档字符串。
就像__init__方法,这里还有另一个特殊方法__del__,当对象挂掉的时候将被调用。
对象挂掉是指对象不再被使用了,它占用的空间将返回给系统以便重复使用。
在__del__中我们只是简单的将Robot.population减1。
当对象不再被使用时__del__方法将可以被执行,但无法保证到底啥时执行它。
如果你想显式执行它则必须使用del语句,就象本例中做的那样。(注:本例中del后对象的引用计数降为0)。
C++/Java/C#程序员请注意
python中所有类成员(包括数据成员)全部为public,并且所有方法都为virtual。
只有一个例外:如果你使用的数据成员,其名字带有双下划线前缀例如__privatevar,则python将使用名字混淆机制有效的将其作为private变量。
另外存在一个惯例,任何只在类或对象内部使用的变量应该以单下划线为前缀,其他的变量则为public可以被其它类/变量使用。
记住这只是一个惯例而不是强迫(不过双下划线前缀例外).
继承
OOP的主要好处就是代码重用,而达此目的的方法之一是利用继承机制。最好将继承想象为实现类与类之间的类型与子类型关系。
假设你想要编写一个程序用来追踪大学里的师生情况。师生有很多共同的特征比如名字,年龄和地址等。
他们还拥有一些不同的特征例如教师的薪水课程假期,学生的成绩学费等。
你可以分别为师生创建两个独立的类并分别处理之,但增加一个共同的特征意味两个类都要增加。很快这种设计方式就会变的笨重臃肿。
更好的办法是创建一个称为SchoolMember的通用类,然后让教师和学生类分别继承它即成为SchoolMember的子类型。之后再增加各自的专有特征。
这样做有很多优点,我们为SchoolMember增加或修改任何功能都会自动反映到它的子类型中。
例如,你可以为教师和学生类增加一个ID卡字段,这只需简单的将其加入SchoolMember即可。不过子类型中的改变不会影响到其他子类型
另一个好处是如果你能够以SchoolMember对象引用教师或学生对象,这对于统计学校人数之类的情形很有用。
这被称作多态,在任何需要父类型的地方其子类型都可以被替换成父类型,即对象可以被当做父类的实例。
另外我们可以复用父类代码而无需在不同的类中重复这些代码,但使用我们先前讨论的独立类时就不得不重复了。
本例中的SchoolMember类被称为基类或超类。Teacher和Student类叫做派生类或子类。
现在我们就来看看这个范例吧。
#!/usr/bin/python
# Filename: inherit.py
class SchoolMember:
'''Represents any school member.'''
def __init__(self, name, age):
self.name = name
self.age = age
print('(Initialized SchoolMember: {0})'.format(self.name))
def tell(self):
'''Tell my details.'''
print('Name:"{0}" Age:"{1}"'.format(self.name, self.age), end="")
class Teacher(SchoolMember):
'''Represents a teacher.'''
def __init__(self, name, age, salary):
SchoolMember.__init__(self, name, age)
self.salary = salary
print('(Initialized Teacher: {0})'.format(self.name))
def tell(self):
SchoolMember.tell(self)
print('Salary: "{0:d}"'.format(self.salary))
class Student(SchoolMember):
'''Represents a student.'''
def __init__(self, name, age, marks):
SchoolMember.__init__(self, name, age)
self.marks = marks
print('(Initialized Student: {0})'.format(self.name))
def tell(self):
SchoolMember.tell(self)
print('Marks: "{0:d}"'.format(self.marks))
t = Teacher('Mrs. Shrividya', 40, 30000)
s = Student('Swaroop', 25, 75)
print() # 打印一个空行
members = [t, s]
for member in members:
member.tell() # 在Teacher和Student上都能工作
输出:
$ python inherit.py
(Initialized SchoolMember: Mrs. Shrividya)
(Initialized Teacher: Mrs. Shrividya)
(Initialized SchoolMember: Swaroop)
(Initialized Student: Swaroop)
Name:"Mrs. Shrividya" Age:"40" Salary: "30000"
Name:"Swaroop" Age:"25" Marks: "75"
范例如何工作:
为了使用继承,我们在类定义的类名后的一个元组中指定基类名。
然后我们注意到使用self变量作为参数显式的调用了基类的__init__方法,这样我们就能初始化对象的基类部分了。
牢记这点非常重要 – python不会自动调用基类的构造函数,你必须自己显式的调用它。
我们同样注意到可以通过增加类名前缀调用基类方法,然后为其传送self变量和其他任何实参。
注意当我们使用SchoolMember类的tell方法时,可以将Teacher或Student的实例当做SchoolMember的实例。
同时应该注意到程序调用的是子类型的tell方法二不是SchoolMember类的tell方法。
理解这个机制的一个思路是python永远先在实际类型中查找被调用的方法,这个例子中就是如此。
如果python没有找到被调用方法,则会在基类中逐个查找,查找顺序由类定义时在元组中指定的基类顺序决定。
另外解释一个术语 – 如果一个类继承了多个基类,则被称做多重继承。
小结
我们已经研究了类和对象的方方面面,还包括与其相关的各种术语。同样也看到了OOP的优点与陷阱。
python是一个高度OO语言,认真理解这些概念将对你大有裨益。
接下来,我们将学习如何在python中处理输入/输出与文件存取。
本文来自博客园,作者:方倍工作室,转载请注明原文链接:https://www.cnblogs.com/txw1958/archive/2012/12/10/A_Byte_of_Python3_Object_Oriented_Programming.html