Python基础手册8——Python对象

2018-03-01 11:04:41来源:https://www.jianshu.com/p/3842eeb4f4a7作者:常大鹏人点击

分享


一、python 对象

Python 使用对象模型来存储数据,构造的任何类型的值都是一个对象(比如我们创建的整数:26,字符串:“hello world”,列表:[1, 2, 3] 等都是对象)。对象可以理解为保存在内存中的一段具有固定格式的数据,所有的 Python 对象都拥有三个特性:身份(ID),类型 和 值。


1、身份(ID)

每一个对象都有一个唯一的身份标识自己,对象一旦建立,它的ID永远不会改变,你可以认为它是该对象在内存中的地址。


内建函数 id()

内建函数 id() 函数返回一个表示对象ID的整数。





CPython实现细节:对于CPython,id(x)为x存储在内存中的地址。


操作符 is 和 is not

isis not 操作符比较两个变量所指向的对象(或者变量指向的对象)的ID是否相同,也就是比较两个变量是否指向同一个对象。







2、类型

每个对象的头部信息中都有一个类型标识符来标识这个对象的类型(实际上是一个指向对应类型对象(比如:int、str、dict等)的指针)。对象的类型决定了对象数据的特性以及支持的操作,还定义了该类型的对象可能具有的值。


type() 函数返回对象的类型(它本身也是一个对象)。与ID 一样,对象的类型也是不可以修改的。







3、值

某些值可以改变的对象称为可变的;一旦建立,值就不可以改变的对象称为不可变的。一个对象的可变性由它的类型所决定。



注意:上面三个特性在对象创建的时候就被赋值,除了值之外,其它两个特性都是只读的。


4、对象属性

某些 Python 对象有属性:数据或相关联的可执行代码(比如方法)。 Python 用点(.)标记法来访问属性。属性包括相应对象的名字等等,最常用的属性是方法,不过有一些 Python 类型也有数据属性。含有数据属性的对象包括(但不限于):类、类实例、模块、复数和文件。







5、引用计数器

每个对象的头部信息中不仅包含了标识该对象类型的类型标识符,还包含一个引用的计数器,用来计数这个对象被变量的引用次数,来决定是不是可以回收这个对象。


你可以向Python查询对一个对象的引用的次数:在 sys 模块中的 getrefcount 函数会返回对象的引用次数。







Python提供了强大的内置对象类型作为语言的组成部分,除非你有内置类型无法提供的特殊对象要处理,最好总是使用内置对象而不是使用自己的实现。


Python的内置工具是标准的,他们一般都是一致的。对于简单的任务,内置类型往往能够表现问题领域的所有结构,仅使用Python内置对象类型就能够完成很多工作。而且Python的内置对象类型优化了用C实现的数据结构算法。尽管可以实现属于自己的类似的数据类型,但往往很难达到内置数据类型所提供的性能水平。对于复杂的任务,或许仍然需要提供自己的对象,这时需要使用Python的类或C语言的接口,人工实现的对象往往建立在像列表和字典这样的内置类型的基础上。




二、标准类型(基本数据类型)
整型
布尔型
浮点型
复数型
字符串
列表
元祖
字典
这些类型是 Python 内建的基本数据类型,我们会在后面的章节来详细介绍它们。


标准类型的分类

如果让我们以最啰嗦的方式来描述标准类型,我们也许会称它们是 Python 的 “基本内建数据对象原始类型” 。


“基本”,是指这些类型都是 Python 提供的标准或核心类型。
“内建”,是由于这些类型是 Python 默认就提供的
“数据”,因为他们用于一般数据存储
“对象”,因为对象是数据和功能的默认抽象
“原始”,因为这些类型提供的是最底层的粒度数据存储
“类型”,因为他们就是数据类型

首先, 我们对数据类型进行三个方面的分类。 Python 提供了高级的数据结构,我们需要将那些原始的类型和功能强大的扩展类型区分开来。另外这有助于搞清楚某种类型应该具有什么行为。





1、存储模型

我们对类型进行分类的第一种方式, 就是看看这种类型的对象能保存多少个对象。Python的类型, 就象绝大多数其它语言一样,能容纳一个或多个值。一个能保存单个字面对象的类型我们称它为原子或标量存储。那些可容纳多个对象的类型,我们称之为容器存储。容器类型又带来一个新问题,那就是它是否可以容纳不同类型的对象。所有的 Python 容器对象都能够容纳不同类型的对象。



