欢迎光临散文网 会员登陆 & 注册

简明Python教程·面向对象编程&输入与输出

2023-02-14 08:00 作者:琉璃汐阳  | 我要投稿

上一篇专栏

面向对象编程 

在至今我们编写的所有程序中,我们曾围绕函数设计我们的程序,也就是那些能够处理数据 的代码块。这被称作面向过程(Procedure-oriented)的编程方式。还有另外一种组织起你的 程序的方式,它将数据与功能进行组合,并将其包装在被称作“对象”的东西内。在大多数情况下,你可以使用过程式编程,但是当你需要编写一个大型程序或面对某一更适合此方法的问 题时,你可以考虑使用面向对象式的编程技术。

类与对象是面向对象编程的两个主要方面。一个类(Class)能够创建一种新的类型 (Type),其中对象(Object)就是类的实例(Instance)。可以这样来类比:你可以拥有 类型 int 的变量,也就是说存储整数的变量是 int 类的实例(对象)。 

针对静态编程语言程序员的提示

请注意,即使是整数也会被视为对象( int 类的对象)。这不同于 C++ 与 Java(1.5 版之前),在它们那儿整数是原始内置类型。1

 有关类的更多详细信息,请参阅 help(int)

 C# 与 Java 1.5 程序员会发现这与装箱与拆箱(Boxing and Unboxing)概念 2 颇有相似 之处。

 对象可以使用属于它的普通变量来存储数据。这种从属于对象或类的变量叫作字段 (Field)。对象还可以使用属于类的函数来实现某些功能,这种函数叫作类的方法 (Method)。这两个术语很重要,它有助于我们区分函数与变量,哪些是独立的,哪些又是 属于类或对象的。总之,字段与方法通称类的属性(Attribute)。

字段有两种类型——它们属于某一类的各个实例或对象,或是从属于某一类本身。它们被分 别称作实例变量(Instance Variables)与类变量(Class Variables)。

 通过 class 关键字可以创建一个类。这个类的字段与方法可以在缩进代码块中予以列出。


 self 

类方法与普通函数只有一种特定的区别——前者必须有一个额外的名字,这个名字必须添加 到参数列表的开头,但是你不用在你调用这个功能时为这个参数赋值,Python 会为它提供。 这种特定的变量引用的是对象本身,按照惯例,它被赋予 self 这一名称。

尽管你可以为这一参数赋予任何名称,但是强烈推荐你使用 self 这一名称——其它的任何 一种名称绝对会引人皱眉。使用一个标准名称能带来诸多好处——任何一位你的程序的读者 能够立即认出它,甚至是专门的 IDEIntegrated Development Environments,集成开发环境)也可以为你提供帮助,只要你使用了 self 这一名称。

针对 C++/Java/C# 程序员的提示

Python 中的 self 相当于 C++ 中的指针以及 Java C# 中的 this 指针。 

你一定会在想 Python 是如何给 self 赋值的,以及为什么你不必给它一个值。一个例子或许 会让这些疑问得到解答。假设你有一个 MyClass 的类,这个类下有一个实例 myobject 。当 你调用一个这个对象的方法,如 myobject.method(arg1, arg2) 时,Python 将会自动将其转 换成 MyClass.method(myobject, arg1, arg2) ——这就是 self 的全部特殊之处所在。

这同时意味着,如果你有一个没有参数的功能,你依旧必须拥有一个参数—— self


最简单的类(Class)可以通过下面的案例来展示(保存为 oop_simplestclass.py ):

输出:

它是如何工作的 

我们通过使用 class 语句与这个类的名称来创建一个新类。在它之后是一个缩进的语句块, 代表这个类的主体。在本案例中,我们创建的是一个空代码块,使用 pass 语句予以标明。

