Python函数的命名参数相关

起因

今天师弟问了一个关于Python函数参数的一个问题:

#1
def func(x, l=[]):
    pass

#2
def func(x, l=None):
    if l is None:
        l = []

为啥第一个函数会把l每次调用完的值保留下来?

起初我认为问的是这两个函数使用的时候,为何会保持对传入的参数l的修改。从这个方面来讲,是因为Python对于数据赋值的处理的原因。

在Python中,赋值是传引用的。一个列表,比如[1, 2, 3],或者一个字符串,’tonychow’,这些对象在创建的时候会在内存中分配一段空间。如果将这些对象赋值给一个变量名,那就会导致在Python的命名空间中该变量名指向内存中这个对象。对该变量名的操作就是对内存中这个对象的操作。所以如果尝试直接将一个变量a赋值给另外一个变量b,导致的后果是,命名空间中,这两个变量名a和b指向内存中同样一个对象,也就是所谓传引用赋值。对其中任意一个变量的操作,实质是对该对象进行操作,所以同样的操作后结果也会可以在另外一个变量中看到。如下:

>>> a = [1, 2, 3]
>>> b = a
>>> a.pop()
3
>>> b
[1, 2]
>>> a
[1, 2]
>>> 

从上面的代码可以看到,在将a赋值给b之后,对a列表调用pop方法,导致的是b列表也发生了变化。我们还可以通过Python内置的globals函数和id函数来加深这个理解。globals函数将会返回一个字典,这个字典是当前的全局符号表。而id函数则会返回一个对象的标识,实际上就是这个对象在内存中的地址。

>>> globals()
{'a': [1, 2], 'b': [1, 2],
'__builtins__': <module '__builtin__' (built-in)>,
'value': None, '__package__': None, 
'key': '__doc__', '__name__': '__main__', '__doc__': None}
>>> id(a)
3077280588L
>>> id(b)
3077280588L
>>> 

可以看到,a和b都在当前的全局字符表中,他们的值也都是一致的。此外,id函数的结果明确地说明了a和b这两个变量名都是指向了内存中的同一个对象。而在Python中,调用函数的时候,传入参数,也是进行传引用的赋值。所以我师弟说的这两个函数都会保留对于传入参数的修改,也就是:

>>> def func(l=None):
...     if l is None:
...         l = []
...     l.append(1)
... 
>>> bar = [2]
>>> bar
[2]
>>> func(bar)
>>> func(bar)
>>> bar
[2, 1, 1]
>>> 

题外话,在Python内置的数据类型中,有两种不同的数据类型。一种是可变类型,比如list,dict等;另外一种就是不可变类型,比如字符串或者tuple。

可是后来师弟贴出了另外一段代码:

>>> def func2(items=None):
...     if items == None:
...         items = []
...     items.append(1)
...     return items
... 
>>> func2()
[1]
>>> func2()
[1]
>>> 

这下我明白了,师弟说的不是我想到的那个问题,而是命名参数的问题。

解决

说实话这个问题一开始我也没有想到答案。大家在学习Python的时候,无论看的是哪本入门书,应该在开始的时候都会看到一句话“Python中一切都是对象”。看代码:

>>> isinstance(1, int)
True
>>> isinstance('test', str)
True
>>> def func():
...     pass
... 
>>> type(func)
<type 'function'>
>>> dir(func)
['__call__', '__class__', '__closure__', '__code__', '__defaults__',
'__delattr__', '__dict__', '__doc__', '__format__', '__get__', 
'__getattribute__', '__globals__', '__hash__', '__init__', 
'__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc',
'func_globals', 'func_name']
>>> func.__code__
<code object func at 0xb76c8410, file "<stdin>", line 1>
>>> func.__name__
'func'
>>> 

对的,数字1是一个对象,字符串’test’也是一个对象,甚至一个函数也是一个类型为function的对象,也有一堆的属性和方法。对于function对象而言,有一个特殊属性defaults,这个属性用一个元组保存了是这个function对象的命名参数的缺省值,如下:

>>> def func(a=1, b=2):
...     pass
...
>>> func.__defaults__
(1, 2)
>>> def foo(a, b):
...     pass
...
>>> foo.__defaults__ is None
True
>>> def func_no():
...     pass
...
>>> func_no.__defaults__ is None
True
>>>

