Python源码学习(三)

函数

typedef struct {
    PyObject_HEAD
    PyObject *func_code;	/* 函数编译后的 PyCodeObject对象 */
    PyObject *func_globals;	/* 函数运行时的global名字空间 */
    PyObject *func_defaults;	/* 默认参数(tuple或NULL) */
    PyObject *func_closure;	/* NULL or a tuple of cell objects 用于实现闭包 */
    PyObject *func_doc;		/* 文档 */
    PyObject *func_name;	/* 名称 */
    PyObject *func_dict;	/* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist;	/* List of weak references */
    PyObject *func_module;	/* The __module__ attribute, can be anything */
} PyFunctionObject;
typedef struct {
    PyObject_HEAD
    int co_argcount;		/* 位置参数个数 */
    int co_nlocals;		/* 局部变量个数 */
    int co_stacksize;		/* 执行该段代码需要的栈空间 */
    int co_flags;		/* CO_..., see below */
    PyObject *co_code;		/* 编译得到的字节码指令序列 */
    PyObject *co_consts;	/* 代码块中所有常量 */
    PyObject *co_names;		/* 代码块中所有符号 */
    PyObject *co_varnames;	/* 局部变量名集合 */
    PyObject *co_freevars;	/* 实现闭包 */
    PyObject *co_cellvars;      /* 内嵌函数引用的局部变量名集合 */
    /* The rest doesn't count for hash/cmp */
    PyObject *co_filename;	/* py文件的完整路径 */
    PyObject *co_name;		/* 函数名或类名 */
    int co_firstlineno;		/* 起始行 */
    PyObject *co_lnotab;	/* 字节码指令和源文件行号的对应关系 */
    void *co_zombieframe;     /* for optimization only (see frameobject.c) */
    PyObject *co_weakreflist;   /* to support weakrefs to code objects */
} PyCodeObject;

Python语言中,使用def关键字定义函数,增加缩进表示语句块开始,减少缩进表示结束,函数的返回值装在元组里,按位赋值。函数传参时,除了有位置参数,还有默认参数,可变参数,关键字参数和命名关键字参数(跟在可变参数后,或者加一个特殊分隔符*)
-args和**kwargs变长可选参数,args收集额外位置参数组成元组,kwargs手机额外的关键字参数组成字典。如果想要将可选参数进行传递,就要带上解包操作符和**传递出去。
可以看下这个PyfunctionObjcet 和PyCodeObject,PyCodeObject这个对象记录了源代码的静态表示,包括赋值语句,还有对应名字空间之中的符号和值。它是import module进行动态加载时,生成的.pyc文件在内存中的表示,是一个对象。这些信息是编译时候就得到的。
而PyfunctionObject是运行时动态产生的。在执行def时候,会创建这个PyfunctionObject对象。它除了指向函数代码对应的PyCodeObject对象,还包含了一些函数执行时必须的上下文信息,比如func_globals,就是函数执行时关联的global作用域,这部分中的对应关系必须运行时才能确定。一个函数每次调用都会产生一个PyFunctionObject对象,它们指向同一份PyCodeObject,

a = 1

def g():
	print(a)

def f():
	print(a)	#编译错误
	a = 2
	print (a)

g()
f()

