## 面向对象编程
到目前为止,在我们编写的所有程序中,主要使用函数---也就是处理数据的代码块来设计我们的程序,这叫做**面向过程**的编程方式。还有一种方式来组织你的程序,是将数据和函数组合起来打包到称为对象的东西里面,这叫做**面向对象**编程技术。在大多数情况下,你可以使用面向过程的编程,但是当写大型程序或者遇到了一些更加适合这种方法的时候,你可以使用面向对象的编程技术。
类和对象是面向对象编程的两个主要概念。一个**类**创建一个新的**类型**,而**对象**就是类的一个**实例**。例如,你可以有一个`int`的类型(类),而所有的存储整数的变量是`int`类的一个实例(对象)。
> **静态语言的程序员应该注意**
>
> 注意整型被看待为一个`int`类的对象。这一点与 C++ 和 Java ( 早于 1.5 版本)不同。在这些语言中,整型被看成一种基本数据类型。
>
> 关于`int`类的更多细节,请看help(int)。
>
> C#和Java 1.5程序员将发现这和**装箱和拆封**的概念相似。
对象可以使用**属于**对象的普通变量存储数据。属于一个对象或类的变量被称为**字段**。对象也可以通过拥有**属于**类的函数实现一定的功能。这样的函数被称为类的**方法**,这个术语是很重要的,因为它帮助我们区分函数和变量哪些是独立的,那些是属于一个类或对象的。总体而言,这些字段和方法可以被称为类的**属性**。
字段有两种类型,他们可以属于每一个类的实例(也就是对象),也可以属于类本身。它们分别被称为**实例变量**和**类变量**。
要创建一个类使用`class`的关键字,类的字段和方法在一个缩进的代码块中。
## `self`
类的方法与普通的函数相比只有一个区别 - 他们在入口参数表的第一个位置必须有一个额外的形式参数。但是当你调用这个方法的时候,你**不需要**为这个参数赋予任何一个值,Python 会提供给它。这个特别的参数指向对象**本身**,约定它的名字叫做`self`.
尽管你可以给这个参数起任何一个名字,但是这里**强烈推荐**使用`self` —— 任何其他的名字绝对会引起歧义。使用一个标准的名字有许多优点 - 如果你使用 self ,任何人阅读你的程序都会马上理解它,甚至一些特定的集成开发环境(IDE,Integrated Development Environments)还可以给你提供额外的帮助。
> **C++/Java/C#程序员要注意**
>
> 在Python中,`self`相当于C++中的指针`this`、Java和C#中的`this`引用。
你一定很想知道Python怎样给`self`赋值,为什么你不需要给它一个值。一个例子会使这个清楚。假设,你有一个称为`MyClass`的类和这个类的实例称为`myobject`。当你调用这个对象的方法`myobject.method(arg1, arg2)`时,Python将自动转换成`MyClass.method(myobject, arg1, arg2)`--这是关于`self`的所有特殊之处。
你一定好奇Python是如何给`self`赋值的,以及为什么你不必给它赋值。一个例子将会把这些问题说明清楚。假设你有一个类叫做`MyClass`以及这个类的一个对象叫做`myobject`。当你需要这样调用这个对象的方法的时候:`myobject.method(arg1, arg2)`,这个语句会被Python自动的转换成`MyClass.method(myobject, arg1, arg2)`这样的形式 —— 这就是`self`特殊的地方。
这也意味着如果你有一个不声明任何形式参数的方法,却仍然有一个入口参数 —— `self` 。
## 类
最简单的类可能如下列代码所示(保存为文件oop_simplestclass.py)。
```python
class Person:
pass # 一个空的代码块
p = Person()
print(p)
```
输出:
```shell
C:\> python3 simplestclass.py
<__main__.Person object at 0x000001DEE25BC2C8>
```
**它是如何工作的:**
我们使用`class`语句和类名创建了一个类。在这之后跟着一个代码块形成了类的主体。在这个例子中,我们使用`pass`语句声明了一个空的代码块。
之后,我们使用类的名字和一对括号创建了一个类的对象/实例(我们将在下一节学习更多的例子)。我们通过简单地打印变量`p`的方法确认这个变量类型。结果证明这是`__main__`模块中`Person`类的一个对象。
注意这个对象在内存中的地址也被显示出来。这个地址可能在你的电脑上有一个不同的值,这是由于Python只要找到空闲的内存空间就会在此处存储这个对象。
## 方法
我们已经讨论过了,类和对象可以拥有一些成员函数,它们都有一个额外的`self`参数。现在我们来看一个例子(保存为文件`oop_method.py`)。
例子(保存为 oop_method.py):
```python
class Person:
def say_hi(self):
print('嗨,你好吗?')
p = Person()
p.say_hi()
# 上面这两行也可写成Person().say_hi()
```
输出:
```shell
C:\> python oop_method.py
嗨,你好吗?
```
**它是如何工作的:**
现在我们具体的看一下`self`是如何工作的。注意到在`say_hi`方法中不包含任何参数,却在方法定义的时候仍然有一个`self`参数。
## `__init__` 方法
对Python类来说,许多方法名有特殊的含义。现在,我们来考察一个重要的`__init__`方法。
`__init__`方法将在类的对象被初始化(也就是创建)的时候自动被调用。这个方法将按照你的要求`初始化`对象(例如:给对象传递初始值)。请注意这个名字的开头和结束都是双下划线。
例子 (保存为 oop_init.py):
```python
class Person:
def __init__(self, name):
self.name = name
def say_hi(self):
print('嗨,我的名字是', self.name)
p = Person('Swaroop')
p.say_hi()
# 以上两行也可以写成 Person('Swaroop').sayHi()
```
输出:
```shell
C:\> python class_init.py
嗨,我的名字是 Swaroop
```
**它是如何工作的:**
最重要的是。请注意。我们没有显式地调用 `__init__` 方法,而是当创建类的一个实例时,通过在类名称后的括号内传递参数,这是该方法的特殊意义。
现在,我们可以在我们的方法中使用`self.name`字段了,在`say_hi`方法中已经做了演示。
这里,我们定义了`__init__`方法。这个方法除了通常的`self`变量之外,还有一个参数`name`。 这里我们创建了一个新的名为`name`的字段。注意这里有两个不同的变量却都被叫做 `name`。这是没有问题的,因为带点的标记`self.name`表示有一个叫做“name”的字段是这个类的一部分,而另外一个`name`是一个局部变量。这里我们显式地指出使用哪个变量,因此没有任何冲突。
当新建一个新的`Person`类的实例`p`的时候,我们通过调用类名的方式来创建这个新的实例,在紧跟着的括号中填入初始化参数: p = Person('Swaroop') 。
我们没有显式的调用`__init__`这个方法,这是这个方法特殊之处。
正如`say_hi`方法所示的,现在在我们的方法中可以使用`self.name`这个字段了。
## 类变量和对象变量
我们已经讨论了关于类和对象中函数的部分(即方法),现在让我们来学习关于数据的部分。数据的部分(即字段)并不是什么特别的东西,只是一些**绑定**到类或者对象命名空间的普通的变量。这意味着这些变量只在和这些类和对象有关的上下文中有效。这就是为什么他们被称作**命名空间**。
有两种类型的字段–类变量和对象变量。这是通过他们是**属于**类还是**属于**对象这一点来区分的。
**类变量**是共享的 – 他们可以通过所有这个类的对象来访问。类变量只有一份拷贝,这意味着当一个对象改变了一个类变量的时候,改变将发生在所有这个类的对象中。
**对象变量**属于每一个对象(实例)自身。在这种情况下,每一个对象都有属于它自己的字段(在不同的对象中,这些变量不是共享的,它们也并不相关,仅仅是名称相同。为了便于理解我们举个例子(保存到文件`oop_objvar.py`):
```python
class Robot:
"""表示人一机器人,有一个名字。"""
# 一个类变量,数机器人的数量
population = 0
def __init__(self, name):
"""初始化数据。"""
self.name = name
print("(初始化 {})".format(self.name))
# 当创建一个人时,机器人人口加1
Robot.population += 1
def __del__(self):
"""我将要死了。"""
print("{0} 正在被毁!".format(self.name))
Robot.population -= 1
if Robot.population == 0:
print("{}是最后一个。".format(self.name))
else:
print("还有{:d}机器人在工作。".format(Robot.population))
def say_hi(self):
"""机器人问候。
是的,它们能做作那个。"""
print("你好,我的主人叫我".format(self.name))
@classmethod
def how_many(cls):
"""打印当前人口。"""
print("我们有{:d}个机器人。".format(cls.population))
droid1 = Robot('R2-D2')
droid1.say_hi()
Robot.how_many()
droid2 = Robot('C-3PO')
droid2.say_hi()
Robot.how_many()
print("\n机器人在这能做一些工作。\n")
print("机器人已经完成了它们的工作,因此,让我们销毁它们。")
droid1.die()
droid2.die()
Robot.how_many()
```
输出:
```shell
C:\> python objvar.py
(初始化 R2-D2)
你好,我的主人叫我
我们有1个机器人。
(初始化 C-3PO)
你好,我的主人叫我
我们有2个机器人。
机器人在这能做一些工作。
机器人已经完成了它们的工作,因此,让我们销毁它们。
R2-D2 正在被毁!
还有1机器人在工作。
C-3PO 正在被毁!
C-3PO是最后一个。
我们有0个机器人。
```
**它是如何工作的:**
这是一个很长的例子,但有助于展示类和对象变量的特性。在这里,`population` 属于`Robot`类,因此是一个类变量。`name`变量属于对象(使用`self`分配),因此是一个对象变量。
由此,我们可以推测出`population`类变量应当用`Robot.population`来访问,而非`self.population`;可以推测在对象的方法之中,对象变量`name`应当使用`self.name`来访问。请记住这个类变量和对象变量之间这一个简单的区别。也请记住一个对象变量与一个类变量名字相同时,类变量将被隐藏。
除了`Robot.population`之外,我们还可以通过`self.__class__.population`来访问这个类变量,因为每一个对象都通过`self.__class__`属性指向自己的类。
`how_many`实际上是一个属于类的方法,而非属于对象的方法,这意味着我们可以使用`classmethod`或者`staticmethod`来定义它。这取决于我们是否需要知道是哪个类。因此既然我们想要声明一个类变量,让我们使用`classmethod`吧。
我们使用一个[装饰器](./more.md)来标记`how_many`方法,并将其作为一个类方法。
我们可以把装饰器想象成为一个包装函数的快捷方式(一个包裹着另外一个函数的函数,因此可以在内部函数调用之前及之后做一些事情),因此使用`@classmethod`装饰器和如下调用等价:
```python
how_many = classmethod(how_many)
```
我们注意到`__init__`方法被用作初始化一个`Robot`实例,并给这个机器人取一个名字。在这个方法之中,我们每获得一个新的机器人,就使得`population`增加 1 。此外,还注意到`self.name`变量的值会因对象的不同而不同,这就是对象变量的特征。
请记住,你**只能**通过`self`来指向同一个对象的变量和方法。这被称为`属性引用`(attribute reference) 。
在这个程序中,我们还可以看到**文档字符串**(docstrings)在类和方法值中的使用。在运行时我们可以通过`Robot.__doc__`来访问类的文档字符串以及通过`Robot.say_hi.__doc__`来访问方法的文档字符串。
在`die`方法中,我们简单的将`Robot.population`减少 1 。
所有的类成员都是公共的。只有一种情况除外:如果你使用`双下划线前缀`(例如`__privatevar`)时,Python会使用命名修饰(name-mangling) 作用于这个变量,并使其变为私有变量。
因此,只在对象和类中使用的任何变量,首先应该以一个下划线开始,其他所有的名字都是公共的,可以被其他类和对象访问。请记住这只是约定而非Python强制规定(使用双下划线除外)。
> **C++/Java/C#程序员要注意**
> 在Python中,所有类成员(包括数据成员)是公共有,所有的方法是虚拟。
## 继承
面向对象编程的主要优势之一就是代码的**重用**,一种方式是通过**继承**机制实现。继承可以被想象成为类之间的一种**类型和子类型**的关系的实现。
假设你想要写一个程序来跟踪一所大学之中的老师和同学。他们有一些共同的特征,比如名字、年龄、地址等。他们还有一些独有的特征,比如对老师来说有薪水、课程、离开等,对学生来说有成绩和学费。
你当然可以为这两种类型构建两种独立的类,并且处理它们。但是当需要添加一个共同的属性的时候,意味着需要在这两个独立的类中同时添加。这很快就会变得非常笨拙。
一个更好的办法就是构造一个共同的类`SchoolMember`,然后在让老师和学生分别**继承**这个类。换句话说,他们都是这个类型(类)的子类型,之后我们也可以为这些子类型添加独有的属性。
这种方式有很多优点,如果我们在`SchoolMember`中添加/更改任何功能,在子类中会自动反映出来。举个例子,你可以通过简单的修改`SchoolMember`类的方式来为学生和老师添加新的 ID 卡的字段。然而,子类中的变化不影响其他子类。另外一个好处就是你可以使用一个`SchoolMember`对象来指向任意一个老师或者学生的对象。这将会在某些情况下非常有用,比如统计学校中人的总数。这被称作**多态**:如果程序的某个地方期望出现的是父类型的对象,那么可以用它的子类型的对象来替代。也就是说,一个子类型的对象可以被当作父类型的对象。
此外,我们还重用了父类的代码。我们不需要在不同的类中重复这些代码,除非我们使用独立类的方式来实现。
`SchoolMember`类在这种情况下被称为**基类**或者**超类**。而`Teacher`和`Student`类被成为**派生类**或者**子类**。
我们来看看这个例子(保存为`oop_subclass.py`):
```python
class SchoolMember:
'''代表学校的任何成员。'''
def __init__(self, name, age):
self.name = name
self.age = age
print('(初始化学校成员:{})'.format(self.name))
def tell(self):
'''告诉我细节。'''
print('Name:"{}" Age:"{}"'.format(self.name, self.age), end=' ')
class Teacher(SchoolMember):
'''代表老师。'''
def __init__(self, name, age, salary):
SchoolMember.__init__(self, name, age)
self.salary = salary
print('(初始化老师:{})'.format(self.name))
def tell(self):
SchoolMember.tell(self)
print('Salary: "{0:d}"'.format(self.salary))
class Student(SchoolMember):
'''代表学生。'''
def __init__(self, name, age, marks):
SchoolMember.__init__(self, name, age)
self.marks = marks
print('(初始化学生:{})'.format(self.name))
def tell(self):
SchoolMember.tell(self)
print('Marks: "{: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()
```
输出:
```shell
C:\> python oop_subclass.py
(初始化学校成员:Mrs. Shrividya)
(初始化老师:Mrs. Shrividya)
(初始化学校成员:Swaroop)
(初始化学生:Swaroop)
Name:"Mrs. Shrividya" Age:"40" Salary: "30000"
Name:"Swaroop" Age:"25" Marks: "75"
```
**它是如何工作的:**
为了使用继承,我们在类名之后的元祖中指明父类的类名。例如:`class Teacher(SchoolMember)`。之后我们可以看到在`__init__`方法中,通过`self`变量显式的调用了父类的`__init__`方法来初始化子类对象中属于父类的部分。这非常重要,请记住 -- 既然我们在`Teacher`和`Student`子类中定义了`__init__`方法,Python不会自动的调用父类`SchoolMember`中的构造方法,你必须显式的调用。
相反的,如果我们不定义子类的`__init__`方法,Python 将会自动地调用父类中的构造方法。
我们可以把`Teacher`或者`Student`的实例当作`SchoolMember`的实例,当我们想调用父类`SchoolMember`的`tell`方法的时候,只需要简单的输入`Teacher.tell`或者`Student.tell`即可。本例中我们没有这么做,我们在每个子类之中定义了另一个新的`tell`方法( 父类`SchoolMember`的`tell`方法作为其中的一部分)来定制子类的功能。因为我们已经做了这样的工作,当我们调用`Teacher.tell`的时候, Python 将会使用子类中`tell`方法,而非父类的。然而,如果我们没有在子类中定义`tell`方法,Python 将使用父类中的方法。Python 总是首先在子类中寻找方法,如果不存在,将会按照子类声明语句中的顺序,依次在父类之中寻找(在这里我们只有一个父类,但是你可以声明多个父类)。
注意术语 -- 如果有多个类被列在继承元组之中,这就叫做**多重继承**。
在父类`tell()`方法中的`print`函数中我们使用了`end`参数,这样在打印完一句话之后,下一次打印紧接在第一句话之后,而不换行。这个技巧可以使得`print`函数在输出结束时不打印`\n`符号(换行)。
## 小结
我们已经探讨了类和对象的各个方面以及相关的术语。我们也已经领略到了面向对象编程的优势和陷阱。Python是高度面向对象,从长远看仔细理解这些概念将对你很有帮助。
接下,我们将学习如何处理输入/输出和如何在Python中访问文件。
- 开始学习
- 搭建Python开发环境
- 简明Python教程
- 致敬
- 前言
- 关于Python
- 安装
- 第一步
- 基础
- 运算符和表达式
- 控制流
- 函数
- 模块
- 数据结构
- 实战案例
- 面向对象编程
- 输入与输出
- 异常处理
- 标准库
- 更多
- 继续学习
- 附录:免费/自由和开放源码软件
- 附录: 关于
- 附录: 版本历史
- 附录: 翻译
- 附录: 参与翻译工作
- 反馈
- Django Step Sy Step
- 第一讲 从简单到复杂
- 第二讲 做加法的例子
- 第三讲 使用Template
- 第四讲 生成csv格式文件
- 第五讲 session示例
- 第六讲 wiki的例子
- 第七讲 通讯录的例子
- 第八讲 文件导入和导出
- 第九讲 通讯录的美化
- 第十讲 扩展django的模板
- 第十一讲 用户管理
- 第十二讲 搜索和部署
- 第十三讲 Ajax的实现(一)
- 第十四讲 Ajax的实现(二)
- 第十五讲 i18n的一个简单实现
- 第十六讲 自定义Calendar Tag
- 第十七讲 View,Template和Tag
- Django开发实战
- Python开发规范
- Django项目的gitignore
- 怎样配置开发环境的settings
- 如何使用Django和Vue.js构建项目
- 使用WebSocket开发网页聊天室
- 怎样使Django Admin显示中文
- 怎样使Model在Admin界面中显示中文
- 使用Django Admin怎样上传并显示图片
- 解决Django模板和Vue指令花括号冲突的问题
- 使用Django和Vue开发微信公众号
- 使用Django和Vue调用微信JSSDK开发微信支付