如果一个函数有命名参数,则按顺序保存了命名参数的缺省值。如果这个函数命名参数没有缺省值或者没有命名参数,则为None。回到问题,为什么第一个函数中指定缺省值为[]会导致随着执行过程中,缺省参数的值会被保留下来呢?代码如下:

>>> def foo(l=[]):
...     l.append(1)
...     return l
... 
>>> foo()
[1]
>>> foo()
[1, 1]
>>> foo()
[1, 1, 1]
>>> 

其实通过上面的罗嗦一大堆,答案很容易就可以得到了:foo是一个function类型的对象,这个对象中有个defaults属性,保存了命名参数l的值,而在一次次的调用过程中,因为没有传入参数,所以实际上foo函数改变的是命名参数的缺省值。也就是师弟所说的这个函数在一次次调用中保留了对命名参数l的结果的修改。而师弟贴出的第二个函数的命名参数缺省值是None,实质上就是没有缺省值,所以l的值修改没有在调用中保留下来。是不是真的这样?我们来看下:

>>> def foo(l=[]):
...     print 'default_arg_addr:' + str(id(l))
...     l.append(1)
...     print 'changed_var_addr:' + str(id(l))
...     print l
... 
>>> id(foo.__defaults__[0])
3077402860L
>>> foo()
default_arg_addr:3077402860
changed_var_addr:3077402860
[1]
>>> foo()
default_arg_addr:3077402860
changed_var_addr:3077402860
[1, 1]
>>> foo.__defaults__
([1, 1],)
>>> 

上面这个函数foo有一个命名参数l,它的命名参数缺省值是一个空的列表,虽然是空列表,可是它确确实实是一个对象,已经在内存给它分配了空间。我们可以通过id函数的结果看出来。然后是两次的调用foo函数可以看到,因为没有传入参数,所以这两次修改的都是这个缺省的命名参数的值,所以可以得到所谓的对l的值的修改保留下来了的感觉。

深入

首先我们应该明白,在Python中,一个对象的实例化和初始化是不同的。一个对象实例化调用的是对象的new函数,而初始化调用的是init函数。所以,要深入地去看在Python中,函数在实例化的时候到底发生了什么,我们应该要去看Python源码。如下,源码版本为Python2.7.4。

Python2.7.4/Objects/funcobject.c, func_new, L436-L439

if (defaults != Py_None) {
    Py_INCREF(defaults);
    newfunc->func_defaults  = defaults;
}

Python2.7.4/Include/object.h, L765-L767

#define Py_INCREF(op) (                     \
_Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA       \
((PyObject*)(op))->ob_refcnt++)

上面第一断代码是funcobject的func_new中的代码,也就应该是functions对象的new函数代码。可以看到,如果defaults不是None,也就是说有值,而我们上面也提到Python中一切都是对象,所以就会对这个对象进行Py_INCREF操作,并且将这个defaults值设定为func_defaults。Py_INCREF操作是什么?从第二段代码可以看到,这是一个宏定义,将参数op的ob_refcnt值加一。ob_refcnt是什么?refcnt——reference count,这样明白了,就是将该对象的引用计数值加一。在执行了函数函数之后,该命名函数的缺省值对象并没有被销毁,而是随着该函数对象的存在而存在。对这个缺省之对象的修改当然也会被保留下来。

-EOF-

Tags: Python

Python中sqlite3模块使用小记

前记

Python的标准库中包含了对sqlite这个轻巧的数据库的支持模块,也就是sqlite3模块。sqlite数据库的好处我就不多说了,小型而强大,适合很多小型或者中型的数据库应用。最近在使用sqlite3模块遇到一些问题,解决了,顺便就记下来。

问题

sqlite3模块的使用很简单,如下这段测试代码,创建一个person数据表然后进行一次数据库查询操作。

#!/usr/bin/env pypthon
#_*_ coding: utf-8 _*_


import sqlite3

SCHEMA = """
         CREATE TABLE person (
             p_id int,
             p_name text
         )
         """

