python黑魔法之 --- 参数传递

我们都听说,python世界里面,万物皆对象。
怎么说万物皆对象呢?最常见的:

> class A: pass
> a = A()

我们说a是一个对象。
那么既然是万物了,其实A也是对象。3 也是对象。True 也是对象。"hello" 也是对象。

> def Func(): pass

o~yee, Func 也是对象。

那么对象之间的传递是如何呢?我们看看下面两个简单的例子:

> a = 3
> b = a
> b = 3 + 1
> print b
4
> print a
3

> a = []
> b = a
> b.append(1)

> print a
[1]
> print b
[1]

不是都说python所有对象都是引用传递吗?为毛第一个b不是3?

好吧。事实是,在python的实现上,对象分为mutable 和 immutable。
这里说的对象分类,是说在实现上具备这样的特性。而非对象本身的属性。

什么是immutable?表示对象本身不可改变。这里先记住一点,是对象 本身 不可改变。
什么叫做对象本身不可改变呢?

一个简单的例子:

> a = (1,2,3)
> a[0] = 10   

TypeError: 'tuple' object does not support item assignment

元组的元素在初始化后就不能再被改变。也就是说,元组对象具备immutable的特性。

那么很简单,相对的,mutable 就是可变的。比如:

> a = {}
> a[0] = 10

有了上面的两个例子,相信大家已经有了基本的认识。
那么,在python世界中,哪些是具备immutable特性,哪些又是mutable的呢?

简单讲,基本类型都是immutable, 而object都是mutable的。
比如说:int, float, bool, tuple 都是immutable。
再比如:dict, set, list, classinstance 都是mutable的。

那么问题来了。既然说基本类型是 immutable ,那么最上面的 b = 3 + 1 为什么不会像tuple一样,抛异常呢?
原因在于,int+操作会执行自己的__add__方法。而__add__方法会返回一个新的对象。

事实是,当基本类型被改变时,并不是改变其自身,而是创建了一个新的对象。最终返回的是新的对象的引用。

怎么证明?
我们可以使用一个叫做id()的函数。该函数会返回对象的一个唯一id(目前的实现可以间接理解为对象的内存地址)。
那么我们看下:

> a = 3
> id(a)
140248135804168

> id(3)
140248135804168

> id(4)
140248135804144

> a = a + 1
> id(a)
140248135804144

you see ? 当我们执行a=a+1 后,id(a) 已经改变了。
深究一点,为什么会这样呢?
其实,a = a + 1 经历了两个过程:

  1. a + 1
  2. a 赋值

第2步只是一个引用的改变。重点在第1步。a + 1,那么python实际上会调用a.__add__(1)
对于int类型__add__函数的实现逻辑,是创建了一个新的int对象,并返回。

不知道细心的你有没有发现一个特别的地方?
id(4)的值等于id(3+1) 。这个只是python对int,和bool做的特殊优化。不要以为其他基本类型只要值一样都会指向相同的对象。

有个特殊的例子,str。做个简单的实验:

> a = "hello"
> id(a)
4365413232
> b = "hell"
> id(b)
4365386208

> id(a[:-1])
4365410928
> id(a[:-1])
4365413760

看到了吗?虽然值相同,但是还是指向(创建)了不同的对象,尤其是最后两句,哪怕执行相同的操作,依然创建了不同的对象。

我靠,python这么傻,每次都创建新的对象?
no no no 他只是缓存了“一些”结果。我们可以再试试看:

> a = "hello"
> ret = set()
> for i in range(1000):
    ret.add(id(a[:-1]))
> print ret
{4388133312, 4388204640}

看到了吗?python还是挺聪明的。不过具体的缓存机制我没有深究过,期望有同学能分享下。

再次回到我们的主题,python中参数是如何传递的?

答案是,引用传递。

平时使用静态语言的同学(比如我),可能会用下面的例子挑战我了:

def fun(data):
    data = 3

a = 100
func(a)

print a # 100

不是尼玛引用传递吗?为毛在执行func(a)后,a 的值没有改变呢?这里犯了一个动态语言基本的错误。
data=3,语义上是动态语言的赋值语句。千万不要和C++之类的语言一个理解。

看看我们传入一个mutable 的对象:

> def func(m):
    m[3] = 100

