【问题标题】:Deeper understanding of Python object mechanisms深入理解Python对象机制
【发布时间】:2016-12-02 10:44:52
【问题描述】:

我想更好地理解 Python 3.x 数据模型。但是我没有找到关于 Python 对象行为的完整而准确的解释。

我正在寻找参考资料,如果我在下面展示的每个案例都可以链接到 Python API 参考资料或 PEP 或其他任何有价值的东西,那就太好了。进一步感谢您的明智建议...

假设我们有一些复杂的 Python 结构用于测试目的:

d1 = {
    'id': 5432
   ,'name': 'jlandercy'
   ,'pets': {
        'andy': {
            'type': 'cat'
           ,'age': 3.5
        }
       ,'ray': {
            'type': 'dog'
           ,'age': 6.5
        }
    }
   ,'type': str
   ,'complex': (5432, 6.5, 'cat', str)
   ,'list': ['milk', 'chocolate', 'butter']
}

1) 不可变原子对象是单例

无论我如何创建一个新整数:

n1 = 5432
n2 = int(5432)
n3 = copy.copy(n1)
n4 = copy.deepcopy(n1)

不会创建此编号的新副本,而是指向与d1['id'] 相同的对象。更简洁

 d1['id'] is n1
 ...

它们都有相同的id,我无法创建一个值为 5432 的 int 的新实例,因此它是一个单例

2) 不可变和可迭代对象可能是单例...

先前的观察也适用于str,它们是不可变的可迭代的。以下所有变量:

s1 = 'jlandercy'
s2 = str('jlandercy')
s3 = copy.copy(s1)
s4 = copy.deepcopy(s1)

指向最初创建的副本d1['name']。字符串也是单例

...但不完全是...

Tuple 也是 immutableiterable,但它们的行为不像字符串。众所周知,神奇的空元组是一个单例

() is ()

但其他元组不是。

t1 = (5432, 6.5, 'cat', str)

...相反,它们的哈希值相等

他们没有相同的id:

id(d1['complex']) != id(t1)

但这两个结构中的所有项目都是原子的,因此它们指向相同的实例。重要的一点是,hash 的两个结构都一样:

hash(d1['complex']) == hash(t1)

所以它们可以用作字典键。对于嵌套元组也是如此:

t2 = (1, (2, 3))
t3 = (1, (2, 3))

他们确实有相同的hash

3) 通过双重解引用传递字典作为它的浅拷贝

让我们定义以下函数:

def f1(**kwargs):
    kwargs['id'] = 1111
    kwargs['pets']['andy'] = None

将通过双重取消引用(** 运算符)接收我们的试用字典的一级成员将被复制,但最深的成员将通过引用传递。

这个简单程序的输出,说明它:

print(d1)
f1(**d1)
print(d1)

返回:

{'complex': (5432, 6.5, 'cat', <class 'str'>),
 'id': 5432,
 'list': ['milk', 'chocolate', 'butter'],
 'name': 'jlandercy',
 'pets': {'andy': {'age': 3.5, 'type': 'cat'},
          'ray': {'age': 6.5, 'type': 'dog'}},
 'type': <class 'str'>}

{'complex': (5432, 6.5, 'cat', <class 'str'>),
 'id': 5432,
 'list': ['milk', 'chocolate', 'butter'],
 'name': 'jlandercy',
 'pets': {'andy': None, 'ray': {'age': 6.5, 'type': 'dog'}},
 'type': <class 'str'>}

字典d1 已被函数f1 修改,但不是完全修改。成员id'被保留,因为我们在做一个副本,但是成员pets也是一个字典,浅拷贝没有复制它,所以它已经被修改了。

此行为类似于 dict 对象的 copy.copy 行为。我们需要copy.deepcopy 来获得对象的递归和完整副本。

我的要求是:

  • 我的观察得到了正确的解释吗?

    1. 不可变原子对象是单例

    2. Immutable 和 Iterable 对象可能是单例,但不完全是,它们的哈希值相等

    3. 通过双重解引用传递字典作为它的浅拷贝

  • 这些行为是否有据可查?
  • 对于每种情况,都说明了正确的属性和行为。

【问题讨论】:

    标签: python python-3.x object reference


    【解决方案1】:

    不可变的原子对象是单例

    不,有些是有些不是,这是 CPython 实现的一个细节。

    • (-6, 256] 范围内的整数被缓存,当对这些整数发出新请求时,将返回已经存在的对象。该范围之外的数字会受到常量折叠的影响,其中解释器在编译期间重用常量作为轻微优化。 This is documented 在创建新的PyLong 对象部分。

      另外,关于这些的讨论,请参见以下内容:

    • 字符串字面量在编译为字节码期间会像 int 一样进行实习。但是,管理这一点的规则不像整数那样简单:仅考虑由特定字符组成的特定大小的字符串。我不知道文档中的任何部分指定了这一点,您可以通过阅读 here 来查看行为。

    • 例如,可以认为是“原子”的浮点数(即使在 Python 中该术语没有您认为的含义)没有单例:

      i = 1.0
      j = 1.0
      i is j # False
      

      当然,它们仍然会不断折叠。如您所见:'is' operator behaves unexpectedly with floats

    Immutable 和 Iterable 对象可能是单例,但不完全是,它们的哈希值相等

    空的不可变集合是符号;这又是一个在 Python 参考中找不到的实现细节,但只有在查看源代码时才能真正发现。

    查看这里查看实现:Why does '() is ()' return True when '[] is []' and '{} is {}' return False?

    通过双重解引用传递字典作为它的浅拷贝。

    是的。尽管该术语不是双重解引用,但它是拆包。

    这些行为是否在某处有详细记录?

    那些被认为是实现细节的内容不需要以您查找max 函数文档的方式记录。如果做出这样的决定,这些是很容易改变的具体事情。

    【讨论】:

    • 你的回答很完整,如果有其他弹出我会稍等。还是谢谢你。
    • 这很有意义@jlandercy,给它尽可能多的时间你认为最好的:-)
    • 您能否在 [2] 中将信息添加到散列中,不可变和可迭代(仅在 stdlib 中)散列始终以相同的方式进行?例如必须让我使用 named_tuple 或“原子类型”的元组就足够了吗?谢谢