def init():
    data = [(1, 'tony'), (1, 'jack')]
    conn = sqlite3.connect(':memory:')
    c = conn.cursor()
    try:
        c.execute(SCHEMA)
        for person in data:
            c.execute('insert into person values(?, ?)', person)
        conn.commit()
    except sqlite3.Error as e:
        print 'error!', e.args[0]
    return conn


if __name__ =='__main__':
    conn = init()
    c = conn.cursor()
    #Do a query.
    c.execute('select * from person where p_name = ?', 'tony')
    person = c.fetchone()
    print person

运行这段代码,抛出了个异常,如下提示:

Traceback (most recent call last):
      File "sqlite3_test.py", line 32, in 
          c.execute('select * from person where p_name = ?', 'tony')
          sqlite3.ProgrammingError: Incorrect number of bindings supplied.  
The current statement uses 1, and there are 4 supplied.

很莫名奇妙是不?明明我提供的占位符?绑定只有一个字符串参数,可是却说我提供了四个。再看仔细点,说提供了四个,正好字符串’tony’是四个字符。

解决

翻了翻文档,发现也给出了一个占位符查询的例子如下:

t = (’RHAT’,)
c.execute(’SELECT * FROM stocks WHERE symbol=?’, t)

所以将字符参数放到元组中就可以了,修改如下:

c.execute('select * from person where p_name = ?', ('tony'))

结果依旧是抛出了同样的异常。再仔细看下,漏了个’,’,于是加上:

c.execute('select * from person where p_name = ?', ('tony',))

这次终于得到最终的结果了,其中的字符为unicode类型:

(1, u'tony')

原因

但是为什么?Python中的sqlite3模块提供了对sqlite数据操作的API,执行查询的函数是在sqlite3模块源码中定义的,很明显想要知道为啥,最好的方式是去看sqlite3模块的源码。我手上的Python源码是Python-2.7.4,在源码 Python-2.7.4/Modules/_sqlite/cursor.c 的函数PyObject* _pysqlite_query_execute(pysqlite_Cursor* self, int multiple, PyObject* args)中497-529行:

...

/* execute() */
if (!PyArg_ParseTuple(args, "O|O", &amp;operation, &amp;second_argument)) {
    goto error;
}

if (!PyString_Check(operation) &amp;&amp; !PyUnicode_Check(operation)) {
    PyErr_SetString(PyExc_ValueError, "operation parameter must be str or unicode");
    goto error;
}

parameters_list = PyList_New(0);
if (!parameters_list) {
    goto error;
}

if (second_argument == NULL) {
    second_argument = PyTuple_New(0);
    if (!second_argument) {
        goto error;
    }
} else {
    Py_INCREF(second_argument);
}
if (PyList_Append(parameters_list, second_argument) != 0) {
    Py_DECREF(second_argument);
    goto error;
}
Py_DECREF(second_argument);

parameters_iter = PyObject_GetIter(parameters_list);
if (!parameters_iter) {
    goto error;
}

...

从这段源码中可以看到这段代码将参数args解析成为Python的一个元组作为parameters_list,然后最这个得到的元组进行iter操作,不断地读取这个元组的元素作为参数,而Python中对一个字符串进行 parse tuple 会怎样?我觉得PyArg_ParseTuple这个函数的操作和以下代码会是类似的:

>>> tuple('test')
...('t', 'e', 's', 't')

所以现在我们可以看到我们的答案了,sqlite3模块中,cursor对象的execute方法会接受两个参数,第二个参数会被PyArg_ParseTuple函数转化成为Python中的tuple。而对一个字符进行tuple parse 导致的结果是将这个字符串的每个字符作为tuple的一个元素,所以上面抛出错误的时候提示的提供了四个所以错误也可以理解了。那为什么’(‘tony’)’同样是错误呢?如下:

>>> type(('tony'))
...<type 'str'>
>>> type(('tony',))
...<type 'tuple'>

很明显,(‘tony’)是一个str也就是一个字符串,相当于是’tony’,而(‘tony’,)才是一个单元素的tuple。同样,因为:

>>> tuple(['tony'])
...('tony',)

所以如果那一行查询执行改为:

c.execute('select * from person where p_name = ?', ['tony'])

同样也是可以执行成功的。

-EOF-