然后,我们通过采用类的名称后跟一对括号的方法,给这个类创建一个对象(或是实例,我 们将在后面的章节中了解有关实例的更多内容)。为了验证我们的操作是否成功,我们通过 直接将它们打印出来来确认变量的类型。结果告诉我们我们在 Person 类的 __main__ 模块 中拥有了一个实例。 

要注意到在本例中还会打印出计算机内存中存储你的对象的地址。案例中给出的地址会与你 在你的电脑上所能看见的地址不相同,因为 Python 会在它找到的任何空间来存储对象。

 

方法

面向对象编程 100我们已经在前面讨论过类与对象一如函数那般都可以带有方法(Method),唯一的不同在于 我们还拥有一个额外的 self 变量。现在让我们来看看下面的例子(保存为 oop_method.py ):

输出:

它是如何工作的 

这里我们就能看见 self 是如何行动的了。要注意到 say_hi 这一方法不需要参数,但是依 旧在函数定义中拥有 self 变量。


 __init__ 方法 

Python 的类中,有不少方法的名称具有着特殊的意义。现在我们要了解的就是 __init__ 方法的意义。

  __init__  方法会在类的对象被实例化(Instantiated)时立即运行。这一方法可以对任何你想 进行操作的目标对象进行初始化(Initialization)操作。这里你要注意在 init 前后加上的双下 划线。

 案例(保存为 oop_init.py ):

输出:

它是如何工作的 

在本例中,我们定义 __init__ 方法用以接受 name 参数(与更普遍的 self 一道)。在这 里,我们创建了一个字段,同样称为 name 。要注意到尽管它们的名字都是“name”,但这是 两个不相同的变量。虽说如此,但这并不会造成任何问题,因为点号 self.name 意味着这个 叫作“name”的东西是某个叫作“self”的对象的一部分,而另一个 name 则是一个局部变量。由 于我们已经如上这般明确指出了我们所指的是哪一个名字,所以它不会引发混乱。 

当我们在 Person 类下创建新的实例 p 时,我们采用的方法是先写下类的名称,后跟括在括号中的参数,形如: p = Person('Swaroop') 。 

我们不会显式地调用 __init__ 方法。 这正是这个方法的特殊之处所在。 

现在,我们可以使用我们方法中的 self.name 字段了,使用的方法在 say_hi 方法中已经作过说明。


类变量与对象变量 3

我们已经讨论过了类与对象的功能部分(即方法),现在让我们来学习它们的数据部分。数 据部分——也就是字段——只不过是绑定(Bound)到类与对象的命名空间(Namespace) 的普通变量。这就代表着这些名称仅在这些类与对象所存在的上下文中有效。这就是它们被 称作“命名空间”的原因。

 字段(Filed)有两种类型——类变量与对象变量,它们根据究竟是类还是对象拥有这些变量 来进行分类。

类变量(Class Variable)是共享的(Shared)——它们可以被属于该类的所有实例访问。 该类变量只拥有一个副本,当任何一个对象对类变量作出改变时,发生的变动将在其它所有 实例中都会得到体现。

 对象变量(Object variable)由类的每一个独立的对象或实例所拥有。在这种情况下,每个 对象都拥有属于它自己的字段的副本,也就是说,它们不会被共享,也不会以任何方式与其 它不同实例中的相同名称的字段产生关联。下面一个例子可以帮助你理解(保存为 oop_objvar.py ):

输出:

它是如何工作的 

这是一个比较长的案例,但是它有助于展现类与对象变量的本质。在本例中, population 属 于 Robot 类,因此它是一个类变量。 name 变量属于一个对象(通过使用 self 分配),因 此它是一个对象变量。