字符串看上去像一个容器类型,因为它“包含”字符(并且经常多于一个字符),不过由于 Python 并没有字符类型,所以字符串是一个自我包含的文字类型。


2、更新模型

另一种对标准类型进行分类的方式就是根据对象创建成功之后,它的值可不可以进行更新。可变对象允许他们的值被更新,而不可变对象则不允许他们的值被更改。







3、访问模型

根据访问我们存储的数据的方式对数据类型进行分类。在访问模型中共有三种访问方式:直接存取,顺序,和映射。


对非容器类型可以直接访问。所有的数值类型都归到这一类。


序列类型是指容器内的元素可以按从 0 开始的索引顺序访问。一次可以访问一个元素或多个元素, 也就是大家所了解的切片(slice)。 字符串, 列表和元组都归到这一类。


映射类型类似序列的索引属性,不过它的索引并不使用顺序的数字偏移量取值, 它的元素无序存放, 通过一个唯一的 key 来访问, 这就是映射类型, 它容纳的是哈希键-值对的集合。







三、其他内建类型
类型
None
文件
集合
函数
模块

这些是当你做 Python 开发时可能会用到的一些数据类型。我们在这里讨论 Type 和 None类型的使用,除此之外的其他类型我们会在后面的单独章节来详细介绍它们。


Python中所有一切都是某种类型的对象,即便是某个对象的类型!任何对象的类型都是类型为 “type” 的对象。







1、type 类型对象

对象的一系列固有行为和特性(比如支持哪些运算,具有哪些方法)必须事先定义好。从这个角度看,对象的类型正是保存这些信息的最佳位置。描述一种类型所需要的信息不可能用一个字符串来搞定,所以类型不能是一个简单的字符串,这些信息不能也不应该和数据保存在一起, 所以我们将类型定义成对象。


通过调用 type() 函数你能够得到特定对象的类型信息。





我们得到一个简洁的输出结果<class 'int'>。但是它并不是一个简简单单的告诉你 123 是个整数这样的字符串。您看到的<class 'int'>实际上是一个类型对象,碰巧它输出了一个字符串来告诉你它是个 int 型对象。


所有类型对象的类型都是 type,它也是所有 Python 类型的根和所有 Python 标准类的默认元类(metaclass)。


2、None 对象

Python 有一个特殊的类型,被称作 NoneType,它只有一个值,那就是 None。它用于表示在许多情况下不存在值,一般都用来起到一个空的占位符的作用。它不支持任何运算也没有任何内建方法。None 没有什么有用的属性,它的布尔值总是 False。



None不是意味着“未定义”,None是某些内容,而不是没有内容,他是一个真正的对象,并且有一块内存,由Python给定一个内置的名称。




四、内部类型
代码

跟踪记录
切片
省略
Xrange

我们在这里简要介绍一下这些内部类型,一般的程序员通常不会直接和这些对象打交道。
(后期补充 ... )




五、动态类型

python是动态类型的(它自动的跟踪对象的类型),Python中没有类型声明,运行的表达式的语法(创建对象时的表达式,例如:一个方括号的表达式会生成一个列表,大括号中的表达式会建立一个字典)决定了创建和使用的对象的类型。一旦创建一个对象,它就和操作集合绑定了(只可以对字符串对象进行字符串相关的操作,对列表对象进行列表相关的操作)所以Python也是强类型语言。


1、变量、对象和引用

在Python中我们使用对象模型来存储数据,使用变量(变量名)来指向我们创建的对象,我们在程序代码中使用变量名来引用他们所指向的对象。我们可以简单的认为对象(数据)就是变量的值,实际上,变量为对象的一个引用。



对于大多数编译型语言来说,变量在使用前必须先声明。但是在 Python 中,无需显式的声明变量,变量在第一次被赋值时自动声明并创建。变量一旦被赋值,您就可以通过变量名来访问它的值,之后的赋值将会改变变量的值。变量只有被创建和赋值后才能被使用,当变量出现在表达式中,它会马上被当前引用的对象所替代。





变量的创建和使用

通过下面的例子,我们直观的了解一下变量的创建和使用过程:




1、创建一个int类型的对象代表值4;
2、创建一个变量 a,如果它还没有被创建的话;
3、通过赋值运算符(=)将变量 a 指向对象4;
4、在表达式中将变量a替换为对象4;
5、对象4和对象5进行加法运算,打印运算结果的字符串格式 “9”;






图示:在运行 a = 4 后,变量a变成对象4 的一个引用,在内部,变量事实上是到对象内存空间(通过运行常量表达式 = 4而创建)的一个指针。一旦变量a被使用,Python自动跟随这个变量到对象4的链接,使用对象4参与和对象5的运算。


