老齐教室

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

dir and vars :一切皆字典

你有没有想过 Python 是如何存储对象、变量、方法等等的?我们知道所有对象都有自己的属性和方法,但是 Python 到底是如何跟踪它们的呢?

简单的答案是,所有内容都存储在字典中。 vars 方法揭示了存储在对象和类中的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> class C:
... some_constant = 42
... def __init__(self, x, y):
... self.x = x
... self.y = y
... def some_method(self):
... pass
...
>>> c = C(x=3, y=5)
>>> vars(c)
{'x': 3, 'y': 5}
>>> vars(C)
mappingproxy(
{'__module__': '__main__', 'some_constant': 42,
'__init__': <function C.__init__ at 0x7fd27fc66d30>,
'some_method': <function C.some_method at 0x7fd27f350ca0>,
'__dict__': <attribute '__dict__' of 'C' objects>,
'__weakref__': <attribute '__weakref__' of 'C' objects>,
'__doc__': None
})

如你所见,与对象 c 的属性 xy 存储在它自己的字典中,而方法( some_function__init__)实际上作为函数存储在类的字典中。这是有道理的,因为函数本身的代码并不会因为每个实例对象而改变,只有传给它的变量会改变。

这可以通过 c.method(x)C.method(c, x) 相同的事实来证明:

1
2
3
4
5
6
7
8
9
>>> class C:
... def function(self, x):
... print(f'self={self}, x={x}')

>>> c = C()
>>> C.function(c, 5)
self=<__main__.C object at 0x7f90762461f0>, x=5
>>> c.function(5)
self=<__main__.C object at 0x7f90762461f0>, x=5

它表明定义在类中的方法实际上只是一个函数,而 self 只是作为第一个参数所引用的实例对象。 语法形式 c.method(x) 可以视为 C.method(c, x) 的一种更简洁的方式。

现在有一个稍微不同的问题。 如果 vars 显示了类中的所有方法,那么为什么这种做法有效呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class C:
... def function(self, x): pass
...
>>> vars(C)
mappingproxy({
'__module__': '__main__',
'function': <function C.function at 0x7f607ddedb80>,
'__dict__': <attribute '__dict__' of 'C' objects>,
'__weakref__': <attribute '__weakref__' of 'C' objects>,
'__doc__': None
})
>>> c = C()
>>> vars(c)
{}
>>> c.__class__
<class '__main__.C'>

上面所使用的 __class__ 属性,并没有在类 C 中定义,那么它是从哪里来的?

如果你想知道对象上哪些属性可以被访问,你可以使用 dir 函数查看:

1
2
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__','__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__','__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__','__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__','__subclasshook__', '__weakref__', 'function']

那么其余的属性从何而来呢? 相关的来龙去脉稍微复杂一些,简单地说:来自于继承。

Python中的所有对象默认继承 object 类,实际上, __class__ 是在 object 中定义的:

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

这里的输出与在 dir(c) 的输出中看到的内容相同。

关于继承,前面曾经提到过“方法解析顺序”(MRO),下面就再对其进行深入研究。以如下代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> class A:
... def __init__(self):
... self.x = 'x'
... self.y = 'y'
...
>>> class B(A):
... def __init__(self):
... self.z = 'z'
...
>>> a = A()
>>> b = B()
>>> B.mro()
[<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
>>> dir(b)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'x', 'y', 'z']
>>> set(dir(b)) - set(dir(a)) # all values in dir(b) that are not in dir(a)
{'z'}
>>> vars(b).keys()
dict_keys(['z'])
>>> set(dir(a)) - set(dir(object))
{'x', 'y'}
>>> vars(a).keys()
dict_keys(['x', 'y'])

从上面可以看出来,实例对象 ba 要多一个属性 z ,当调用此属性时,就是按照该 MRO 顺序在继承的类中查询。


补充知识:slots 是什么

先看 Python 的一个奇怪而有趣的行为:

1
2
3
4
5
6
7
8
9
>>> x = object()
>>> x.foo = 5
AttributeError: 'object' object has no attribute 'foo'
>>> class C:
... pass
...
>>> c = C()
>>> c.foo = 5
>>> # works?

因此,出于某种原因,不能给 object 的实例增加任意属性,但是,却可以给自定义的类任意增加实例属性。为什么会这样呢?不是所有的类都继承了 object 吗?

1
2
3
>>> x = list()
>>> x.foo = 5
AttributeError: 'list' object has no attribute 'foo'

上面用内置对象也不行。为什么?

这就是 __slots__ 的用处。首先,也可以定义一个类,不能添加实例属性,像 listobject 那样。

1
2
3
4
5
6
>>> class C:
... __slots__ = ()
...
>>> c = C()
>>> c.foo = 5
AttributeError: 'C' object has no attribute 'foo'

下面的解释比较长,读者选择阅读:

Python 实际上有两种在对象中存储数据的方式:一种是字典(大多数情况如此),另外一种是 C 语言中的结构体(Struct),从本质上看,结构体可以认为是是来自 Python 的元组。字典占用的内存会更多,因为它可以随意扩展,并且依赖额外的内存空间来保证快速访问数据,这就是字典的本质。 另外,结构体的大小是固定的,不能扩展,它占用尽可能少的内存,因为它将这些值一个接一个地打包,不会浪费任何空间。