因此,我们通过 Robot.population 而非 self.population 引用 population 类变量。我们对 于 name 对象变量采用 self.name 标记法加以称呼,这是这个对象中所具有的方法。要记住 这个类变量与对象变量之间的简单区别。同时你还要注意当一个对象变量与一个类变量名称 相同时,类变量将会被隐藏。

 除了 Robot.popluation ,我们还可以使用 self.__class__.population ,因为每个对象都通过 self.__class__ 属性来引用它的类。

  how_many 实际上是一个属于类而非属于对象的方法。这就意味着我们可以将它定义为一个 classmethod(类方法) 或是一个 staticmethod(静态方法) ,这取决于我们是否知道我们需不需 要知道我们属于哪个类。由于我们已经引用了一个类变量,因此我们使用 classmethod(类方法)

我们使用装饰器(Decorator)how_many 方法标记为类方法。 

你可以将装饰器想象为调用一个包装器(Wrapper)函数的快捷方式,因此启用 @classmethod 装饰器等价于调用: 

你会观察到 __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 中所有的方法都是虚拟的 (Virtual)。

 

继承

面向对象编程的一大优点是对代码的重用(Reuse),重用的一种实现方法就是通过继承 (Inheritance)机制。继承最好是想象成在类之间实现类型与子类型(Type and Subtype) 关系的工具。

现在假设你希望编写一款程序来追踪一所大学里的老师和学生。有一些特征是他们都具有 的,例如姓名、年龄和地址。另外一些特征是他们独有的,一如教师的薪水、课程与假期, 学生的成绩和学费。

你可以为每一种类型创建两个独立的类,并对它们进行处理。但增添一条共有特征就意味着 将其添加进两个独立的类。这很快就会使程序变得笨重。

 一个更好的方法是创建一个公共类叫作 SchoolMember ,然后让教师和学生从这个类中继承 (Inherit),也就是说他们将成为这一类型(类)的子类型,而我们就可以向这些子类型中添 加某些该类独有的特征。

这种方法有诸多优点。如果我们增加或修改了 SchoolMember 的任何功能,它将自动反映在子类型中。举个例子,你可以通过简单地向 SchoolMember 类进行操作,来为所有老师与学生 添加一条新的 ID 卡字段。不过,对某一子类型作出的改动并不会影响到其它子类型。另一大优点是你可以将某一老师或学生对象看作 SchoolMember 的对象并加以引用,这在某些情况下 会大为有用,例如清点学校中的成员数量。这被称作多态性(Polymorphism),在任何情 况下,如果父类型希望,子类型都可以被替换,也就是说,该对象可以被看作父类的实例。

同时还需要注意的是我们重用父类的代码,但我们不需要再其它类中重复它们,当我们使用 独立类型时才会必要地重复这些代码。