变量和对象

1、变量和对象保存在内存的不同部分,并通过引用(指针)相关联;
2、变量总是连接到对象,并且绝不会连接到其他变量上,但是更大的对象可能连接到其他的对象;
3、在Python内部,作为一种优化,Python预先缓存了一些不变的对象并对其进行复用;
4、对象有更复杂的结构,而不仅仅是有足够的空间表示它的值这么简单。每一个对象都有两个标准的头部信息:一个类型标识符去标识这个对象的类型;一个引用的计数器,用来决定是不是可以回收这个对象;


2、对象的动态类型——类型标识符

python是动态类型的,变量名不但无需事先声明, 而且也无需类型声明。类型的概念仅存在于对象中而不是变量名中,变量永远不会有任何的和它相关联的类型信息或约束。所以变量是通用的,它只是在一个特定的时间点,简单的引用了一个特定的对象而已。



对象知道自己的类型,每个对象都包含了一个头部信息——类型标识符(实际上是一个指向类型对象的指针),标记了这个对象的类型(例如:一个整数对象10,包含了值10以及一个头部信息,告诉python,这个是一个整数对象)。一旦创建一个对象,它就和操作集合绑定了(只可以对字符串对象进行字符串相关的操作,对列表对象进行列表相关的操作),所以Python也是强类型语言。


Python 语言中,对象的类型和内存占用都是运行时确定的,在创建也就是赋值时,解释器会根据运行的表达式的语法和右侧的操作数(例如:一个方括号的表达式会生成一个列表,大括号中的表达式会建立一个字典)来决定新对象的类型。在对象创建后,一个该对象的引用会被赋值给左侧的变量。


在代码中检验特定的类型,会破坏代码的灵活性,即限制它只能使用一种类型工作。没有这样的检测,代码也许能够使用整个范围的类型工作。在Python中,我们编写对象接口(所支持的操作)而不是类型。不关注特定的类型意味着代码会自动的适应他们中的很多类型:任何具有兼容接口的对象均能够工作,而不管它是什么对象类型。动态类型是Python语言灵活性的根源。


3、对象的垃圾收集——引用计数器

在Python中,每当一个变量名被赋予了一个新的对象,之前的那个对象占用的空间就会被回收(如果它没有被其他的变量名或对象所引用的话)。这种自动回收对象空间的技术叫做垃圾收集。


要保持追踪内存中的对象, Python 使用了引用计数这一简单技术。也就是说 Python 内部记录着所有使用中的对象各有多少引用。在Python内部,它在每个对象的头部信息中保存了一个引用计数器,计数器记录了当前指向该对象的引用的数目。一旦(并精确的在同一时间)这个计数器被设置为零,这个对象的内存空间就会被自动回收(对象的空间自动放入自由内存空间池,等待后来的对象使用)。严格来说这不是 100%正确,不过现阶段你可以就这么认为。


3.1 增加引用计数

当对象被创建并(将其引用)赋值给变量时,该对象的引用计数就被设置为 1。当同一个对象(的引用)又被赋值给其它变量时,或作为参数传递给函数, 方法或类实例时, 或者被赋值为一个窗口对象的成员时,该对象的一个新的引用,或者称作别名,就被创建(则该对象的引用计数自动加 1)。



请看以下声明:


x = 3.14
y = x

语句 x = 3.14 创建了一个浮点数对象(3.14)并将其引用赋值给 x。 x 是其第一个引用, 因此,该对象的引用计数被设置为 1。语句 y=x 创建了一个指向同一对象的别名 y(参阅图 3-2)。事实上并没有为 Y 创建一个新对象, 而是该对象的引用计数增加了 1 次(变成了 2)。这是对象引用计数增加的方式之一。还有一些其它的方式也能增加对象的引用计数, 比如该对象作为参数被函数调用或这个对象被加入到某个容器对象当中时。


3.2 减少引用计数

当对象的引用被销毁时,引用计数会减小。最明显的例子就是当引用离开其作用范围时,这种情况最经常出现在函数运行结束时,所有局部变量都被自动销毁,对象的引用计数也就随之减少。


当变量被赋值了另外一个其他对象时,原对象的引用计数也会自动减 1:


foo = 'xyz'
bar = foo
foo = 123