Tags: python sqlite3

Python中的New-style和Old-style classes

使用super()的错误

super函数是python中的一个内置函数,提供对继承的类的函数调用,特别是在子类中被overridden的夫类函数,比如

__init__()

最近在使用super函数的时候出现了个错误,例如下:

>>> class Base:
...     def __init__(self):
...         self.num = 1
... 
>>> class Next(Base):
...     def __init__(self):
...         super(Next, self).__init__()
... 
>>> obj = Next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in __init__
TypeError: must be type, not classobj

可以看到抛出了参数类型错误的错误.一开始完全不知所措,然后将出错信息google了一下,找到了解决方式:

>>> class Base(object):
...     def __init__(self):
...         self.num = 1
... 
>>> class Next(Base):
...     def __init__(self):
...         super(Next, self).__init__()
... 
>>> obj = Next()
>>> obj.num
1
>>> 

简单地将Base继承object就可以解决这个错误.其实这是python中的NewStyle classes和OldStyle classes而导致的一个问题. super()函数只适用于NewStyle classes.

Newstyle和Oldstyle

Python中,直至python2.1,类和类型是两种不相关的概念,例如:

>>> class Test:
...     pass
... 
>>> Test().__class__
<class __main__.Test at 0xb77373ec>
>>> type(Test())
<type 'instance'>
>>> type(type(Test()))
<type 'type'>
>>> 

在这里Test类是Oldstyle的类.可以看到,Test类的一个实例,它的类是Test,但是type却是instance.这是因为Oldstyle的类与类型是不统一的概念,Oldstyle的实例是独立于它们的类,由一个python内置类型instance实现的.

从2.2开始,python开始使用New-style来统一类和类型.对于一个New-style的类,它的实例的类型和类都是一致的.为了兼容之前的代码,在python2.2之后,默认的类定义还是Old-style的类.而一个New-style的类可以通过继承一个New-style的类或者在类继承中最顶端继承object来实现,如下:

>>> class Test(object):
...     pass
... 
>>> Test().__class__
<class '__main__.Test'>
>>> type(Test())
<class '__main__.Test'>
>>> type(type(Test()))
<type 'type'>
>>> 

New-style类的提出是为了统一python的对象模型.在python3中,Old-style类已经完全移除了.

参考资料:

http://docs.python.org/2/library/functions.html#super http://docs.python.org/2/reference/datamodel.html#newstyle http://stackoverflow.com/questions/9698614/super-raises-typeerror-must-be-type-not-classobj-for-new-style-class http://stackoverflow.com/questions/9699591/instance-is-an-object-but-class-is-not-a-subclass-of-object-how-is-this-po/9699961#9699961

CSAPP读书笔记:计算机系统中的抽象-操作系统

初言

我们使用着计算机系统提供的种种功能,安装不同的操作系统,使用不同的软件,听歌,上网,看视频,似乎理所当然.我们也知道,信息时代是建立在0和1的基础之上的,我们的计算机系统也是遵循着0和1的二进制.但是这两者是如何关联到一起的?当我们启动一个软件的时候,计算机系统底层是怎样的?我们打开一个网页如此的简单,但是这背后,计算机系统又发生了什么事情?

程序的执行

如果是计算机系的学生,或者对计算机技术有着兴趣的人,都会知道计算机操作系统的一些概念,也知道一个程序的执行其实到底是怎么一回事.无非就是将一段在硬盘上的二进制代码加载到内存中,然后由CPU执行相关的指令.程序的执行简单来说就是这么一回事,所以一个软件的启动和执行,也就是在这个简单的基础上再加上一些复杂的操作.

更深入点,我们知道操作系统也是软件,计算机关闭的时候操作系统的编译后的可执行对象也是保存在硬盘上.在计算机启动的时候,将操作系统加载到内存上,之后,操作系统就会一直运行直至计算机重新关闭.一般来说,我们将程序运行分为两种状态,用户的应用程序运行在用户态,而操作系统则是运行在内核态.

操作系统的抽象

计算机系统中的抽象其实应该是涉及两个方面.一个是处理器方面的,处理器的指令集对于硬件的抽象;而另一方面则是操作系统方面的抽象.

