通过内置对象理解 Python(五)
2021-11-03
dir
and vars
:一切皆字典
你有没有想过 Python 是如何存储对象、变量、方法等等的?我们知道所有对象都有自己的属性和方法,但是 Python 到底是如何跟踪它们的呢?
简单的答案是,所有内容都存储在字典中。 vars
方法揭示了存储在对象和类中的变量。
1 | class C: |
如你所见,与对象 c
的属性 x
和 y
存储在它自己的字典中,而方法( some_function
和 __init__
)实际上作为函数存储在类的字典中。这是有道理的,因为函数本身的代码并不会因为每个实例对象而改变,只有传给它的变量会改变。
这可以通过 c.method(x)
与 C.method(c, x)
相同的事实来证明:
1 | class C: |
它表明定义在类中的方法实际上只是一个函数,而 self
只是作为第一个参数所引用的实例对象。 语法形式 c.method(x)
可以视为 C.method(c, x)
的一种更简洁的方式。
现在有一个稍微不同的问题。 如果 vars
显示了类中的所有方法,那么为什么这种做法有效呢?
1 | class C: |
上面所使用的 __class__
属性,并没有在类 C
中定义,那么它是从哪里来的?
如果你想知道对象上哪些属性可以被访问,你可以使用 dir
函数查看:
1 | dir(c) |
那么其余的属性从何而来呢? 相关的来龙去脉稍微复杂一些,简单地说:来自于继承。
Python中的所有对象默认继承 object
类,实际上, __class__
是在 object
中定义的:
1 | '__class__' in vars(object) |
这里的输出与在 dir(c)
的输出中看到的内容相同。
关于继承,前面曾经提到过“方法解析顺序”(MRO),下面就再对其进行深入研究。以如下代码为例:
1 | class A: |
从上面可以看出来,实例对象 b
比 a
要多一个属性 z
,当调用此属性时,就是按照该 MRO 顺序在继承的类中查询。
补充知识:slots 是什么
先看 Python 的一个奇怪而有趣的行为:
1 | x = object() |
因此,出于某种原因,不能给 object
的实例增加任意属性,但是,却可以给自定义的类任意增加实例属性。为什么会这样呢?不是所有的类都继承了 object
吗?
1 | x = list() |
上面用内置对象也不行。为什么?
这就是 __slots__
的用处。首先,也可以定义一个类,不能添加实例属性,像 list
和 object
那样。
1 | class C: |
下面的解释比较长,读者选择阅读:
Python 实际上有两种在对象中存储数据的方式:一种是字典(大多数情况如此),另外一种是 C 语言中的结构体(Struct),从本质上看,结构体可以认为是是来自 Python 的元组。字典占用的内存会更多,因为它可以随意扩展,并且依赖额外的内存空间来保证快速访问数据,这就是字典的本质。 另外,结构体的大小是固定的,不能扩展,它占用尽可能少的内存,因为它将这些值一个接一个地打包,不会浪费任何空间。
Python中存储数据的这两种方式由两个对象属性 __dict__
和__slots__
反映出来。 通常,所有实例属性都存储在 __dict__
字典中,除非你定义了__slots__
属性,在这种情况下,对象只能有固定数量的预定义属性。
为了理解上面所言,举个例子:
1 | class NormalClass: |
因此,创建 __slots__
可以防止 避免 __dict__
的存在,这意味着没有字典可供添加新属性,也意味着节省内存。 基本上是这样。
(补充知识结束)
hasattr
, getattr
, setattr
and delattr
:属性助手
我们知道,在字典中,可以通过键访问键值对的值:
1 | 'property': 42} dictionary = { |
而在对象上,它是通过“ .
”操作符访问到属性的值:
1 | class C: |
你甚至可以设置和删除对象的属性:
1 | 84 C.prop = |
将字典的键值对和对象及其属性比较,两者具有很高的相似性,但字典要灵活得多,例如,可以检查字典中是否存在一个键(对应于对象的属性):
1 | d = {} |
对于实例的属性而言,类似的操作可以通过 try ... except
语句完成:
1 | class X: |
但是,这并不是常用的,更提倡使用 hasattr
函数判断对象是否含有某个属性。
1 | class X: |
下面的操作,很显然不成立:
1 | class X: |
但是,如果想让它成为现实,可以这么做:
1 | class X: |
这里使用了 getattr
函数,它能接受一个字符串为属性,并设置属性值。
setattr
和 delattr
也都有类似的功能:它们接受字符串的属性名称,并相应地设置或删除属性值。
1 | class X: |
下面试着用其中的一个函数来构建一些有意义的东西:
1 | class api: |
这里的 upload_data
函数检查实参是否有 get_value
方法,如果有,则通过此方法读取数据。符合此类要求的对象(作为实参)是:
1 | import json |
super
:用于继承
super
是 Python 引用父类的方式,例如,为了使用父类的方法而使用 super
。
以下面的类为例,它的作用是实现两项相加。
1 | class Sum: |
这个类的应用非常简单:
1 | 2, 3) s = Sum( |
现在又创建了类 DoubleSum
,并且这个类继承了 Sum
类。在 DoubleSum
类中,有与父类相同的方法 perform
,但是它的返回值是两项和的 2 倍。这时候如果不断算写重复代码,而是依然要首先调用父类中的 perform
方法计算两项和,就可以用 super
方法实现:
1 | class DoubleSum(Sum): |
这样做,不需要重复已经定义过的东西,比如 __init__
方法,也不用重复写求和逻辑,只需要在继承父类的基础上调用父类方法即可。
1 | 3, 5) d = DoubleSum( |
super
函数也不只用在类中,其他地方也可以使用,比如:
1 | super(int) |
但说实话,我不明白这些有什么用。 如果你知道,请在评论中告诉我,感谢。
property
, classmethod
and staticmethod
:方法的装饰器
这三个是对类中方法的三个装饰器:
property
:当你想要在类中对属性通过
getter
或setter
进行读写时,就要使用装饰器@property
。在此情形下,可以通过getter
或setter
对读写过程给予干涉。通过
property
装饰器,将属性转换为一组方法来实现的:一个方法在访问属性时运行,另一个方法尝试更改属性值时运行。看一个例子,我们试图确保一个学生的
marks
属性总是被设置为正数,因为学习成绩不能是负数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Student:
def __init__(self):
self._marks = 0
def marks(self):
return self._marks
def marks(self, new_value):
# Doing validation
if new_value < 0:
raise ValueError('marks cannot be negative')
# before actually setting the value.
self._marks = new_value运行这段代码:
1
2
3
4
5
6
7
8student = Student()
student.marks
0
85 student.marks =
student.marks
85
-10 student.marks =
ValueError: marks cannot be negativeclassmethod
:装饰器
@classmethod
用在方法上,使其成为类方法。这样一来,它就获得对类对象的引用,而不是对实例 (self
)的引用。比如,在类里面创建一个返回类名称的方法:
1
2
3
4
5
6
7
8class C:
@classmethod
def class_name(cls):
return cls.__name__
...
x = C()
x.class_name
'C'staticmethod
:装饰器
@staticmethod
用于将方法转换为静态方法:相当于位于类中的函数,独立于任何类或对象属性。 使用@staticmethod
后的方法就不必在参数列表中的第一个位置使用self
参数。We could make one that does some data validation for example:
下面的应用中是利用
@staticmethod
进行数据验证:1
2
3
4
5class API:
def is_valid_title(title_text):
"""Checks whether the string can be used as a blog title."""
return title_text.istitle() and len(title_text) < 60
这些内置函数是使用一个非常高级的主题 descriptors(描述符)创建的。 坦率地说,描述符是一个非常高级的话题,如果在这里试图涵盖它,没有任何用处,因为它只是和已告知的内容有所关联。
list
, tuple
, dict
, set
and frozenset
:容器
Python 中的“容器”指的是一类数据结构,在这类数据结构中可以保存任意数量的成员。
Python 有5种基本容器类型:
list
:有序的索引容器。每个元素都有一个特定的索引。列表是可变的,即:可以在任何时候添加或删除成员。1
2
3
4
5
6
7
8
9
10
1110, 20, 30] # Creates a list with 3 items my_list = [
0] # Indexes start with zero my_list[
10
1] # Indexes increase one by one my_list[
20
40) # Mutable: can add values my_list.append(
my_list
[10, 20, 30, 40]
0] = 50 # Can also reassign indexes my_list[
my_list
[50, 20, 30, 40]tuple
:像列表一样有序和,但有一个关键区别: 它们是不可变的,这意味着一旦创建了元组对象,就不能添加或删除其中的成员。1
2
3
4
5
6
71, 2, 3) some_tuple = (
0] # Indexable some_tuple[
1
4) # But NOT mutable some_tuple.append(
AttributeError: ...
0] = 5 # Cannot reassign an index as well some_tuple[
TypeError: ...dict
:以键值对为成员。1
2
3
4
5
6
7
8
9'roses': 'red', 'violets': 'blue'} flower_colors = {
'violets'] # Use keys to access value flower_colors[
'blue'
'violets'] = 'purple' # Mutable flower_colors[
flower_colors
{'roses': 'red', 'violets': 'purple'}
'daffodil'] = 'yellow' # Can also add new values flower_colors[
flower_colors
{'roses': 'red', 'violets': 'purple', 'daffodil': 'yellow'}set
:是由无序的、唯一的成员组成。1
2
3
4
5
6
7
8
9'cedar', 'bamboo', 'cedar', 'cedar', 'cedar', 'oak', 'bamboo'] forest = [
tree_types = set(forest)
tree_types
{'bamboo', 'oak', 'cedar'} # Only unique items
'oak' in tree_types
True
'oak') # Sets are also mutable tree_types.remove(
tree_types
{'bamboo', 'cedar'}frozenset
与集合相同,但它是不可变的。1
2
3
4
5
6
7
8'cedar', 'bamboo', 'cedar', 'cedar', 'cedar', 'oak', 'bamboo'] forest = [
tree_types = frozenset(forest)
tree_types
frozenset({'bamboo', 'oak', 'cedar'})
'cedar' in tree_types
True
'mahogany') # CANNOT modify tree_types.add(
AttributeError: ...
内置的 list
、tuple
和 dict
也可以用来创建这些数据结构的空实例:
1 | x = list() |
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
关注微信公众号,读文章、听课程,提升技能