老齐教室

通过内置对象理解 Python(四)

str, bytes, int, bool, float and complex:五个基本类型

Python有6个基本的数据类型(分明是5个,随后会解释)。 其中4个是数字,另外2个基于文本。

先看基于文本的数据类型,因为简单。

str 是 Python 中最常见的数据类型之一,使用 input 函数接受用户输入会得到字符串,Python 中的其他所有数据类型都可以转换成字符串。 这是必要的,因为所有计算机输入/输出都是文本形式的,无论是用户 I/O 还是文件 I/O ,这可能是字符串无处不在的原因。

bytes 字节类型实际上是计算中所有 I/O 的基础。 如果你了解计算机,可能会知道所有的数据都是以位和字节的形式存储和处理的——这也是终端真正的工作方式。

如果想看一下位于 inputprint 之下的字节,需要查看 sys 模块中的 I/O 缓存: sys.stdout.buffersys.stdin.buffer

1
2
3
4
5
6
7
8
9
>>> import sys
>>> print('Hello!')
Hello!
>>> 'Hello!\n'.encode() # Produces bytes
b'Hello!\n'
>>> char_count = sys.stdout.buffer.write('Hello!\n'.encode())
Hello!
>>> char_count # write() returns the number of bytes written to console
7

buffer 对象接收字节,把它们直接写入输出缓存,并返回字节数。

为了证明下面的一切都是字节,让我们看看另一个使用字节打印表情的例子:

1
2
3
4
5
>>> import sys
>>> '🐍'.encode()
b'\xf0\x9f\x90\x8d' # utf-8 encoded string of the snake emoji
>>> _ = sys.stdout.buffer.write(b'\xf0\x9f\x90\x8d')
🐍

int 是另一种广泛使用的基本数据类型。 它也是另外两种数据类型 floatcomplex 的最小单元, complexfloat 的超类,而 float 又是 int 的超类。

这意味着所有的 int 也可以视作 floatcomplex ,但反过来说就不成立了。 类似地,所有的 float 可以作为 complex 。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> x = 5
>>> y = 5.0
>>> z = 5.0+0.0j
>>> type(x), type(y), type(z)
(<class 'int'>, <class 'float'>, <class 'complex'>)
>>> x == y == z # All the same value
True
>>> y
5.0
>>> float(x) # float(x) produces the same result as y
5.0
>>> z
(5+0j)
>>> complex(x) # complex(x) produces the same result as z
(5+0j)

刚才提到 Python 中实际上只有5个基本数据类型,而不是6个。 这是因为,bool 实际上不是一个基本数据类型——它实际上是 int 的子类!

你可以通过查看这些类的 mro 属性来查验上述说法。

mro 意思是“方法解析顺序”,它定义了在类中所有方法的搜索顺序。 调用某个方法,首先在类本身中查找,如果找不到,则会在父类中搜索,然后是再上一级的父类,一直到顶端的 object 类。 Python中的一切都继承自 object 。 是的,Python中几乎所有东西都是对象。

看一看下面的代码:

1
2
3
4
5
6
7
8
9
10
>>> int.mro()
[<class 'int'>, <class 'object'>]
>>> float.mro()
[<class 'float'>, <class 'object'>]
>>> complex.mro()
[<class 'complex'>, <class 'object'>]
>>> str.mro()
[<class 'str'>, <class 'object'>]
>>> bool.mro()
[<class 'bool'>, <class 'int'>, <class 'object'>] # Look!

所有类都有共同的“祖先” objectbool是居然继承了 int 类。

或许对 boolint 的子类感觉奇怪。这主要是历史原因造成的。在历史上,曾经用 01 分别表示逻辑假和真,后来,在 Python 2.2 才引入了 TrueFalse 这两个 bool 类型的值,为了能兼容,把它们设计成整数的包装器。于是这个事实就被延续至今。

基于这个事实,就能在本需要整数地方,用 bool 类型替代了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> import json
>>> data = {'a': 1, 'b': {'c': 2}}
>>> print(json.dumps(data))
{"a": 1, "b": {"c": 2}}
>>> print(json.dumps(data, indent=4))
{
"a": 1,
"b": {
"c": 2
}
}
>>> print(json.dumps(data, indent=True))
{
"a": 1,
"b": {
"c": 2
}
}

这里的 indent=True 被视为 indent=1 ,所以它是有效的,但我确信没有人会想要缩进1个空格。

object :基类

object 是整个类层次结构的基类,每个类都继承了 object

object 类中通过特殊方法,定义了 Python 中的一些最基本的函数,比如,通过 __hash__() 可以定义函数 hash() ,等等。

1
2
3
4
5
>>> dir(object)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__']