正如上面提及到的,程序运行于两种状态,这是为了安全的考虑,用户态的用户程序是无法直接进行一些直接操作硬件的指令的.比如创建保存一个文件的操作,涉及到了IO操作,而保存在硬盘上也涉及到磁盘的寻道.这些操作完全交由用户来进行一方面是非常的不安全,另一方面,每个人都有自己的实现方式,那将会导致各种混乱的代码.所以,操作系统一般会通过提供一些系统调用函数给用户程序,用户程序通过系统API从而实现对系统代码的调用.而这些系统代码将会进行相关的底层操作.通过系统API,操作系统作为硬件和用户应用程序的中间层,对用户应用程序隐藏了对硬件的操作,将硬件的操作细节抽象为一个个系统调用.

操作系统的抽象是计算机系统中非常重要的一个概念,总结来说大概有三个方面的抽象:

  • 文件对于IO设备的抽象

IO设备包括硬盘等设备,操作系统将这些设备都抽象为文件.比如硬盘上的数据保存是以0和1的方式保存在不同的磁道或者区域中的,操作系统将这些数据抽象成一个个文件.相关的IO操作也抽象成了文件的操作,复杂具体的底层操作隐藏在一个个简单的系统调用函数在之下.

  • 虚拟内存对于内存和硬盘的抽象
  • 进程对于处理器,内存和IO设备的抽象

-EOF-

python内置函数reduce

原型

reduce函数原型是reduce(function, iterable[, initializer]),返回值是一个单值.使用例子如下:

>>> print reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
15

可以看到通过传入一个函数和一个list, reduce函数返回的是这个list的元素的相加值.注意lambda函数是有两个参数的,如果我们改成一个参数会怎么样?如下:

>>> print reduce(lambda x: x, [1, 2, 3, 4, 5])
Traceback (most recent call last):
File "", line 1, in 
TypeError: () takes exactly 1 argument (2 given)

结果是抛出了错误,提示lambda函数只接受一个参数却给了两个参数.所以在reduce内部中,我们可以知道对于作为参数的function,接受了两个值作为参数的.

深入

第一个例子中,reduce函数返回的是list变量元素的和,那reduce函数是如何实现将这个list变量元素相加起来呢?考虑到定义的匿名函数体中将x的值和y的值加起来了,所以应该和这个函数是相关的,那reduce函数给赋给这个lambda函数的两个参数分别是什么呢?

>>> l = []
>>> def fun(x, y):
...     l.append((x, y))
...     return x + y
... 
>>> result = reduce(fun, [1, 2, 3, 4, 5])
>>> result
15
>>> l
[(1, 2), (3, 3), (6, 4), (10, 5)]

通过这个例子,可以看出答案已经很明显了.在reduce函数内部,对lambda函数的调用一共有四次:

fun(1, 2)     #x = 1, y = 2,x是list的第一个元素,y是第二个元素
fun(3, 3)     #x = 3, y = 3,x是上一次调用返回值1+2,y是第三个元素
fun(6, 4)     #x = 6, y = 4,同上,y是第四个元素
fun(10, 5)    #x = 10, y = 5,同上,y是第五个元素

最后得到reduce函数的返回值15,也就是fun函数的第四次调用的返回值.所以现在我们知道了,reduce函数对作为参数的函数是有要求的,要求这个函数接受两个参数.第一个参数的值是累积的值,而第二个参数的值是reduce函数参数中的序列的下一个元素.其实reduce函数中还有第三个可选的参数初始值,如果这个参数为空则初始值默认为序列的第一个元素,所以上面可以看到第一次调用这个函数是以序列的第一个和第二个元素作为参数的.最终,最后一次调用返回的值作为reduce函数的返回值.

定义

reduce函数可以参考下面的定义(来自官网):

def reduce(function, iterable, initializer=None):
it = iter(iterable)
if initializer is None:
    try:
        initializer = next(it)
    except StopIteration:
        raise TypeError('reduce() of empty sequence with no initial value')
accum_value = initializer
for x in it:
    accum_value = function(accum_value, x)
return accum_value

reduce函数对function的调用次数为iterable参数的长度n减1.

参考资料:

python build-in function reduce

python functional programming

-EOF-