万物皆对象-可变与不可变-参数传递

2018-07-20  本文已影响14人  小温侯

万物皆对象。

万物皆对象

怎么理解?就是在Python中,你能想到的所有的东西它都是一个对象:整型、浮点数、字符串、元组、列表、字典、集合,甚至函数、类等等,都是对象(object)。对象之间是怎么传递内容的呢?引用,这点和C/C++的指针很像,相当于两个不同名字的指针指向同一个内存位置。将Python的赋值操作和C/C++中的(void*)指针对比是比较合理的。

这同样也意味着在编写代码时,同一个变量名字可以赋予不同类型的对象。而加上Python是门动态语言,它在运行的时候会去适配合适的类型,因此代码执行起来就不会有问题。

和C/C++不同,在Python中赋值符号=表示的就是引用引用的概念。最简单的:

a = 1

首先这个数字1也是对象,由于Python的内存池机制的存在,Python会对小于256字节的对象使用pymalloc方法分配内存,这是题外话,重点是最终对象1会有一个地址,而这里是把这个地址给了变量a。这就相当于C中的a = (void*)1,怎么证明?

>>> type(1)
<class 'int'>   # 类型是class,
>>> int.__bases__
(<class 'object'>,)   # int类的父类是object类
>>> id(1)    # 查看对象1的内存地址
1912499216
>>> a = 1    
>>> id(a)    # a和1的对象地址是一样的
1912499216

再看一个例子:

>>> a = [[2,1], 2, 3]
>>> id(a)
1894079804680
>>> id(a[0]), id(a[1]), id(a[2])
(1894079807304, 1912499248, 1912499280)
>>> id(a[0][0]), id(a[0][1])
(1912499248, 1912499216)

有几点观察:

这几点想明白之后,“万物皆对象“这句话就可以理解一大半了。

对象的三要素

每一种对象都会对应三种属性:

>>> type(1)
<class 'int'> # 整型
>>> type(a)
<class 'list'> # 列表
>>> type(type) 
<class 'type'> # type()也是对象,同理id()也是

判断两个对象是否一样使用的是is关键词,而判断两个对象的值是不是一样使用的是==操作符。

除了这个三要素,对象也可以拥有方法。这是另一个话题。

赋值、浅拷贝和深拷贝

先考虑如下代码:

a = [1, ['a', 'b', 'c'], 3]
b = a
c = ['a', 'b', 'c']

assert (a is b) == True
assert (a[1] is c) == False # 注意这条,考虑下为什么

b[0] = 2
print (a) # 输出[2, ['a', 'b', 'c'], 3]

如果b修改了其列表中的值,那么a中的也会变,这点不难理解。但是如果我想要要的是a的一份拷贝呢?

a = [1, ['a', 'b', 'c'], 3]
b = a.copy()

print (id(a),[id(x) for x in a])
print (id(b),[id(x) for x in b])
# 输出
# 3082815203464 [1912499216, 3082815204040, 1912499280]
# 3082815172104 [1912499216, 3082815204040, 1912499280]

细心的话会发现,虽然ab的地址已经变了,但是a[1]b[1]的内存地址是一样的,也就是说修改b[1]仍然会影响到a中的内容,当然这种结果有时候是可以接受的。

如果不接受呢?就是要给a中的所有对象,包括对象中包含的对象都重新申请内存,这里要用到copy模块:

import copy
a = [1, ['a', 'b', 'c'], 3]
b = copy.deepcopy(a)

print (id(a),[id(x) for x in a])
print (id(b),[id(x) for x in b])

# 输出
# 1596348426376 [1912499216, 1596348426952, 1912499280]
# 1596348395016 [1912499216, 1596348394696, 1912499280]

除了这些,关于列表复制也可以这么做:

a = [1, ['a', 'b', 'c'], 3]
b = a[:]

那么这属于哪种拷贝呢?

可变对象和不可变对象

Python中有一个概念,它有的对象内容会有两种可变的不可变的。怎么理解呢?回到第一个例子:

a = 1
print (id(a), id(1), id(2)) # 1912499216 1912499216 1912499248
a = 2
print (id(a), id(1), id(2)) # 1912499248 1912499216 1912499248

a这里是可变的,它的值从1912499216变成了1912499248,这不重要。

这里需要理解的是,整型对象1不可变的,它的地址永远是1912499216。而且似乎也没什么办法能修改这个值。

按照这种逻辑,Python中的对象可以分为两类:

关于可变对象和不可变对象其实没有太多可说。但是如果你了解这个细节,有时候写代码的时候会无形中提高代码的效率。

不可变的字符串

最有可能遇到的场景是关于字符串的操作,因为在Python中字符串是不可变对象,也就是说字符串"abc""abcd"是不同的对象,这和我们的原始认知可能有点不协调。考虑如下代码:

print ([id("abc"), id("abcd")])
a = "abc"
print (id(a))
a += a
print (id(a))
b = "abc" + "d"
e = "a" + "b" + "c" + "d"
print (id(b), id(e))

# 输出
# [2840292183040, 2840294857784]
# 2840292183040
# 2840294857672
# 2840294857784 2840294857784

