Python科学计算与数据处理7:类和对象
类与对象
Python是面向对象的语言(object-oriented),同样面向对象的语言还有C++,Java等;与之相对的是面向过程的语言(procedural),例如C语言。前面的教程中,我们也主要是采用了面向过程的编程方式,在这一节中,将为大家介绍面向对象的编程方法,其实现途径就是使用类(class)和对象(object)。
在面向对象的编程中,我们主要处理的不是前面介绍的那样传统的一行行的代码,而是来模拟现实世界中的不同的事物,即对象。一个对象可以有属性和行为,也可以和其他对象相联系和发生相互作用。例如,一只小狗可以有属性(年龄、体重等)和行为(吠、跑、打滚等)。面向对象的编程方式基于以下四个核心特征:
- 多态(Polymorphism)
- 继承(Inheritance)
- 抽象(Abstraction)
- 封装(Encapsulation)
核心概念
这里我们简要介绍一下上述这些重要的概念。
- 类:类是创建对象的蓝图,我们可以把类理解为一个生产对象的工厂。类提供了创建对象的模板,通过方法来指定对象的行为,通过属性来指定其状态。
- 对象:对象就是类的一个实例,一个对象可以有状态(属性)和行为。
- 继承:在面向对象的编程中,子类可以从父类继承属性和方法。继承可以分为单一继承和多重继承。单一继承是指一个子类只继承一个父类,而多重继承指一个子类可以继承多个父类。
- 多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。简单的说:就是用基类的引用指向子类的对象。在这里我们可以理解为,同一个方法有不同的输出。
- 抽象:这个不知道具体该怎么表达,直接引用一段"Learn Python in 7 Days (Mohit, Bhaskar N. Das)"里的原文“Here, we hide the necessary details and are only interested in showing the relevant details to the other intended user. Here, by other intended user we mean another software application, or another class, or other client who will be the end users of the program.”
- 封装:封装是指将类的方法的细节进行隐藏,对外界来说,类就像一个“黑盒子”。封装是保证软件部件具有优良的模块性的基础,封装的目标就是要实现软件部件的“高内聚、低耦合”,防止程序相互依赖性而带来的变动影响。
本节的部分内容直接参考了菜鸟教程。
创建类
Python中创建类的语法非常简单:
class <class name >(<parent class name>):
'''statements'''
<method definition-1>
<method definition-n>
空类
空类是最简单的类,我们在这里创建一个空类:
class Student():
'''An empty class.'''
pass
Student
<class __main__.Student at 0x7f1e7803c668>
上面这个类中没有任何内容,用pass
关键字来起到一个占位的作用,Student后面的括号也可以省略,第一行三撇号'''
里面的语句为类的文档(可选)。类可以看作是创建实例的蓝图,即使是空类也可以。实际上,空类在我们后面做科研数据处理的过程中有很重要的应用,它可以作为将要分析的数据的一个容器,我们后面会讲到。下面我们就用空类Student
来创建实例:
student1 = Student()
student2 = Student()
print student1
print student2
<__main__.Student instance at 0x7fe3507474d0>
<__main__.Student instance at 0x7fe3507472d8>
从上面的输出结果我们看到所创建的两个实例(instance),而且这两个实例在内存中存储在不同的地址上。
实例变量
实例变量是指某个具体实例(对象)特有的变量。例如,针对上面创建的空类的两个实例,我们可以分别赋予它们一些变量:
# define variables for L_obj1
student1.name = 'da zhuang'
student1.gender = 'male'
# define variables for L_obj2
student2.name = 'xiao hong'
student2.gender = 'female'
student2.age = 20
print student1.__dict__.keys()
print student2.__dict__.keys()
['gender', 'name']
['gender', 'age', 'name']
在上面的例子中,我们分别为student1
和student2
两个实例定义了变量,其中student1
有两个变量name
和gender
,而student2
还多了age
变量。上面代码中的__dict__
是Python类的内置属性,它是一个存储了类的所有方法或对象的所有属性键值对的字典。
虽然上面的例子显示了Python定义实例变量的灵活性,但是这种方式没有充分利用到面向对象编程的优势。实际上,我们可以在定义类的时候给定变量,从而使得所有的实例都同时具有这些变量,我们将在下面介绍。
类的变量和方法
__init__
方法
__init__
方法是类的一种特殊方法,被陈为类的构造函数或初始化方法,当创建这个类的实例时就会自动调用该方法。下面我们重新定义一下Student
类,以演示__init__
方法的使用:
class Student:
'''所有学生的基类'''
def __init__(self, name, gender, age):
self.name = name
self.gender = gender
self.age = age
student1 = Student('da zhuang', 'male', 21)
student2 = Student('xiao hong', 'famale', 20)
print student1.__dict__.keys()
print student2.__dict__.keys()
['gender', 'age', 'name']
['gender', 'age', 'name']
在上面的代码中,我们利用__init__
方法定义了类的三个变量name
,gender
和age
,这些变量被自动应用于类的所有实例。self
代表类的实例,是在定义类方法时必须的,虽然在调用时不必为self
传入任何参数。
当然,与普通的函数类似,我们也可以为这些变量指定初始值,例如:
class Student:
'''所有学生的基类'''
def __init__(self, name, gender, age=23):
self.name = name
self.gender = gender
self.age = age
student1 = Student('da zhuang', 'male')
print student1.__dict__
{'gender': 'male', 'age': 23, 'name': 'da zhuang'}
自定义方法
类的方法与普通函数类似,只有一个特别之处,即需要一个额外的第一个参数,就是我们上面讲的self
参数(按惯例使用这个名称,但不是必须的,可以随意指定其他名称)。例如,我们将上述Student
类加入一个自定义的方法:
class Student:
'''所有学生的基类'''
def __init__(self, name, gender, age=23):
self.name = name
self.gender = gender
self.age = age
def report(self):
print "%s is a %d years old %s student." % (self.name.title(), self.age, self.gender)
student1 = Student('da zhuang', 'male', 21)
student2 = Student('xiao hong', 'female', 20)
student1.report()
student2.report()
Da Zhuang is a 21 years old male student.
Xiao Hong is a 20 years old female student.
类的属性
上面类中定义的类变量即为类的实例的属性,可以通过点号.
访问,我们上面的例子中已经用到了。这里介绍一下类的内置属性,一共有一下几个:
-
__dict__
: 类的属性(包含一个字典,由类的数据属性组成) -
__doc__
:类的文档字符串 -
__name__
: 类名 -
__module__
: 类定义所在的模块(类的全名是'main.className',如果类位于一个导入模块mymod中,那么className.module 等于 mymod) -
__bases__
: 类的所有父类构成元素(包含了一个由所有父类组成的元组)
例如:
print "Student.__doc__:", Student.__doc__
print "Student.__name__:", Student.__name__
print "Student.__module__:", Student.__module__
print "Student.__bases__:", Student.__bases__
print "Student.__dict__:", Student.__dict__
Student.__doc__: 所有学生的基类
Student.__name__: Student
Student.__module__: __main__
Student.__bases__: ()
Student.__dict__: {'report': <function report at 0x7f1e70f29aa0>, '__module__': '__main__', '__doc__': '\xe6\x89\x80\xe6\x9c\x89\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe5\x9f\xba\xe7\xb1\xbb', '__init__': <function __init__ at 0x7f1e70f29938>}
注意对类调用__dict__
属性和对类的实例调用__dict__
效果是不一样的。对类调用时会列出类的方法和属性,但是对实例调用时,只会列出实例的属性,而不会列出方法。
print Student.__dict__
print student1.__dict__
{'report': <function report at 0x7f1e70f29aa0>, '__module__': '__main__', '__doc__': '\xe6\x89\x80\xe6\x9c\x89\xe5\xad\xa6\xe7\x94\x9f\xe7\x9a\x84\xe5\x9f\xba\xe7\xb1\xbb', '__init__': <function __init__ at 0x7f1e70f29938>}
{'gender': 'male', 'age': 21, 'name': 'da zhuang'}
私有属性和方法
类的私有属性
__private_attrs
:两个下划线开头,声明该属性为私有,不能在类的外部被使用或直接访问。在类内部的方法中使用时 self.__private_attrs
。
类的私有方法
__private_method
:两个下划线开头,声明该方法为私有方法,不能在类的外部调用。在类的内部调用self.__private_methods
。
单下划线、双下划线、头尾双下划线说明:
-
__foo__
: 定义的是特殊方法,一般是系统定义名字 ,类似__init__()
之类的。 -
_foo
: 以单下划线开头的表示的是protected类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于from module import *
-
__foo
: 双下划线的表示的是私有类型(private)的变量, 只能是允许这个类本身进行访问了。
类的继承
面向对象的编程带来的主要好处之一是代码的重用,实现这种重用的方法之一是通过继承机制。通过继承创建的新类称为子类或派生类,被继承的类称为基类、父类或超类。通过继承,子类自动继承父类的所有方法和属性。创建子类的语法如下:
class DerivedClassName(BaseCalssName):
<statement-1>
...
...
<statement-N>
单一继承
只从一个父类来继承,例如:
class Student:
'''所有学生的基类'''
def __init__(self, name, gender, age=23):
self.name = name
self.gender = gender
self.age = age
def report(self):
print "%s is a %d years old %s student." % (self.name.title(), self.age, self.gender)
class Boy(Student):
'''Boy类继承自Student类'''
def play(self):
print '%s is playing soccer.' % self.name.title()
boy1 = Boy('da zhuang', 'male', 12)
boy1.report()
boy1.play()
Da Zhuang is a 12 years old male student.
Da Zhuang is playing soccer.
多重继承
也可以从多个父类继承,例如:
class A:
def a_print(self):
print 'This is class A.'
class B:
def b_print(self):
print 'This is class B.'
class C(A, B):
def c_print(self):
print 'This is class C.'
c = C()
c.a_print()
c.b_print()
c.c_print()
This is class A.
This is class B.
This is class C.
方法重写
如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法:
class Parent: # 定义父类
def myMethod(self):
print '调用父类方法'
class Child(Parent): # 定义子类
def myMethod(self):
print '调用子类方法'
c = Child() # 子类实例
c.myMethod() # 子类调用重写方法
调用子类方法
运算符重载(operator overloading)
运算符重载有时被称为魔术方法(magic methods),指的是利用一些特殊方法来改变Python运算符的行为。特殊方法由连续的两组双下划线(__
)包围,有的人也将其称为dunder方法。例如,运算符加号(+
),对不同的操作变量有不同的行为:
4 + 6, 'M' + 'R'
(10, 'MR')
从输出结果我们看到了加号运算的不同行为:对于数字,加号代表两数相加;而对于字符串,加号代表连接两个字符串。根据操作对象的不同,加号运算有着不同的行为,实际上,在后台运行的就是特殊方法。对两个整数相加,+
调用的是int.__add__()
方法;而对于字符串,则调用str.__add__()
方法。例如上面代码也可以写成:
int.__add__(4, 6), str.__add__('M', 'R')
(10, 'MR')
因此,我们可以用__add__()
方法自定义加法运算。下面我们将前面定义的Student
类的两个实例进行相加,看看会出现什么结果?
class Student:
'''所有学生的基类'''
def __init__(self, name, gender, age=23):
self.name = name
self.gender = gender
self.age = age
def report(self):
print "%s is a %d years old %s student." % (self.name.title(), self.age, self.gender)
student1 = Student('da zhuang', 'male', 21)
student2 = Student('xiao hong', 'female', 20)
student1 + student2
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-44-c9dbee72f74f> in <module>()
12 student2 = Student('xiao hong', 'female', 20)
13
---> 14 student1 + student2
TypeError: unsupported operand type(s) for +: 'instance' and 'instance'
我们看到错误提示unsupported operand type(s) for+: 'instance' and 'instance'
,即加号运算符不支持两个实例来进行运算。这时,我们就可以在类中对加号运算符进行重载,以使其支持两个实例的相加。比如,我们可以定义两个实例相加的运算为两个实例中age
属性的相加。
class Student:
'''所有学生的基类'''
def __init__(self, name, gender, age=23):
self.name = name
self.gender = gender
self.age = age
def __add__(self, other):
'''重载加法运算'''
return self.age + other.age
def report(self):
print "%s is a %d years old %s student." % (self.name.title(), self.age, self.gender)
student1 = Student('da zhuang', 'male', 21)
student2 = Student('xiao hong', 'female', 20)
student1 + student2
41
从输出可见,加号实现了对两个实例的相加。
以上是关于类的一些基本内容,当然类的应用还有很多知识需要学习,这是面向对象编程的核心,如果大家在以后的使用中遇到问题,可以从网上寻找对应的解决方法。