使用 obj.x 形式访问属性会调用 __getattr__() 方法。 类似地,设置新属性和删除属性分别调用 __setattr__()__delattr__() 。 对象的哈希值由 __hash__() 方法生成,对象的字符串表示形式来自 __repr__()

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> object()  # This creates an object with no properties
<object object at 0x7f47aecaf210> # defined in __repr__()
>>> class dummy(object):
... pass
>>> x = dummy()
>>> x
<__main__.dummy object at 0x7f47aec510a0> # functionality inherited from object
>>> hash(object())
8746615746334
>>> hash(x)
8746615722250
>>> x.__hash__() # is the same as hash(x)
8746615722250

实际上,关于 Python 的特殊方法还有很多内容,推荐参考《Python大学实用教程》或《Python 完全自学教程》,参阅:www.itdifferc.om 的说明。

type :类工厂

如果 object 是所有对象之父,那么 type 就是所有“类”之父。即所有的对象继承 object ,所有的类来自于 type

type 可以用来动态地创建新类,它有两种用途:

  • 如果给定一个参数,它返回该参数的“类型”,即:用于创建该对象的类:

    1
    2
    3
    4
    5
    6
    7
    >>> x = 5
    >>> type(x)
    <class 'int'>
    >>> type(x) is int
    True
    >>> type(x)(42.0) # Same as int(42.0)
    42
  • 用三个参数,可以创建一个新类,这三个参数是 namebasesdict

    • name 定义类的名称

    • bases 定义所继承的类

    • dict 定义了所有的类属性和方法。

    如果要定义这样的类:

    1
    2
    3
    class MyClass(MySuperClass):   
    def x(self):
    print('x')

    type 可以定义同样的类:

    1
    2
    3
    4
    def x_function(self):
    print('x')

    MyClass = type('MyClass', (MySuperClass), {'x': x_function})

    这也是实现 collections.namedtuple 类的一种方法,例如,它以类的名字和元组作为参数。

hash and id :判断相等的基础

Python的内置函数 hashid 是用于判断对象相等的依据。

Python 对象默认情况下是不具有可比性的,除非它们是完全相同的。 如果你尝试创建两个 object() 对象并检查它们是否相等…

1
2
3
4
5
6
7
>>> x = object()
>>> y = object()
>>> x == x
True
>>> y == y
True
>>> x == y # Comparing two objectsFalse

结果总是 False 。 这是因为:事实上,object 以同一性来比较自己,即它们只是与自己相等,而不是与别的对象相等。


补充知识:哨兵

通常很少用类 object 直接创建实例,在编程中,有一种情景可以使用 object实例,并且称此实例为“哨兵”,这是什么意思呢?

看下面的例子。假设有一个函数 what_was_passed ,用它能够实现下面所演示的功能:

1
2
3
4
5
6
>>> what_was_passed(42)
You passed a 42.
>>> what_was_passed('abc')
You passed a 'abc'.
>>> what_was_passed()
Nothing was passed.

也就是,只要输入一个非空的值,就能对应相应的输出;如果输入的是空值,则提示 Nothing was passed 。或许觉得这个函数的代码很容易编写,比如:

1
2
3
4
5
def what_was_passed(value=None):
if value is None:
print('Nothing was passed.')
else:
print(f'You passed a {value!r}.')

但是,如果提供的参数就是 None ——注意,None 在有的情况下,并不表示“什么也没有”,也可能具有某种意义。

1
2
>>> what_was_passed(None)
Nothing was passed.

显然这样做,并不总能实现期望。或者以省略号 ... 为参数(详见《Python 中的省略号》),也不能通过测试。

这时就需要一个“哨兵”了:

1
2
3
4
5
6
7
__my_sentinel = object()

def what_was_passed(value=__my_sentinel):
if value is __my_sentinel:
print('Nothing was passed.')
else:
print(f'You passed a {value!r}.')

现在,不论给函数提供任何职,都能看做对象,只有在不提供值时才为空。

1
2
3
4
5
6
7
8
9
10
>>> what_was_passed(42)
You passed a 42.
>>> what_was_passed('abc')
You passed a 'abc'.
>>> what_was_passed(None)
You passed a None.
>>> what_was_passed(object())
You passed a <object object at 0x7fdf02f3f220>.
>>> what_was_passed()
Nothing was passed.

(补充知识完毕)


要理解为什么对象只与自己比较,我们必须理解关键词 is

Python 的 is 用于检查两个名称是否在内存中引用了完全相同的对象。 我们可以把 Python 对象想象成在空间中漂浮的盒子,把变量、数组索引等想象成指向这些对象的箭头。

举个简单的例子:

