多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
## 面向对象编程 到目前为止,在我们编写的所有程序中,主要使用函数---也就是处理数据的代码块来设计我们的程序,这叫做**面向过程**的编程方式。还有一种方式来组织你的程序,是将数据和函数组合起来打包到称为对象的东西里面,这叫做**面向对象**编程技术。在大多数情况下,你可以使用面向过程的编程,但是当写大型程序或者遇到了一些更加适合这种方法的时候,你可以使用面向对象的编程技术。 类和对象是面向对象编程的两个主要概念。一个**类**创建一个新的**类型**,而**对象**就是类的一个**实例**。例如,你可以有一个`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中访问文件。