可以看到:

接下来的问题就是,这对我们写代码有什么帮助?那么再考虑一个问题,如何将一个列表中的所有字符串串联起来?不难想到,两种方法:

slist = ['I', 'am', 'Groot']

def fn1():
    r = ''
    for i in slist:
        r += i + ' '
    return r[:-1]

def fn2():
    return " ".join(slist)

哪种好呢?其实是第二种,我们可以简单测试一下两个函数的效率:

import time
slist = []
for i in range(1000000):
    slist.append('1')

def fn1():
    ...

def fn2():
    ...

t1 = time.time()
fn1()
t2 = time.time()
fn2()
t3 = time.time()

print (t2-t1, t3-t2)
# 输出
# 0.36515259742736816 0.013962268829345703

可以看到使用join的话效率明显好,而且还省了很多资源。理由其实很简单,就是fn1里面新建了很多对象。换句话说,join的实现里应该没有这些额外的对象,它是怎么实现的呢?用C,申请一段内存,不断地将列表中的字符串拷贝到对应的位置。能不能用Python实现?不能,因为不知道怎么动态申请内存。证明一下?

slist = ['I', 'am', 'Groot']

def op(slist):
    print ('-----')
    res = ''
    print (id(res))
    for s in slist:
        res += s
        print (id(res))
    print ('-----')
    return res

print (id(slist))
print (id(op(slist)))

# 输出
# 3103558232392
# -----
# 3103550569136
# 3103557636368
# 3103558269056
# 3103558266160
# -----
# 3103558266160

可以看到res的地址一直在变,从整体看,传入op的列表和从op传出的列表已经不是同一个了。这里其实不一定要是字符串,任何可迭代的对象都可以使用join连接,鸭子类型了解一下。

可变的默认参数

有时候函数是有默认参数的,Python文档中明确说了不推荐使用可变对象作为函数的默认参数。然而不推荐不代表不可以,那么如果使用了可变对象作为默认参数会导致什么问题呢?考虑如下代码:

def fn(default=[]):
    default.append(1)
    return default

print (fn())
print (fn())

# 输出
# [1]
# [1, 1]

为什么?这是因为函数fn也是类,在定义函数生成fn这个类时,Python会把它的一些信息下来,这里涉及到的两个内置变量是__defaults____code__。我们将它们打印出来看看:

def fn(default=[]):
    default.append(1)
    return default

p1 = fn()
p2 = fn()

print(id(p1), id(p2))

print (fn.__defaults__)
print (id(fn.__defaults__[0]))
print (dir(fn.__code__))
print (fn.__code__.co_varnames)

# 输出
# 2328737311048 2328737311048
# ([1, 1],)
# 2328737311048
# ['__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__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
# ('default',)

同样,这里有几点可以说说:

这样一看其实就明朗了,对于可变参数,如果在函数实现中对其进行了修改,那么就会对应的修改这个函数的meta信息;显然,不可变参数就不存在这个问题。

a += b 和 a = a + b哪个好?

不可变的字符串里的例子中已经可以证明,对于不可变对象(字符串)aa += '1'a = a + '1'都会产生额外的新对象,那么如果对象是可变的呢?

slist = ['I', 'am', 'Groot']
print (id(slist))

slist.append('!')
print (id(slist))

slist += ['?']
print (id(slist))

slist = slist + ['!']
print (id(slist))

因此答案是+=好。

参数传递

再说说Python中参数的传递过程,我看到很多网上的文章会将参数传递的方式分为传值传引用,但是根据我自己的理解,我以为这两种说法其实说的是一回事。

先说我的观点:在Python中,根本没有传值,或者每次都是传值。因为每次传递的其实就是对象的引用,而传递的值就是该对象对应的内存地址,即C中的(void*),这也呼应了文章开头的那句:万物皆为对象

现在我找了两个网上常用的例子,很多作者在讲述参数传递的时候就用这两个例子作为切入点,然后先把问题讲复杂了,然后在引出可变对象和不可变对象之类的概念,显得特别高大上。

其实完全没必要,我以为如果你能理解万物皆对象,就能理解参数传递,它和可变不可变其实没什么关系。先来看这两个例子:

def foo(var):
    var = 2
    print(var)  # 2
a = 1
foo(a)
print(a) # 1
def bar(var):
    var.append(1)

b = []
bar(b)
print(b) # [1]

我们换个角度看这个问题。

再看一个我从网上找来的例子:

def change(val):
    val.append(100)
    val = ['T', 'Z', 'Y']
nums = [0, 1]
change(nums)
print(nums) # [0, 1, 100]

这就很容易理解了,在执行append的时候,val指向的仍然是nums指向的列表,因此100能成功添加到nums列表中。而之后val被赋予了一个新的列表对象['T', 'Z', 'Y'],而这和nums没有任何关系。

因此,我最后想重新提一下,其实传什么在Python中只有一种形式,不需要和C/C++中的传参传值相类比。同样,传什么与对象是不是可变的什么关系。关键看它们的对象引用指向了哪块内存。

上一篇 下一篇

猜你喜欢

热点阅读