在函数中,我们可以定义跟全局变量重名的局部变量。像这个例子,都是在函数中调用a,函数g可以正常输出,函数f的第一行print就会抛出异常“局部变量没有赋值”,如果用的IDE还会直接提示编译错误。
在python中有一个很重要的概念叫做名字空间,在语法层面也被称为作用域。实际上是一个PyDictObject对象。假设这个文件是A.py,这个module被加载到python中,会以一个module对象的形式存在内存中,这个对象A中维护着一个名字空间,也就是一个字典,a=1这个赋值语句就组成了一个约束(a,1),key是a,值是1,位于这个命名空间中,同样的,函数g,函数f也都是赋值语句,也都是约束,存在于这个命名空间中。在A的内部,也存在两个作用域,分别是函数g的代码块和函数f的代码块,他们也豆有一个名字空间,在f代码块中,赋值语句a=2也是一个约束,这个约束起了作用,所以在函数f中的a是f的名字空间中的a,是一个局部变量。那为啥f函数第一行的print(a)会编译错误呢,这是因为这个约束是由源程序的文本决定的,而不是运行时动态决定的。也就是说函数f执行之前,python已经f创建一个名字空间,此时执行代码块,所使用的的就是局部变量。
在python中有三个作用域,module对应的源文件定义了一个作用域,就是global作用域,函数定义的作用域,是local作用域,python自身还定义了一个最高层的作用域builtin内建空间,主要是python自带的内置命名空间包括内置函数,异常类。在查找名字对应的约束时,会按照local,global,builtin这样的顺序。

a = 1

def f():
	a = 2
	def g():
		print(a)	#输出2
	return g

func = f()
func()

还有一个作用域是嵌套函数形成闭包的作用域,函数f返回了一个函数g,函数g定义的作用域内嵌于函数f的作用域之内,所以引用的a是函数f定义的作用域范围内的约束。所以这里func虽然是在外层执行,但是他实际的作用域是嵌套作用域。
另外如果想要在函数作用域内访问global作用域的变量,需要在函数内共用global关键字重新声明,python会忽略这个作用域的规则,直接参考global名字空间。

虚拟机

在这里插入图片描述

Python是一门跨平台的语言,本质上它和java和C#一样,先把源码编译成字节码,然后启用虚拟机执行字节码。
当把py文件作为模块导入时,会在同目录的pycache中生成一个pyc文件。它是被序列化的代码对象PyCodeObject,在运行时直接反序列化就可以得到代码对象,避免重复编译。
编译器根据语法规则,对源码进行作用域的划分,每个作用域作为一个单位生成代码对象。每个代码对象都保存了字节码,名字,常量等一些静态的上下文信息。
编译器把源码编译成代码对象后,虚拟机会先创建一个栈帧对象,用来执行代码对象。我们知道类中可能包含着方法,方法中同样可能嵌套着方法。所以在执行代码对象时发生了函数调用,虚拟机就会新建一个栈帧对象,开始执行新的代码对象,这样一层层调用,会形成一个调用链,每个函数执行完毕后,虚拟机会通过back字段找到前一个栈帧对象,回到调用者代码中继续执行。

pi = 3.14
r = 3
area = pi * r ** 2

-----------------------------------------

1           0 LOAD_CONST               0 (3.14)
            2 STORE_NAME               0 (pi)

2           4 LOAD_CONST               1 (3)
            6 STORE_NAME               1 (r)

3           8 LOAD_NAME                0 (pi)
           10 LOAD_NAME                1 (r)
           12 LOAD_CONST               2 (2)
           14 BINARY_POWER
           16 BINARY_MULTIPLY
           18 STORE_NAME               2 (area)
           20 LOAD_CONST               3 (None)
           22 RETURN_VALUE

可以看一下这几行顺序执行语句编译出的字节码,字节码指令和汇编语言有点像,最左边是字节码的编码,总共三条。Load const是操作码,0是操作数。执行这条指令时会根据操作数0作为常量数组的下标,找到3.14,压入栈帧对象的临时栈,接下来storename,会把临时栈顶的元素弹出保存到名字空间,也是通过操作数取到。下面步骤类似,就不赘述了。除此之外还有分支和循环指令,大家有兴趣可以看一下。

线程安全

