通过内置对象理解 Python(四)
2021-11-03
str
, bytes
, int
, bool
, float
and complex
:五个基本类型
Python有6个基本的数据类型(分明是5个,随后会解释)。 其中4个是数字,另外2个基于文本。
先看基于文本的数据类型,因为简单。
str
是 Python 中最常见的数据类型之一,使用 input
函数接受用户输入会得到字符串,Python 中的其他所有数据类型都可以转换成字符串。 这是必要的,因为所有计算机输入/输出都是文本形式的,无论是用户 I/O 还是文件 I/O ,这可能是字符串无处不在的原因。
bytes
字节类型实际上是计算中所有 I/O 的基础。 如果你了解计算机,可能会知道所有的数据都是以位和字节的形式存储和处理的——这也是终端真正的工作方式。
如果想看一下位于 input
和 print
之下的字节,需要查看 sys
模块中的 I/O 缓存: sys.stdout.buffer
和 sys.stdin.buffer
:
1 | import sys |
buffer 对象接收字节,把它们直接写入输出缓存,并返回字节数。
为了证明下面的一切都是字节,让我们看看另一个使用字节打印表情的例子:
1 | import sys |
int
是另一种广泛使用的基本数据类型。 它也是另外两种数据类型 float
和 complex
的最小单元, complex
是 float
的超类,而 float
又是 int
的超类。
这意味着所有的 int
也可以视作 float
或 complex
,但反过来说就不成立了。 类似地,所有的 float
可以作为 complex
。例如:
1 | 5 x = |
刚才提到 Python 中实际上只有5个基本数据类型,而不是6个。 这是因为,bool
实际上不是一个基本数据类型——它实际上是 int
的子类!
你可以通过查看这些类的 mro
属性来查验上述说法。
mro
意思是“方法解析顺序”,它定义了在类中所有方法的搜索顺序。 调用某个方法,首先在类本身中查找,如果找不到,则会在父类中搜索,然后是再上一级的父类,一直到顶端的 object
类。 Python中的一切都继承自 object
。 是的,Python中几乎所有东西都是对象。
看一看下面的代码:
1 | int.mro() |
所有类都有共同的“祖先” object
。 bool
是居然继承了 int
类。
或许对 bool
是 int
的子类感觉奇怪。这主要是历史原因造成的。在历史上,曾经用 0
和 1
分别表示逻辑假和真,后来,在 Python 2.2 才引入了 True
和 False
这两个 bool
类型的值,为了能兼容,把它们设计成整数的包装器。于是这个事实就被延续至今。
基于这个事实,就能在本需要整数地方,用 bool
类型替代了。
1 | import json |
这里的 indent=True
被视为 indent=1
,所以它是有效的,但我确信没有人会想要缩进1个空格。
object
:基类
object
是整个类层次结构的基类,每个类都继承了 object
。
object
类中通过特殊方法,定义了 Python 中的一些最基本的函数,比如,通过 __hash__()
可以定义函数 hash()
,等等。
1 | dir(object) |
使用 obj.x
形式访问属性会调用 __getattr__()
方法。 类似地,设置新属性和删除属性分别调用 __setattr__()
和 __delattr__()
。 对象的哈希值由 __hash__()
方法生成,对象的字符串表示形式来自 __repr__()
。
1 | # This creates an object with no properties object() |
实际上,关于 Python 的特殊方法还有很多内容,推荐参考《Python大学实用教程》或《Python 完全自学教程》,参阅:www.itdifferc.om 的说明。
type
:类工厂
如果 object
是所有对象之父,那么 type
就是所有“类”之父。即所有的对象继承 object
,所有的类来自于 type
。
type
可以用来动态地创建新类,它有两种用途:
如果给定一个参数,它返回该参数的“类型”,即:用于创建该对象的类:
1
2
3
4
5
6
75 x =
type(x)
<class 'int'>
>>> type(x) is int
True
>>> type(x)(42.0) # Same as int(42.0)
42用三个参数,可以创建一个新类,这三个参数是
name
、bases
和dict
。name
定义类的名称bases
定义所继承的类dict
定义了所有的类属性和方法。
如果要定义这样的类:
1
2
3class MyClass(MySuperClass):
def x(self):
print('x')用
type
可以定义同样的类:1
2
3
4def x_function(self):
print('x')
MyClass = type('MyClass', (MySuperClass), {'x': x_function})这也是实现
collections.namedtuple
类的一种方法,例如,它以类的名字和元组作为参数。
hash
and id
:判断相等的基础
Python的内置函数 hash
和 id
是用于判断对象相等的依据。
Python 对象默认情况下是不具有可比性的,除非它们是完全相同的。 如果你尝试创建两个 object()
对象并检查它们是否相等…
1 | x = object() |
结果总是 False
。 这是因为:事实上,object
以同一性来比较自己,即它们只是与自己相等,而不是与别的对象相等。
补充知识:哨兵
通常很少用类 object
直接创建实例,在编程中,有一种情景可以使用 object
实例,并且称此实例为“哨兵”,这是什么意思呢?
看下面的例子。假设有一个函数 what_was_passed
,用它能够实现下面所演示的功能:
1 | 42) what_was_passed( |
也就是,只要输入一个非空的值,就能对应相应的输出;如果输入的是空值,则提示 Nothing was passed
。或许觉得这个函数的代码很容易编写,比如:
1 | def what_was_passed(value=None): |
但是,如果提供的参数就是 None
——注意,None
在有的情况下,并不表示“什么也没有”,也可能具有某种意义。
1 | None) what_was_passed( |
显然这样做,并不总能实现期望。或者以省略号 ...
为参数(详见《Python 中的省略号》),也不能通过测试。
这时就需要一个“哨兵”了:
1 | __my_sentinel = object() |
现在,不论给函数提供任何职,都能看做对象,只有在不提供值时才为空。
1 | 42) what_was_passed( |
(补充知识完毕)
要理解为什么对象只与自己比较,我们必须理解关键词 is
。
Python 的 is
用于检查两个名称是否在内存中引用了完全相同的对象。 我们可以把 Python 对象想象成在空间中漂浮的盒子,把变量、数组索引等想象成指向这些对象的箭头。
举个简单的例子:
1 | x = object() |
在上面的代码中,有两个独立的对象,三个标签 x
,y
和 z
指向这两个对象:x
指向第一个对象,y
和 z
都指向另一个对象。
1 | del x |
这个操作将删除箭头 x
。 对象本身不受赋值或删除的影响,只有箭头受影响。 但现在没有指向第一个对象的箭头,这个对象的存在也就没有意义了。 所以 Python 的“垃圾回收器”将其清除。 现在我们只剩下一个 object
了。
1 | 5 y = |
现在 y
箭头被改为指向一个整数对象 5
。 z
仍然指向第二个 object
,所以该对象仍然存在。
1 | 2 z = y * |
现在 z
指向另一个新对象 10
,这个新对象存储在内存中的某个地方。 现在也没有箭头指向第二个object
了,因此该对象随后被作为垃圾收走。
为了能够验证所有上述说法,我们可以使用 id
内置函数。 id
表示对象在内存中的确切位置,用数字表示。
1 | x = object() |
相同的对象,相同的 id()
返回值,否则不同。
With object
s, ==
and is
behaves the same way:
对于 object
类的示例对象,==
和 is
具有相同的效果:
1 | x = object() |
这是因为,在 object
类中,专门定义了针对 ==
的方法 __eq__
,像下面这样:
1 | class object: |
当然,实际上 object
的实现是用 C 语言编写的。
另一方面,如果容器类型可以相互替换,则它们是相等的。典型例子是具有相同索引的相同项的列表,或者包含完全相同值的集合。
1 | 1, 2, 3] x = [ |
这些可以这样定义:
1 | class list: |
类似地,集合是无序的,所以成员的位置无关紧要,重要的是它们的“存在”:
1 | class list: |
现在,开始讨论“等价”的概念。Python 有哈希(或:散列)的概念。 任何数据块的“散列”指的都是一个看起来非常随机的预计算值,但它在某种程度上可以用于标识该数据块。
哈希有两个特定的属性:
相同的数据块总是有相同的哈希值。
即使只是稍微改变数据,也会返回一个完全不同的哈希值。
这意味着,如果两个数据块具有相同的哈希值,那么它们也很可能具有相同的值。
比较哈希值是检查“是否存在”的一种非常快速的方法。 这就是字典和集合用来快速查找内部的值的方法:
1 | import timeit |
注意:集合解决方案的运行速度比列表解决方案快数百倍!!这是因为它们使用哈希值作为“索引”的替代,如果相同哈希的值已经存储在集合或字典中,Python 可以快速检查它是否是同一项。 这个过程可以非常即时地检查哈希值是否存在。
补充知识:关于哈希
在 Python中,所有相等的数值都具有相同的哈希值,这一点往往鲜为人知。
1 | 42) == hash(42.0) == hash(42+0j) hash( |
另一个事实是不可变对象,如字符串、元组和不可变集合,通过组合各项的哈希来生成它们自己的哈希。 这使得你只需通过编写 hash
函数就可以为类创建自定义哈希函数:
1 | class Car: |
(补充知识结束)
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
关注微信公众号,读文章、听课程,提升技能