> a = {}
> print a
{}
> func(a)
> print a
{3:100}

现在同学们知道该如何进行参数传递了吧?好嘞,进阶!
像很多语言如C++,js,swift... 一样,python 的函数声明支持默认参数:

def func(a=[]): pass

不知道什么意思?自己看书去!

我这里要说的是,如果我们的默认参数是mutable类型的对象,会有什么黑魔法产产生?
我们看看下面的函数:

def func(a=[]):
    a.append(3)
    return a

可能有同学会说了:我去!这么简单?来骗代码的吧?

但是,真的这么简单吗?我们看下下面的调用结果:

> print func()
[3]
> print func()
[3,3]
> print func()
[3,3,3]

这真的是你想要的结果吗?

No,我要的是[3][3],[3]!

原因?好吧,我们再用下id()神奇看看:

def func(a=[]):
    print id(a)
    a.append(3)
    return a

> print func()
4365426272
[3]
> print func()
4365426272
[3, 3]
> print func()
4365426272
[3, 3, 3]

明白没?原来在python中,默认参数不是每次执行时都创建的!

这下你再想想,曾经嘲笑过的代码(至少我)为什么要 多此一举

def func(a=None):
    if a is None:
        a = []

这里在顺带提一下==, is:

== : 值比较
is : 比较左右两边是否是同一个对象。 a is b ==> id(a) == id(b)

ok, let's move on!

我们都知道,在python中,不定参数我们可以这样定义:

def func(args, *kv): pass

什么你不知道?看书去!
argskv到底是什么情况呢?到底是mutable 还是 immutable 呢?

再一次请出id()神器:

def func(*args):
    print id(args)

> a = [1,2]
> print id(a)
4364874816
> func(*a)
4364698832
> func(*a)
4364701496

看到了吧?实际上args也会产生一个新的对象。但是值是填入的传入参数。那么每一个item也会复制吗?
我们再看看:

def func(*args):
    print id(args[0])

> a = [1,2]
> print id(a[0])
140248135804216
> func(*a)
140248135804216

答案是,No。值会像普通list赋值一样,指向原先list(a)所引用的对象。
那么为什么会这样呢?

python的源码就是这么写的.......

最最后,还记得我说过的一句话吗?

immutable 限制的是对象本身不可变

意思就是说,对象的immtable 只是限制自身的属性能否被改变,而不会影响到其引用的对象。
看下下面的例子:

> a = [1,2]
> b = (a,3)
> b[1] = 100
TypeError: 'tuple' object does not support item assignment

> print b
([1, 2], 3)
> b[0][0] = 10
> print b
([10, 2], 3)

最最最后,我有个对象,它本身应该是 mutable 的,但是我想让他具备类似immutable的特性,可以吗?
答案是,可以模拟!

还是之前说的,immutable 限制的是其自身属性不能改变。
那么,我们的可以通过重定义(重载)属性改变函数,来模拟immutable特性。

python可以吗?O~Yee

在python的类函数中,有这样的两个函数: __setattr____delattr__。分别会在对象属性赋值和删除时执行。
那么我们可以进行简单重载来模拟immutable:

class A:
    def __setattr__(self, name, val):
        raise TypeError("immutable object could not set attr")

好啦,全文结束!
后面有时间再给大家分享下python的metaclass

时间: 2024-12-10 09:16:55

python黑魔法之 --- 参数传递的相关文章

Python黑魔法之描述符

引言 Descriptors(描述符)是Python语言中一个深奥但很重要的一个黑魔法,它被广泛应用于Python语言的内核,熟练掌握描述符将会为Python程序员的工具箱添加一个额外的技巧.本文我将讲述描述符的定义以及一些常见的场景,并且在文末会补充一下__getattr,__getattribute__, __getitem__这三个同样涉及到属性访问的魔术方法. 描述符的定义 descr__get__(self, obj, objtype=None) --> value descr.__s

Python def函数的定义、使用及参数传递实现代码_python

Python编程中对于某些需要重复调用的程序,可以使用函数进行定义,基本形式为: def 函数名(参数1, 参数2, --, 参数N): 执行语句函数名为调用的表示名,参数则是传入的参数,可以更具需要定义,也可以没有. # 例1:简单的函数使用 # coding=gb2312 # 定义函数 def hello(): print 'hello python!' # 调用函数 hello() >>> hello python! 函数可以带参数和返回值,参数将按从左到右的匹配,参数可设置默认值