Python虚拟机维护了一个全局锁GIL,python线程想在虚拟机执行字节码,必须取得全局锁。所以无论任何时刻,只有一个线程在虚拟机中运行。
Python的线程调度会在一个线程取得全局锁开始执行字节码的时候进行计数,执行的字节码达到一定数量,就会主动释放锁,并唤醒其他线程。所以python并没有多核并行执行的能力。
如何避免GIL的影响:1.使用多进程模式,每个进程都独立运行一个虚拟机,所以不同python进程可以在多核CPU上并行运行,不收GIL限制。
线程安全,由于GIL全局锁会对线程执行一定数量的字节码然后切换,所以内建类型的基本操作,都是一行字节码可以完成的,所以这些基本操作是线程安全的。但是在某些场景中,比如最常见的+=语句,需要一读一写来完成,就需要在应用程序上层加锁,让它编程一个原子操作,不然内部的数据结构就可能被破坏。
++++++++++++++++++++++++++++++++++++++++++
Python的内存分为3部分:内存分配器;对象分配器;特定对象分配器。通俗讲就是内存池,内存分配和对象池。
Python为了避免内存碎片化,将小于512的内存分配,托管给内存池,大于512的直接调用malloc函数。
Python以8字节为梯度,将内存块分为64个类别。以符合的最小单位分配内存,
Python每次申请一个内存页,然后划分成统一尺寸的内存块,一个内存页大小4k
++++++++++++++++++++++++++++++++++++++++

列表生成式

相当于语法更加简化紧凑的for循环,可以新增条件过滤元素,也可以多层嵌套。还可以用于生成集合和字典,非常方便,节省很多代码。但是这种方法生成的列表所有元素都占着内存。生成器就可以解决这个问题,只返回一个可迭代对象,每次迭代调用next函数时,才计算生成元素,与列表解析式一样,也可以用判断语句对生成的值进行过滤,也可以多层嵌套。可以看到不同的地方就是,列表生成式是中括号,生成器是圆括号。需要注意的是生成器一旦经过使用,就不能重新启动,不能复用。
刚才生成器生成的是一个可迭代对象,调用next函数可以返回下一个值,这样的对象称作迭代器,那我们如果把多个迭代器进行链接,就可以创造一个管道,多次的返回值可以看做生成器声明周期产生的“数据流”,流进下一个生成器进行下一步处理,每一个元素在里面单向流动,这样的管道,成为迭代器链。与生气器一样不能复用,而且也不能使用函数参数进行配置。

动态类型

python中赋值语句进行的操作,左侧的变量存储了右侧对象的地址,是一个引用关系,可以对同一个变量随便更改数据类型,只在运行时才确定数据类型。把函数本身赋值给变量,调用变量等价于调用函数,所以函数名其实也是变量,既然是变量就可以传递,让函数的参数能够接受别的函数,这也就是高阶函数的特性。
Python内建了map/reduce、filter(飞优特)和sort函数,可以把函数当做参数传递,可以对传入函数的元素进行新一轮加工,返回一个新的迭代器。除了把函数当做参数传递,还可以把函数当做返回值,返回的函数还可以引用外层函数的局部变量,产生闭包。

装饰器

装饰器的一大用途就是将通用的功能应用到现有的类或者函数的行为上。因为函数是对象,可以分配给变量传递给其他函数,并且从其他函数返回;函数内部也可以定义函数,而且子函数可以捕捉父函数的局部状态。所以装饰器就是在此特性上实现的。其本质就是在将被包装函数作为输入,并返回另一个可调用对象,并运行之前和之后执行一些代码。@装饰函数,是一个语法糖,可以简化的使用装饰器,先定义函数然后运行装饰器,但是定义时就立即修饰该函数也导致很难访问未装饰的原函数,所以如果想要保留未装饰函数的能力,还是要手动装饰。
一个函数也可以使用多个装饰器,多个装饰器的应用顺序是从下向上
使用装饰器时,实际上是使用一个函数替换另一个函数,这个过程会隐藏原函数所附带的一些元数据,因为都被闭包的元数据代替了。可以在自己的装饰器中使用functools.wraps把将要丢失的元数据复制到装饰器闭包中。
Functools中的偏函数,可以把函数的某些参数固定住,返回一个新的函数。可以对函数进行简化,针对一些函数参数个数太多的情况。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值