Python中存储数据的这两种方式由两个对象属性 __dict____slots__反映出来。 通常,所有实例属性都存储在 __dict__ 字典中,除非你定义了__slots__ 属性,在这种情况下,对象只能有固定数量的预定义属性。

为了理解上面所言,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> class NormalClass:
... classvar = 'foo'
... def __init__(self):
... self.x = 1
... self.y = 2
...
>>> n = NormalClass()
>>> n.__dict__
{'x': 1, 'y': 2} # Note that `classvar` variable isn't here.
>>> # That is stored in `NormalClass.__dict__`
>>> class SlottedClass:
... __slots__ = ('x', 'y')
... classvar = 'foo' # This is fine.
... def __init__(self):
... self.x = 1
... self.y = 2
... # Trying to create `self.z` here will cause the same
... # `AttributeError` as before.
...
>>> n = SlottedClass()
>>> s.__dict__
AttributeError: 'SlottedClass' object has no attribute '__dict__'
>>> s.__slots__
('x', 'y')

因此,创建 __slots__ 可以防止 避免 __dict__ 的存在,这意味着没有字典可供添加新属性,也意味着节省内存。 基本上是这样。

(补充知识结束)


hasattr, getattr, setattr and delattr :属性助手

我们知道,在字典中,可以通过键访问键值对的值:

1
2
3
>>> dictionary = {'property': 42}
>>> dictionary['property']
42

而在对象上,它是通过“ . ”操作符访问到属性的值:

1
2
3
4
5
>>> class C:
... prop = 42
...
>>> C.prop
42

你甚至可以设置和删除对象的属性:

1
2
3
4
>>> C.prop = 84
>>> C.prop
84
>>> del C.prop

将字典的键值对和对象及其属性比较,两者具有很高的相似性,但字典要灵活得多,例如,可以检查字典中是否存在一个键(对应于对象的属性):

1
2
3
4
5
6
>>> d = {}
>>> 'prop' in d
False
>>> d['prop'] = 'exists'
>>> 'prop' in d
True

对于实例的属性而言,类似的操作可以通过 try ... except 语句完成:

1
2
3
4
5
6
7
8
9
>>> class X:
... pass
...
>>> x = X()
>>> try:
... print(x.prop)
>>> except AttributeError:
... print("prop doesn't exist.")
prop doesn't exist.

但是,这并不是常用的,更提倡使用 hasattr 函数判断对象是否含有某个属性。

1
2
3
4
5
6
7
8
9
>>> class X:
... pass
...
>>> x = X()
>>> hasattr(x, 'prop')
False
>>> x.prop = 'exists'
>>> hasattr(x, 'prop')
True

下面的操作,很显然不成立:

1
2
3
4
5
6
7
>>> class X:
... value = 42
...
>>> x = X()
>>> attr_name = 'value'
>>> x.attr_name
AttributeError: 'X' object has no attribute 'attr_name'

但是,如果想让它成为现实,可以这么做:

1
2
3
4
5
6
7
8
9
>>> class X:
... value = 42
...
>>> x = X()
>>> getattr(x, 'value')
42
>>> attr_name = 'value'
>>> getattr(x, attr_name)
42 # It works!

这里使用了 getattr 函数,它能接受一个字符串为属性,并设置属性值。

setattrdelattr 也都有类似的功能:它们接受字符串的属性名称,并相应地设置或删除属性值。

1
2
3
4
5
6
7
8
9
10
>>> class X:
... value = 42
...
>>> x = X()
>>> setattr(x, 'value', 84)
>>> x.value
84
>>> delattr(x, 'value') # deletes the attribute completety
>>> hasattr(x, 'value')
False # `value` no longer exists on the object.

下面试着用其中的一个函数来构建一些有意义的东西:

1
2
3
4
5
6
7
8
9
10
11
12
class api:
"""A dummy API."""
def send(item):
print(f'Uploaded {item!r}!')

def upload_data(item):
"""Uploads the provided value to our database."""
if hasattr(item, 'get_value'):
data = item.get_value()
api.send(data)
else:
api.send(item)

这里的 upload_data 函数检查实参是否有 get_value 方法,如果有,则通过此方法读取数据。符合此类要求的对象(作为实参)是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import json
>>> class DataCollector:
... def __init__(self):
... self.items = []
... def add_item(self, item):
... self.items.append(item)
... def get_value(self):
... return json.dumps(self.items)
...
>>> upload_data('some text')
Uploaded 'some text'!
>>> collector = DataCollector()
>>> collector.add_item(42)
>>> collector.add_item(1000)
>>> upload_data(collector)
Uploaded '[42, 1000]'!

super :用于继承

super 是 Python 引用父类的方式,例如,为了使用父类的方法而使用 super

以下面的类为例,它的作用是实现两项相加。

1
2
3
4
5
6
7
class Sum:
def __init__(self, x, y):
self.x = x
self.y = y

