Python基础入门自学——8

面向对象编程

面向对象编程——Object Oriented Programming,简称OOP。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。面向过程把大块函数通过切割成小块函数来降低系统的复杂度。面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

面向对象程序设计的一个关键性观念是将数据以及对数据的操作封装在一起,组成一个相互依存、不可分割的整体,即对象。对于相同类型的对象进行分类、抽象后,得出共同的特征而形成了类,面向对象程序设计的关键就是如何合理地定义和组织这些类以及类之间的关系。

Python中,所有数据类型都为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。Python中对象的概念很广泛,Python中的一切内容都可以称为对象,除了数字、字符串、列表、元组、字典、集合、range对象、zip对象等等,函数也是对象,类也是对象。创建类时用变量形式表示的对象属性称为数据成员,用函数形式表示的对象行为称为成员方法,成员属性和成员方法统称为类的成员。

以打印学生的成绩表为例:面向对象的程序设计思想,首选思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有name和score这两个属性(Property)。如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score消息,让对象自己把自己的数据打印出来。

类(Class)和实例(Instance)的概念:Class是一种抽象概念,比如我们定义的Class——Student,是指学生这个概念,而实例(Instance)则是一个个具体的Student,比如,zhangsan和lisi是两个具体的Student。所以,面向对象的设计思想是抽象出Class,根据Class创建Instance。面向对象的抽象程度又比函数要高,因为一个Class既包含数据,又包含操作数据的方法。

类提供了一种组合数据和功能的方法。 创建一个新类意味着创建一个新的对象类型,从而允许创建一个该类型的新实例 。 每个类的实例可以拥有保存自己状态的属性。 一个类的实例也可以有改变自己状态的(定义在类中的)方法。必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

Python 作用域和命名空间
在学习类之前,首先要了解 Python 的作用域规则。类定义对命名空间有一些巧妙的技巧,需要知道作用域和命名空间如何工作才能完全理解正在发生的事情。从一些定义开始:

namespace (命名空间)是一个从名字到对象的映射

对象具有个性,多个名称(在多个作用域内)可以绑定到同一个对象。 这在其他语言中称为别名。 在处理不可变的基本类型(数字,字符串,元组)时可以安全地忽略它。 但是,别名对涉及可变对象,如列表,字典和大多数其他类型的 Python 代码的语义可能会产生惊人的影响。 通常这样做是为了使程序受益,因为别名在某些方面表现得像指针。 例如,传递一个对象的代价很小,因为实现只传递一个指针;如果函数修改了作为参数传递的对象,调用者将看到更改。

大部分命名空间当前都由 Python 字典实现,一般情况下基本不会去关注它们(除了要面对性能问题时),而且也有可能在将来更改。 下面是几个命名空间的例子:存放内置函数的集合(包含 abs() 这样的函数,和内建的异常等);模块中的全局名称函数调用中的局部名称。 从某种意义上说,对象的属性集合也是一种命名空间的形式。 关于命名空间的重要一点是,不同命名空间中的名称之间绝对没有关系;例如,两个不同的模块都可以定义一个 maximize 函数而不会产生混淆 --- 模块的用户必须在其前面加上模块名称。

把任何跟在一个点号之后的名称都称为属性 --- 例如,在表达式 z.real 中,real 是对象 z 的一个属性。按严格的说法,对模块中名称的引用属于属性引用:在表达式 modname.funcname 中,modname 是一个模块对象而 funcname 是它的一个属性。在此情况下在模块的属性和模块中定义的全局名称之间正好存在一个直观的映射:它们共享相同的命名空间! 存在一个例外模块对象有一个秘密的只读属性 __dict__,它返回用于实现模块命名空间的字典__dict__ 是属性但不是全局名称。 使用这个将违反命名空间实现的抽象,应当仅被用于事后调试器之类的场合。

属性可以是只读或者可写的。如果为后者,那么对属性的赋值是可行的。模块属性是可以写,你可以写出 modname.the_answer = 42 。可写的属性同样可以用 del 语句删除。例如, del modname.the_answer 将会从名为 modname 的对象中移除 the_answer 属性。

不同时刻创建的命名空间拥有不同的生存期。包含内置名称的命名空间是在 Python 解释器启动时创建的,永远不会被删除。模块的全局命名空间在模块定义被读入时创建;通常,模块命名空间也会持续到解释器退出被解释器的顶层调用执行的语句,从一个脚本文件读取或交互式地读取,被认为是 __main__ 模块调用的一部分,因此它们拥有自己的全局命名空间。(内置名称实际上也存在于一个模块中;这个模块称作 builtins 。)