在上文设想的情况中, SchoolMember 类会被称作基类(Base Class4或是超类 (Superclass)。 Teacher Student 类会被称作派生类(Derived Classes5或是子类 (Subclass)。

我们将通过下面的程序作为案例来进行了解(保存为 oop_subclass.py ):

输出:

它是如何工作的 

要想使用继承,在定义类 时我们需要在类后面跟一个包含基类名称的元组。然后,我们会注 意到基类的 __init__ 方法是通过 self 变量被显式调用的,因此我们可以初始化对象的基 类部分。下面这一点很重要,需要牢记——因为我们在 Teacher Student 子类中定义了 __init__ 方法,Python 不会自动调用基类 SchoolMember 的构造函数,你必须自己显式地 调用它。 

相反,如果我们没有在一个子类中定义一个 __init__ 方法,Python 将会自动调用基类的构 造函数。

我们会观察到,我们可以通过在方法名前面加上基类名作为前缀,再传入 self 和其余变 量,来调用基类的方法。

在这里你需要注意,当我们使用 SchoolMember 类的 tell 方法时,我们可以将 Teacher Student 的实例看作 SchoolMember 的实例。 

同时,你会发现被调用的是子类型的 tell 方法,而不是 SchoolMember tell 方法。理 解这一问题的一种思路是 Python 总会从当前的实际类型中开始寻找方法,在本例中即是如 此。如果它找不到对应的方法,它就会在该类所属的基本类中依顺序逐个寻找属于基本类的 方法,这个基本类是在定义子类时后跟的元组指定的。 

这里有一条有关术语的注释——如果继承元组(Inheritance Tuple)中有超过一个类,这种情 况就会被称作多重继承(Multiple Inheritance)。 

end 参数用在超类的 tell() 方法的 print 函数中,目的是打印一行并允许下一次打印在 同一行继续。这是一个让 print 能够不在打印的末尾打印出 \n (新行换行符)符号的小窍门。


 总结

我们已经探索了有关类和对象的各个方面,还有与它们相关的各类术语。我们还了解了面向 对象编程的益处与陷阱。Python 是高度面向对象的,从长远来看,了解这些概念对你大有帮 助。

接下来,我们将学习如何处理输入与输出,以及如何在 Python 中访问文件。

  1. 原文作 Primitive native types,沈洁元译本表达为“把整数纯粹作为类型”。Primitive type 翻译作“原始类型”,也称作“内置类型”,因此此处也可以翻译成“基本内置类型”。 ↩ 

  2. 沈洁元译本译作“封装与解封装”。 ↩

  3. 本节标题原文作 Class And Object Variables,沈洁元译本译作“类与对象的方法”。 ↩ 

  4. 沈洁元译本译作“基本类”。 ↩

  5.  沈洁元译本译作“导出类”。 ↩ 

  6. 此处的类即派生类或子类。 ↩

输入与输出 

有些时候你的程序会与用户产生交互。举个例子,你会希望获取用户的输入内容,并向用户 打印出一些返回的结果。我们可以分别通过 input() 函数与 print 函数来实现这一需求。

对于输入,我们还可以使用 str String,字符串)类的各种方法。例如,你可以使用 rjust 方法来获得一个右对齐到指定宽度的字符串。你可以查看 help(str) 来了解更多细节。

另一个常见的输入输出类型是处理文件。创建、读取与写入文件对于很多程序来说是必不可 少的功能,而我们将在本章探讨这一方面。 


用户输入内容 

将以下程序保存为 io_input.py

输出:

它是如何工作的 

我们使用切片功能翻转文本。我们已经了解了我们可以通过使用 seq[a:b] 来从位置 a 开 始到位置 b 结束来对序列进行切片 。我们同样可以提供第三个参数来确定切片的步长 (Step)。默认的步长为 1 ,它会返回一份连续的文本。如果给定一个负数步长,如 -1 , 将返回翻转过的文本。

 input() 函数可以接受一个字符串作为参数,并将其展示给用户。尔后它将等待用户输入内 容或敲击返回键。一旦用户输入了某些内容并敲下返回键, input() 函数将返回用户输入的文本。 

我们获得文本并将其进行翻转。如果原文本与翻转后的文本相同,则判断这一文本是回文

 作业练习 

要想检查文本是否属于回文需要忽略其中的标点、空格与大小写。例如,“Rise to vote, sir.”是 一段回文文本,但是我们现有的程序不会这么认为。你可以改进上面的程序以使它能够识别 这段回文吗? 

如果你需要一些提示,那么这里有一个想法…… 1


文件

你可以通过创建一个属于 file 类的对象并适当使用它的 read readline write 方法 来打开或使用文件,并对它们进行读取或写入。读取或写入文件的能力取决于你指定以何种 方式打开文件。最后,当你完成了文件,你可以调用 close 方法来告诉 Python 我们已经完 成了对该文件的使用。

 案例(保存为 io_using_file.py ): 

输出:

它是如何工作的 

首先,我们使用内置的 open 函数并指定文件名以及我们所希望使用的打开模式来打开一个 文件。打开模式可以是阅读模式( 'r' ),写入模式( 'w' )和追加模式( 'a' )。我们还 可以选择是通过文本模式( 't' )还是二进制模式( 'b' )来读取、写入或追加文本。实际 上还有其它更多的模式可用, help(open) 会给你有关它们的更多细节。在默认情况 下, open() 会将文件视作文本(text)文件,并以阅读(read)模式打开它。

在我们的案例中,我们首先采用写入模式打开文件并使用文件对象的 write 方法来写入文 件,并在最后通过 close 关闭文件。 

接下来,我们重新在阅读模式下打开同一个文件。我们不需要特别指定某种模式,因为“阅读 文本文件”是默认的。我们在循环中使用 readline 方法来读取文件的每一行。这一方法将会 一串完整的行,其中在行末尾还包含了换行符。当一个空字符串返回时,它表示我们已经到 达了文件末尾,并且通过 break 退出循环。

最后,我们最终通过 close 关闭了文件。

现在,你可以检查 poem.txt 文件的内容来确认程序确实对该文件进行了写入与读取操作。


Pickle 2

Python 提供了一个叫作 Pickle 的标准模块,通过它你可以将任何纯 Python 对象存储到一 个文件中,并在稍后将其取回。这叫作持久地(Persistently)存储对象。 

案例(保存为 io_pickle.py ):

输出:

它是如何工作的

要想将一个对象存储到一个文件中,我们首先需要通过 open 以写入(write)二进制 (binary)模式打开文件,然后调用 pickle 模块的 dump 函数。这一过程被称作封装 (Pickling)。

接着,我们通过 pickle 模块的 load 函数接收返回的对象。这个过程被称作拆封 (Unpickling)。


 Unicode 3

截止到现在,当我们编写或使用字符串、读取或写入某一文件时,我们用到的只是简单的英 语字符。

 注意:如果你正在使用 Python 2,我们又希望能够读写其它非英语语言,我们需要使用 unicode 类型,它全都以字母 u 开头,例如 u"hello world"

当我们阅读或写入某一文件或当我们希望与互联网上的其它计算机通信时,我们需要将我们 的 Unicode 字符串转换至一个能够被发送和接收的格式,这个格式叫作“UTF-8”。我们可以在 这一格式下进行读取与写入,只需使用一个简单的关键字参数到我们的标准 open 函数中:

它是如何工作的 

现在你可以忽略 import 语句,我们会在模块章节章节探讨有关它的更多细节。 

每当我们诸如上面那番使用 Unicode 字面量编写一款程序时,我们必须确保 Python 程序已经 被告知我们使用的是 UTF-8,因此我们必须将 # encoding=utf-8 这一注释放置在我们程序的 顶端。

我们使用 io.open 并提供了“编码(Encoding)”与“解码(Decoding)”参数来告诉 Python 我们正在使用 Unicode。 

你可以阅读以下文章来了解有关这一话题的更多内容:

  •  "The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets" 

  • Python Unicode Howto 

  • Pragmatic Unicode talk by Nat Batchelder 


总结

我们已经讨论了有关输入和输出的多种类型,这些内容有关文件处理,有关 pickle 模块还有 关于 Unicode

 接下来,我们将探索一些异常的概念。 

  1. 使用一个元组(你可以在这里找到一份列出所有标点符号的列表)来保存所有需要禁 用的字符,然后使用成员资格测试来确定一个字符是否应该被移除,即 forbidden = ( ! , ? , . , ...)。——原书注 ↩ 

  2. 沈洁元译本将本节标题译作“储存器”,两版原书在本节的标题相同,但是内容已大不相同。 ↩ 

  3. Unicode 有“统一码”“万国码”“国际码”等多种译名。出于交流习惯的考虑,此处全部采 用原文。 ↩ 

  4. 可能你已经注意到,在前面章节的一些程序文件中开头标注了采用 UTF-8 编码。这是 在中文版翻译过程中为了修改程序中使用三引号括起的说明性字符串,同时要保证程序 代码能被 Python 正常识别而作出的改动。 ↩

下一篇专栏


简明Python教程·面向对象编程&输入与输出的评论 (共 条)

分享到微博请遵守国家法律