def perform(self):
return self.x + self.y

这个类的应用非常简单:

1
2
3
>>> s = Sum(2, 3)
>>> s.perform()
5

现在又创建了类 DoubleSum ,并且这个类继承了 Sum 类。在 DoubleSum 类中,有与父类相同的方法 perform ,但是它的返回值是两项和的 2 倍。这时候如果不断算写重复代码,而是依然要首先调用父类中的 perform 方法计算两项和,就可以用 super 方法实现:

1
2
3
4
class DoubleSum(Sum):
def perform(self):
parent_sum = super().perform()
return 2 * parent_sum

这样做,不需要重复已经定义过的东西,比如 __init__ 方法,也不用重复写求和逻辑,只需要在继承父类的基础上调用父类方法即可。

1
2
3
>>> d = DoubleSum(3, 5)
>>> d.perform()
16

super 函数也不只用在类中,其他地方也可以使用,比如:

1
2
3
4
5
6
>>> super(int)
<super: <class 'int'>, NULL>
>>> super(int, int)
<super: <class 'int'>, <int object>>
>>> super(int, bool)
<super: <class 'int'>, <bool object>>

但说实话,我不明白这些有什么用。 如果你知道,请在评论中告诉我,感谢。

property, classmethod and staticmethod :方法的装饰器

这三个是对类中方法的三个装饰器:

  • property:

    当你想要在类中对属性通过 gettersetter 进行读写时,就要使用装饰器 @property 。在此情形下,可以通过 gettersetter 对读写过程给予干涉。

    通过 property 装饰器,将属性转换为一组方法来实现的:一个方法在访问属性时运行,另一个方法尝试更改属性值时运行。

    看一个例子,我们试图确保一个学生的 marks 属性总是被设置为正数,因为学习成绩不能是负数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Student:
    def __init__(self):
    self._marks = 0

    @property
    def marks(self):
    return self._marks

    @marks.setter
    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
    8
    >>> student = Student()
    >>> student.marks
    0
    >>> student.marks = 85
    >>> student.marks
    85
    >>> student.marks = -10
    ValueError: marks cannot be negative
  • classmethod:

    装饰器 @classmethod 用在方法上,使其成为类方法。这样一来,它就获得对类对象的引用,而不是对实例 (self)的引用。

    比如,在类里面创建一个返回类名称的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    >>> class 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
    5
    class API:
    @staticmethod
    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
    11
    >>> my_list = [10, 20, 30]  # Creates a list with 3 items
    >>> my_list[0] # Indexes start with zero
    10
    >>> my_list[1] # Indexes increase one by one
    20
    >>> my_list.append(40) # Mutable: can add values
    >>> my_list
    [10, 20, 30, 40]
    >>> my_list[0] = 50 # Can also reassign indexes
    >>> my_list
    [50, 20, 30, 40]
  • tuple :像列表一样有序和,但有一个关键区别: 它们是不可变的,这意味着一旦创建了元组对象,就不能添加或删除其中的成员。

    1
    2
    3
    4
    5
    6
    7
    >>> some_tuple = (1, 2, 3)
    >>> some_tuple[0] # Indexable
    1
    >>> some_tuple.append(4) # But NOT mutable
    AttributeError: ...
    >>> some_tuple[0] = 5 # Cannot reassign an index as well
    TypeError: ...
  • dict:以键值对为成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> flower_colors = {'roses': 'red', 'violets': 'blue'}
    >>> flower_colors['violets'] # Use keys to access value
    'blue'
    >>> flower_colors['violets'] = 'purple' # Mutable
    >>> flower_colors
    {'roses': 'red', 'violets': 'purple'}
    >>> flower_colors['daffodil'] = 'yellow' # Can also add new values
    >>> flower_colors
    {'roses': 'red', 'violets': 'purple', 'daffodil': 'yellow'}
  • set :是由无序的、唯一的成员组成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> forest = ['cedar', 'bamboo', 'cedar', 'cedar', 'cedar', 'oak', 'bamboo']
    >>> tree_types = set(forest)
    >>> tree_types
    {'bamboo', 'oak', 'cedar'} # Only unique items
    >>> 'oak' in tree_types
    True
    >>> tree_types.remove('oak') # Sets are also mutable
    >>> tree_types
    {'bamboo', 'cedar'}
  • frozenset 与集合相同,但它是不可变的。

    1
    2
    3
    4
    5
    6
    7
    8
    >>> forest = ['cedar', 'bamboo', 'cedar', 'cedar', 'cedar', 'oak', 'bamboo']
    >>> tree_types = frozenset(forest)
    >>> tree_types
    frozenset({'bamboo', 'oak', 'cedar'})
    >>> 'cedar' in tree_types
    True
    >>> tree_types.add('mahogany') # CANNOT modify
    AttributeError: ...

内置的 listtupledict 也可以用来创建这些数据结构的空实例:

1
2
3
4
5
6
>>> x = list()
>>> x
[]
>>> y = dict()
>>> y
{}
使用支付宝打赏
使用微信打赏

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

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