一个函数的本地命名空间在这个函数被调用时创建,并在函数返回或抛出一个不在函数内部处理的错误时被删除。当然,每次递归调用都会有它自己的本地命名空间。

一个作用域是一个 Python 程序可直接访问命名空间的文本区域。 这里的 “可直接访问” 意味着对名称的非限定引用会尝试在命名空间中查找名称。

虽然作用域是静态地确定的,但它们会被动态地使用。 在执行期间的任何时刻,会有3或4个命名空间可被嵌套的作用域直接访问:

● 最先搜索的最内部作用域包含局部名称
● 从最近的封闭作用域开始搜索的任何封闭函数的作用域包含非局部名称,也包括非全局名称
● 倒数第二个作用域包含当前模块的全局名称
● 最外面的作用域(最后搜索)是包含内置名称的命名空间

如果一个名称被声明为全局变量,则所有引用和赋值将直接指向包含该模块的全局名称的中间作用域。 要重新绑定在最内层作用域以外找到的变量,可以使用 nonlocal 语句声明为非本地变量。 如果没有被声明为非本地变量,这些变量将是只读的(尝试写入这样的变量只会在最内层作用域中创建一个新的局部变量,而同名的外部变量保持不变)。

通常,当前局部作用域将(按字面文本)引用当前函数的局部名称。 在函数以外,局部作用域将引用与全局作用域相一致的命名空间:模块的命名空间。 类定义将在局部命名空间内再放置另一个命名空间

重要的是应该意识到作用域是按字面文本来确定的:在一个模块内定义的函数的全局作用域就是该模块的命名空间,无论该函数从什么地方或以什么别名被调用。 另一方面,实际的名称搜索是在运行时动态完成的 --- 但是,Python 正在朝着“编译时静态名称解析”的方向发展,因此不要过于依赖动态名称解析! (事实上,局部变量已经是被静态确定了。)

Python 的一个特殊规定是这样的 -- 如果不存在生效的 global 或 nonlocal 语句 -- 则对名称的赋值总是会进入最内层作用域赋值不会复制数据 --- 它们只是将名称绑定到对象。 删除也是如此:语句 del x 会从局部作用域所引用的命名空间中移除对 x 的绑定。 事实上,所有引入新名称的操作都是使用局部作用域:特别地,import 语句和函数定义会在局部作用域中绑定模块或函数名称。

global 语句可被用来表明特定变量生存于全局作用域并且应当在其中被重新绑定;nonlocal 语句表明特定变量生存于外层作用域中并且应当在其中被重新绑定。

一般有三种命名空间:
● 内置名称(built-in names), Python 语言内置的名称,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。
● 全局名称(global names),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
● 局部名称(local names),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)

命名空间查找顺序:  Python 的查找顺序为:局部的命名空间去 -> 全局命名空间 -> 内置命名空间

命名空间的生命周期:命名空间的生命周期取决于对象的作用域,如果对象执行完成,则该命名空间的生命周期就结束。因此,无法从外部命名空间访问内部命名空间的对象。

有四种作用域:
●  L(Local):最内层,包含局部变量,比如一个函数/方法内部。
●  E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
●  G(Global):当前脚本的最外层,比如当前模块的全局变量。
●  B(Built-in): 包含了内建的变量/关键字等。,最后被搜索

规则顺序: L –> E –> G –> B。在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内置中找。

Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问。

作用域和命名空间示例:

重启一个干净的python交互窗口:

 局部赋值(这是默认状态)不会改变 scope_test 对 spam 的绑定(即do_local没有改变scope_test对spam的绑定)。 nonlocal 赋值会改变 scope_test 对 spam 的绑定,而 global 赋值会改变模块层级的绑定。

您还可以在 global 赋值之前看到之前没有 spam 的绑定。相当于全局变量可以在局部作用域(函数内部)声明定义,这会不会有什么问题???

初探类
类引入了一些新语法,三种新对象类型和一些新语义。

类定义语法
最简单的类定义看起来像这样:
class ClassName:
    <statement-1>
    .   .    .
    <statement-N>

定义类是通过class关键字:

class Student(object):
    pass
class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

类定义与函数定义 (def 语句) 一样必须被执行才会起作用。 (可以尝试将类定义放在 if 语句的一个分支或是函数的内部。)

在实践中,类定义内的语句通常都是函数定义,但也允许有其他语句。 在类内部的函数定义通常具有一种特别形式的参数列表,这是方法调用的约定规范所指明的 。