当字符串对象"xyz"被创建并赋值给 foo 时, 它的引用计数是 1。当增加了一个别名 bar时, 引用计数变成了 2。不过当 foo 被重新赋值给整数对象 123 时, xyz 对象的引用计数自动减 1,又重新变成了 1。其它造成对象的引用计数减少的方式包括使用 del 语句删除一个变量, 或者当一个对象被移出一个窗口对象时(或该容器对象本身的引用计数变成了 0 时)。


总结一下,一个对象的引用计数在以下情况会减少:一个本地引用离开了其作用范围。


3.3 del 语句

del 语句会删除对象的一个引用,它的语法是:


del obj1[, obj2[,... objN]]

执行 del x 删除该对象的最后一个引用, 也就是该对象的引用计数会减为0, 这会导致该对象从此“无法访问”或“无法抵达”。 从此刻起, 该对象就成为垃圾回收机制的回收对象。 注意任何追踪或调试程序会给一个对象增加一个额外的引用, 这会推迟该对象被回收的时间。


3.4 垃圾收集

像上面说的,虽然解释器跟踪对象的引用计数, 但垃圾收集器负责释放内存。垃圾收集器是一块独立代码, 它用来寻找引用计数为 0 的对象。它也负责检查那些虽然引用计数大于 0 但也应该被销毁的对象。


从技术上讲,Python的垃圾收集主要基于引用计数器,然而它也有一部分功能可以及时的检测并回收带有循环引用的对象。由于引用实现为指针,一个对象有可能会引用自身,或者引用另一个引用了自身的对象。 这说明只靠引用计数是不够的。 Python 的垃圾收集器实际上是一个引用计数器和一个循环垃圾收集器。 尽管这种情况相对 很少,由于这样的对象的引用计数器不会清除为0,必须特别对待它们。


这里对Python的垃圾收集器的介绍只适用于标准的CPython,JPython和IronPython可能使用不同的方案。


垃圾收集最直接的、可感受到的好处就是,这意味着可以在脚本中任意使用对象而不需要考虑释放内存空间,在程序运行时,Python将会清理那些不在使用的空间。Python 解释器承担了内存管理的复杂任务, 这大大简化了应用程序的编写。你只需要关心你要解决的问题,至于底层的事情放心交给 Python 解释器去做就行了。




4、共享引用

在Python中一个变量可以被赋值引用多个对象,也可以多个变量名引用了同一个对象,在Python中这叫作共享引用。


4.1 修改变量的值——变量指向的对象为不可更改类型







图示:运行赋值语句a=3,在内存空间中创建对象3和变量a,a的引用指向对象3的内存空间。运行赋值语句b=a,在内存中创建变量b,并且b的引用指向a变量引用指向的对象3。









图示:运行赋值语句a = "hello",在内存空间中创建对象“hello'”,a的引用指向对象”hello“的内存空间。


在Python中,变量总是一个指向对象的指针,而不是可改变的内存区域的标签:给一个变量赋一个新的值,并不是替换了原始的对象,而是让这个变量去引用完全不同的另一个对象。


4.2 修改变量的值——变量指向的对象为可更改类型







图示:运行赋值语句a=[1, 2, 3],在内存空间中创建对象[1, 2, 3]和变量a,a的引用指向对象[1, 2, 3]的内存空间。运行赋值语句b=a,在内存中创建变量b,并且b的引用指向a变量引用指向的对象[1, 2, 3]。运行a[0] = "hello",因为对象[1, 2, 3]为可变类型,所以直接修改对象的值。变量a和变量b指向对象的引用不变。


在列表中的元素是通过他们的位置进行读取的,所以a[0]为对象1的引用,我们修改a[0]的值,也就是将a[0]的引用指向了其他对象,并不影响a对对象[1, 2 ,3]的引用。



对于这种可变对象也就是说可在原处直接修改的对象,共享引用时需要倍加小心,因为对一个变量名的修改会影响到其他的变量。这种行为通常来说就是你所想要的,应该了解它是如何运作的,让它按照预期去工作。这也是默认的,如果你不想要这样的现象发生,需要Python拷贝对象,而不是创建引用。有很多拷贝一个列表的方法,包括内置列表函数,以及标准库的copy模块,也许最常用的方法是从头到尾的分片。




下面这种方式需要认真思考(变量c的赋值方式并没有将变量c指向变量a指向的对象,而是创建了一个新的对象):




由于Python的引用模型,在Python程序中有两种不同的方法去检查是否相等。第一种是 “ == ”操作符,测试两个被引用的对象是否有相同的值。第二种方法是“ is ”操作符,是在检查对象的同一性。如果两个变量名精确的指向同一个对象,他会返回True。实际上,is只是比较实现引用的指针,所以如果必要的话是代码中检测共享引用的一种办法。