1
2
3
4
5
6
7
>>> x = object()
>>> y = object()
>>> z = y
>>> x is y
False
>>> y is z
True

在上面的代码中,有两个独立的对象,三个标签 xyz 指向这两个对象:x 指向第一个对象,yz 都指向另一个对象。

1
>>> del x

这个操作将删除箭头 x 。 对象本身不受赋值或删除的影响,只有箭头受影响。 但现在没有指向第一个对象的箭头,这个对象的存在也就没有意义了。 所以 Python 的“垃圾回收器”将其清除。 现在我们只剩下一个 object 了。

1
>>> y = 5

现在 y 箭头被改为指向一个整数对象 5z 仍然指向第二个 object ,所以该对象仍然存在。

1
>>> z = y * 2

现在 z 指向另一个新对象 10 ,这个新对象存储在内存中的某个地方。 现在也没有箭头指向第二个object了,因此该对象随后被作为垃圾收走。

为了能够验证所有上述说法,我们可以使用 id 内置函数。 id 表示对象在内存中的确切位置,用数字表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> x = object()
>>> y = object()
>>> z = y
>>> id(x)
139737240793600
>>> id(y)
139737240793616
>>> id(z)
139737240793616 # Notice the numbers!
>>> x is y
False
>>> id(x) == id(y)
False
>>> y is z
True
>>> id(y) == id(z)
True

相同的对象,相同的 id() 返回值,否则不同。

With objects, == and is behaves the same way:

对于 object 类的示例对象,==is 具有相同的效果:

1
2
3
4
5
6
7
8
9
10
11
>>> x = object()
>>> y = object()
>>> z = y
>>> x is y
False
>>> x == y
False
>>> y is z
True
>>> y == z
True

这是因为,在 object 类中,专门定义了针对 == 的方法 __eq__ ,像下面这样:

1
2
3
class object:
def __eq__(self, other):
return self is other

当然,实际上 object 的实现是用 C 语言编写的。

另一方面,如果容器类型可以相互替换,则它们是相等的。典型例子是具有相同索引的相同项的列表,或者包含完全相同值的集合。

1
2
3
4
5
6
>>> x = [1, 2, 3]
>>> y = [1, 2, 3]
>>> x is y
False # Different objects,
>>> x == y
True # Yet, equal.

这些可以这样定义:

1
2
3
4
5
6
7
8
9
class list:
def __eq__(self, other):
if len(self) != len(other):
return False

return all(x == y for x, y in zip(self, other))

# Can also be written as:
return all(self[i] == other[i] for i in range(len(self)))

类似地,集合是无序的,所以成员的位置无关紧要,重要的是它们的“存在”:

1
2
3
4
5
6
class list:
def __eq__(self, other):
if len(self) != len(other):
return False

return all(item in other for item in self)

现在,开始讨论“等价”的概念。Python 有哈希(或:散列)的概念。 任何数据块的“散列”指的都是一个看起来非常随机的预计算值,但它在某种程度上可以用于标识该数据块。

哈希有两个特定的属性:

  • 相同的数据块总是有相同的哈希值。

  • 即使只是稍微改变数据,也会返回一个完全不同的哈希值。

这意味着,如果两个数据块具有相同的哈希值,那么它们也很可能具有相同的值。

比较哈希值是检查“是否存在”的一种非常快速的方法。 这就是字典和集合用来快速查找内部的值的方法:

1
2
3
4
5
>>> import timeit
>>> timeit.timeit('999 in l', setup='l = list(range(1000))')
12.224023487000522 # 12 seconds to run a million times
>>> timeit.timeit('999 in s', setup='s = set(range(1000))')
0.06099735599855194 # 0.06 seconds for the same thing

注意:集合解决方案的运行速度比列表解决方案快数百倍!!这是因为它们使用哈希值作为“索引”的替代,如果相同哈希的值已经存储在集合或字典中,Python 可以快速检查它是否是同一项。 这个过程可以非常即时地检查哈希值是否存在。


补充知识:关于哈希

在 Python中,所有相等的数值都具有相同的哈希值,这一点往往鲜为人知。

1
2
>>> hash(42) == hash(42.0) == hash(42+0j)
True

另一个事实是不可变对象,如字符串、元组和不可变集合,通过组合各项的哈希来生成它们自己的哈希。 这使得你只需通过编写 hash 函数就可以为类创建自定义哈希函数:

1
2
3
4
5
6
7
class Car:
def __init__(self, color, wheels=4):
self.color = color
self.wheels = wheels

def __hash__(self):
return hash((self.color, self.wheels))

(补充知识结束)


使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

关注微信公众号,读文章、听课程,提升技能