当进入类定义时,将创建一个新的命名空间,并将其用作局部作用域 --- 因此,所有对局部变量的赋值都是在这个新命名空间之内。 特别的,函数定义会绑定到这里的新函数名称。

当(从结尾处)正常离开类定义时,将创建一个 类对象。 这基本上是一个包围在类定义所创建命名空间内容周围的包装器; 原始的(在进入类定义之前起作用的)局部作用域将重新生效,类对象将在这里被绑定到类定义头所给出的类名称 。如上例的Student。

类对象
类对象支持两种操作:属性引用和实例化。

属性引用使用Python中所有属性引用所使用的标准语法: obj.name。 有效的属性名称是类对象被创建时存在于类命名空间中的所有名称。 因此,如果类定义是这样的:

那么 MyClass.i 和 MyClass.f 就是有效的属性引用,将分别返回一个整数和一个函数对象。 类属性也可以被赋值,因此可以通过赋值来更改 MyClass.i 的值。 __doc__ 也是一个有效的属性,将返回所属类的文档字符串: "A simple example class"。

类的实例化使用函数表示法。 可以把类对象视为是返回该类的一个新实例的不带参数的函数。 举例来说(假设使用上述的类):

x = MyClass()

创建类的新实例并将此对象分配给局部变量 x。

这里对于类的定义,有几种不同的样式,class ClassName:、class ClassName():、class ClassName(object):,这几种都是等价的,都是默认继承object类创建新类,当然也可以将object换成其他的,那就是继承其他类创建新类。

实例化操作(“调用”类对象,就是调用时在类名称后加括号)会创建一个空对象,创建实例是通过类名+()实现的。

变量x指向的就是一个MyClass的实例,后面的0x000000000249EFD0是内存地址,每个object(对象,即实例化的实例)的地址都不一样,而MyClass本身则是一个类。(个人感觉对象和实例是一个概念,类是一个模板,对象或者说实例是通过模板制造出的具体产品)

可以自由地给一个实例变量绑定属性,比如,给实例x绑定一个name属性:

可以看出,类和实例都可以绑定属性。

许多类喜欢创建带有特定初始状态的自定义实例。 为此类定义可能包含一个名为 __init__() 的特殊方法。类可以起到模板的作用,因此,可以在创建实例的时候,把一些认为必须绑定的属性强制填写进去。通过定义的__init__方法,在创建实例的时候,就把属性绑上去:

def __init__(self):
    self.data = []
当一个类定义了 __init__() 方法时,类的实例化操作会自动为新创建的类实例发起调用 __init__()。 因此在这个示例中,可以通过以下语句获得一个经初始化的新实例:

x = MyClass()

没有实例化时,是没有调用__init__函数的,所以此函数的命名空间就没有建立,也就不可能访问到其下的data变量,这个变量是__init__函数的局部变量。
当然,__init__() 方法还可以有额外参数以实现更高灵活性。 在这种情况下,提供给类实例化运算符的参数将被传递给 __init__()。

注意:特殊方法“__init__”前后分别有两个下划线!!!
注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。

有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去:

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。__init__函数也可以后期绑定,参数self的名字不是规范,只是一种人为的固定规定,增强可读性,可修改:

实例对象
能用实例对象做什么? 实例对象理解的唯一操作是属性引用。 有两种有效的属性名称:数据属性和方法。

数据属性对应于Smalltalk中的“实例变量”,以及 C++ 中的“数据成员”。 数据属性不需要声明;像局部变量一样,它们将在第一次被赋值时产生。 例如,如果 x 是上面创建的 MyClass 的实例,则以下代码段将打印数值 16,且不保留任何追踪信息:

另一类实例属性引用称为 方法。 方法是“从属于”对象的函数。(这里使用方法一词将习惯性的专指类实例对象的方法)

实例对象的有效方法名称依赖于其所属的类。 根据定义,一个类中所有是函数对象的属性都是定义了其实例的相应方法。 因此在我们的示例中,x.f 是有效的方法引用,因为 MyClass.f 是一个函数,而 x.i 不是方法,因为 MyClass.i 不是一个函数。 但是 x.f 与 MyClass.f 并不是一回事 --- 它是一个 方法对象,不是函数对象

x.f叫做实例属性的引用,x.f()是实例方法的调用。

方法对象
通常,方法在绑定后立即被调用:x.f()

在 MyClass 示例中,这将返回字符串 'hello world'。 但是,立即调用一个方法并不是必须的: x.f 是一个方法对象,它可以被保存起来以后再调用。 例如:

将继续打印 hello world,直到结束。

当一个方法被调用时到底发生了什么? 你可能已经注意到上面调用 x.f() 时并没有带参数,虽然 f() 的函数定义指定了一个参数self。 这个参数发生了什么事? 当不带参数地调用一个需要参数的函数时 Python 肯定会引发异常 --- 即使参数实际未被使用...

实际上答案是:方法的特殊之处就在于实例对象会作为函数的第一个参数被传入(所以,类是无法直接调用方法的,它没有实例对象)。 在示例中,调用 x.f() 其实就相当于 MyClass.f(x)。 总之,调用一个具有 n 个参数的方法就相当于调用再多一个参数的对应函数,这个参数值为方法所属实例对象,位置在其他参数之前。

 当一个实例的非数据属性被引用时,将搜索实例所属的类。 如果被引用的属性名称表示一个有效的类属性中的函数对象,会通过打包(指向)查找到的实例对象和函数对象到一个抽象对象的方式来创建方法对象:这个抽象对象就是方法对象。 当附带参数列表调用方法对象时,将基于实例对象和参数列表构建一个新的参数列表,并使用这个新参数列表调用相应的函数对象。

类和实例变量
一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法:

kind相当于nonlocal或global变量。

正如 前面讨论过的,共享数据可能在涉及 mutable 对象例如列表和字典的时候导致令人惊讶的结果。 例如以下代码中的 tricks 列表不应该被用作类变量,因为所有的 Dog 实例将只共享一个单独的列表:

正确的类设计应该使用实例变量:

通过上面的查看,在类Dog的命名空间中是没有name和tricks变量的,只有在实例变量,即d中才有的。因为name和tricks是在__init__中定义的,被绑定到实例对象,而类对象是没有调用__init__函数的。所以每个实例对象是有单独自己的实例变量的。

在上面的Dog类中,每个实例就拥有各自的name数据。可以通过函数来访问这些数据:

但是,既然Dog实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Dog类的内部定义访问数据的函数,这样,就把“数据”给封装起来了。这些封装数据的函数是和Dog类本身是关联起来的,称之为类的方法。

这样一来,从外部看Dog类,就只需要知道,创建实例需要给出name,而如何打印,都是在Dog类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。

封装的另一个好处是可以给Student类增加新的方法,新增的方法可以直接在实例变量上调用,不需要知道内部实现细节。

补充说明
如果同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例:

数据属性可以被方法以及一个对象的普通用户(“客户端”)所引用。 换句话说,类不能用于实现纯抽象数据类型。 实际上,在 Python 中没有任何东西能强制隐藏数据 --- 它是完全基于约定的。

客户端应当谨慎地使用数据属性 --- 客户端可能通过直接操作数据属性的方式破坏由方法所维护的固定变量。 请注意客户端可以向一个实例对象添加他们自己的数据属性而不会影响方法的可用性,只要保证避免名称冲突 --- 再次提醒,在此使用命名约定可以省去许多令人头痛的麻烦。

在方法内部引用数据属性(或其他方法!)并没有简便方式。 这实际上提升了方法的可读性:当浏览一个方法代码时,不会存在混淆局部变量和实例变量的机会。

方法的第一个参数常常被命名为 self。 这也不过就是一个约定: self 这一名称在 Python 中绝对没有特殊含义。 但是要注意,不遵循此约定会使得你的代码对其他 Python 程序员来说缺乏可读性,而且也可以想像一个 类浏览器 程序的编写可能会依赖于这样的约定。

任何一个作为类属性的函数都为该类的实例定义了一个相应方法。 函数定义的文本并非必须包含于类定义之内:将一个函数对象赋值给一个局部变量也是可以的。 例如:

现在 f, g 和 h 都是 C 类的引用函数对象的属性,因而它们就都是 C 的实例的方法 --- 其中 h 完全等同于 g。

方法可以通过使用 self 参数的方法属性调用其他方法:

上面例子中注意addtwice()方法,是调用了add()方法,方法调用方法。

方法可以通过与普通函数相同的方式引用全局名称。 与方法相关联的全局作用域就是包含其定义的模块。 (类永远不会被作为全局作用域。) 虽然很少会有充分的理由在方法中使用全局作用域,但全局作用域存在许多合法的使用场景:举个例子,导入到全局作用域的函数和模块可以被方法所使用,在其中定义的函数和类也一样。 通常,包含该方法的类本身是在全局作用域中定义的。

每个值都是一个对象,因此具有 类 (也称为 类型),并存储为 object.__class__ 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值