因为Python缓存并复用了小的整数和小的字符串,所以他们并不会向我们所说的被回收。大多数种类的独享都会在不再引用时马上回收,对于那些不会被回收的,缓存机制与代码并没有什么关系。而且,我们创建两个变量赋值相同,他们会可能会指向同一个Python缓存的对象。



图示:a和b应该是 == 的,但不是 is 的,但是因为小的整数和字符串被缓存并复用了,所以is表达式告诉我们a 和b引用了同一个对象。


5、作用域

作用域定义一个代码块中变量的可见性。如果一个局部变量在一个代码块中定义,那么它的作用域就在那个代码块中。如果定义出现在函数代码块中,那么其作用域扩展到这个函数代码块包含的任何代码块中,除非某个被包含的代码块为该名称引入一个不同的绑定。


当一个变量在代码块中使用时,它使用包含它最近的作用域解析。对于一个代码块所有可见作用域的集合称做代码块的环境。


当一个变量完全找不到时,将引发一个 NameError 异常。如果当前的作用域是一个函数作用域,而且变量引用一个局部变量,这个变量在该名称使用的时候还没有绑定到一个值,则引发一个UnboundLocalError 异常。UnboundLocalError是 NameError 的子类。


如果名称绑定操作发生在代码代码块内的任何地方,则代码块内的名称的所有使用都被视为对当前代码块的引用。这可能会导致在代码块中绑定名称之前出现错误。这个规则是微妙的。Python缺少声明并允许在代码块内的任何地方进行名称绑定操作。代码代码块的局部变量可以通过扫描用于名称绑定操作的代码块的整个文本来确定。


如果 global 语句出现在代码块内,在语句中指定的名称的所有引用都是指该名称在的顶级命名空间中的绑定。名称在顶级命名空间中的解析通过搜索全局命名空间,即包含该代码块的模块的命名空间,和内建的命名空间——模块 builtins 的命名空间。首先搜索全局命名空间。如果在那里没有找到名称,则搜索 builtins 命名空间。global语句必须位于该名称的所有引用之前。


global 语句的作用域与同一代码块中的名称绑定操作相同。如果自由变量的最近的包围作用域包含全局语句,则该自由变量被视为全局变量。


nonlocal 语句使得对应的名称引用在最靠近的包含它的函数的作用域中绑定的变量。如果给定的名称在任何包含它的函数的作用域中都找不到,则在编译时刻引发 SyntaxError。


模块的命名空间在第一次导入模块时自动创建。脚本的主模块始终叫做 main 。


类定义以及 exec()eval() 的参数在名称解析的上下文中比较特殊。类定义是可以使用和定义名称的可执行语句。这些引用遵循正常的名称解析规则,除了一个例外,就是未绑定的局部变量在全局作用域中查找。类定义的命名空间成为类的属性字典。在类代码块中定义的名称的作用域限制在类代码块中;它不会延伸到方法的代码块中 —— 包括解析式和生成器表达式,因为它们是使用函数作用域实现的。也就是说下面这段代码执行会失败:


class A: 
a = 42
b = list(a + i for i in range(10))




《Python基础手册》系列:

Python基础手册 1 —— Python语言介绍
Python基础手册 2 —— Python 环境搭建(Linux)
Python基础手册 3 —— Python解释器
Python基础手册 4 —— 文本结构
Python基础手册 5 —— 标识符和关键字
Python基础手册 6 —— 操作符
Python基础手册 7 —— 内建函数
Python基础手册 8 —— Python对象
Python基础手册 9 —— 数字类型
Python基础手册10 —— 序列(字符串)
Python基础手册11 —— 序列(元组&列表)
Python基础手册12 —— 序列(类型操作)
Python基础手册13 —— 映射(字典)
Python基础手册14 —— 集合
Python基础手册15 —— 解析
Python基础手册16 —— 文件
Python基础手册17 —— 简单语句
Python基础手册18 —— 复合语句(流程控制语句)
Python基础手册19 —— 迭代器
Python基础手册20 —— 生成器
Python基础手册21 —— 函数的定义
Python基础手册22 —— 函数的参数
Python基础手册23 —— 函数的调用
Python基础手册24 —— 函数中变量的作用域
Python基础手册25 —— 装饰器
Python基础手册26 —— 错误 & 异常
Python基础手册27 —— 模块
Python基础手册28 —— 模块的高级概念
Python基础手册29 —— 包









最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台