深入理解Python中的ThreadLocal变量(上)

我们知道多线程环境下,每一个线程均可以使用所属进程的全局变量.如果一个线程对全局变量进行了修改,将会影响到其他所有的线程.为了避免多个线程同时对变量进行修改,引入了线程同步机制,通过互斥锁,条件变量或者读写锁来控制对全局变量的访问. 只用全局变量并不能满足多线程环境的需求,很多时候线程还需要拥有自己的私有数据,这些数据对于其他线程来说不可见.因此线程中也可以使用局部变量,局部变量只有线程自身可以访问,同一个进程下的其他线程不可访问. 有时候使用局部变量不太方便,因此 python 还提供了 Th

深入理解Python中的ThreadLocal变量(中)

在 深入理解Python中的ThreadLocal变量(上)中我们看到 ThreadLocal 的引入,使得可以很方便地在多线程环境中使用局部变量.如此美妙的功能到底是怎样实现的?如果你对它的实现原理没有好奇心或一探究竟的冲动,那么接下来的内容估计会让你后悔自己的浅尝辄止了. 简单来说,Python 中 ThreadLocal 就是通过下图中的方法,将全局变量伪装成线程局部变量,相信读完本篇文章你会理解图中内容的.(对这张图不眼熟的话,可以回顾下上篇)). 在哪里找到源码? 好了,终于要来分析

Python中使用装饰器和元编程实现结构体类实例_python

Ruby中有一个很方便的Struct类,用来实现结构体.这样就不用费力的去定义一个完整的类来仅仅用作访问属性. 复制代码 代码如下: class Dog < Struct.new(:name, :age) end fred = Dog.new("fred", 5) printf "name:%s age:%d", fred.name, fred.age ##name:fred age:5 Python3.4中也可以这么干,但写法很累赘.其中包含self.nam

利用Python命令行传递实例化对象的方法_python

一.前言 在开发过程中,遇到了这样一个情况:我们需要在脚本中通过 suprocess.call 方法来启动另外一个脚本(脚本 B),当然啦,还得传递一些参数.在这些参数中,有一个需要传递的是一个实例化后的对象.我们知道,通过命令行的方式传递参数是基于字符格式的,也就是说脚本 B 只能接收到字符串格式的参数,那么如何接收启动脚本传递过来的实例化后的对象呢? 今天就来聊聊我使用的两种笨方法:使用 eval 以及使用 pickle 和 base64 模块. 方法一:使用 eval 其实在代码中使用 e

Python中函数的参数传递与可变长参数介绍

  这篇文章主要介绍了Python中函数的参数传递与可变长参数介绍,本文分别给出多个代码实例来讲解多种多样的函数参数,需要的朋友可以参考下 1.Python中也有像C++一样的默认缺省函数 代码如下: def foo(text,num=0): print text,num foo("asd") #asd 0 foo("def",100) #def 100 定义有默认参数的函数时,这些默认值参数 位置必须都在非默认值参数后面. 调用时提供默认值参数值时,使用提供的值,

python可变参数传递给方法问题

问题描述 python可变参数传递给方法问题 import sys def foo(a, b=None, c=None): print(b,c) foo(sys.argv[1],sys.argv[2],sys.argv[3]) 脚本从sys.argv获得参数,必须上面写才能获得b,c的值 执行 python 51.py 1 2 3 像foo(sys.argv[1:]) 所有变量作为列表传给a,b,c仍为空. 请指点该如何处理? 解决方案 可变参数传递的方法 解决方案二: 通过参数名称来指定.用g

《Python数据科学指南》——1.13 将函数作为参数传递

1.13 将函数作为参数传递 Python支持高阶函数功能:将一个函数作为另一个函数的参数传递. 1.13.1 准备工作 我们将前面一个例子中的函数square_input ()重写,以此演示一个函数是如何被作为另一个函数的参数进行传递. 1.13.2 操作方法 请看如何将一个函数作为另一个函数的参数进行传递. from math import log def square_input(x): return x*x # 1.定义一个类函数,它将另外一个函数作为输入 # 并将它应用到给定的输入序列