python高级语言特性
变量,对象和引用
引用是自动形成的从变量到对象的指针。——《Learning Python》
在Python中,赋值(=)操作时,从变量到对象自动建立的连接关系,称为引用。
引用是一种关系,以内存中的指针的形式实现。
类型是属于对象,而不是变量。变量只是对对象的一个引用。
对象有可变对象和不可变对象之分。
在 Python 中,一切皆为对象
Python 中不存在值传递,一切传递的都是对象的引用,也可以认为是传址
python中,连数字都是对象!
a=1的意思是把变量a绑定到整数对象1上(形象理解,给内存中的整数对象1贴上一个名字叫做a的标签)
Python中,从概念上,a = 1 反映所有赋值语句执行的三个步骤:
- 创建一个对象来代表值 1 。
2.创建一个变量 a ,如果它还没有创建的话。
3.将变量 a 与 新的对象 1 相连接。
参数传递,可变对象,不可变对象
不可变对象:
可变对象:
i = 5 # create int(5) instance, bind it to i
j = i # bind j to the same int as i
j = 3 # create int(3) instance, bind it to j
print i # i still bound to the int(5), j bound to the int(3)
i = [1,2,3] # create the list instance, and bind it to i
j = i # bind j to the same list as i
i[0] = 5 # change the first item of i
print j # j is still bound to the same list as i
值传递和址传递
函数参数传递机制问题在本质上是调用函数(过程)和被调用函数(过程)在调用发生时进行通信的方法问题。基本的参数传递机制有两种:值传递和引用传递。
值传递(passl-by-value)过程中,被调函数的形式参数作为被调函数的局部变量处理,即在堆栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
引用传递(pass-by-reference)过程中,被调函数的形式参数虽然也作为局部变量在堆栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过堆栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
python传递的参数不是值传递也不是址传递,而是传递的对象的引用。python的引用传递和址传递的区别是:引用传递,可以有多个引用指向同一块内存区域,而址传递一个内存区域只有唯一的一个地址
immutable和mutable对象其实没有本质区别
python无法知道自己在操作的对象是可变还是不可变
Python是不允许程序员选择采用传值还是传址的。Python参数传递采用的肯定是传对象引用的方式。无论这个对象是可变还是不可变,参数传递时传的都是同一个object。
如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值——相当于传址。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用(其实也是对象地址!!!),就不能直接修改原始对象——类似于传值。
浅拷贝和深拷贝
浅拷贝:只复制顶层的对象,对于有嵌套数据结构,内部的元素还是原有对象的引用,这时候需要特别注意
深拷贝:复制了所有对象,递归式的复制所有对象。复制后的对象和原来的对象是完全不同的对象。对于不可变对象来说,浅拷贝和深拷贝都是一样的地址。但是对于嵌套了可变对象元素的情况,就有所不同
装饰器
装饰器是什么
一个猴子孙悟空怎么被装饰器变成齐天大圣的
一开始的孙悟空是只会吃桃子的普通猴子,
def 孙悟空():
print('吃桃子')
孙悟空()
# 输出:
# 吃桃子
但是仅仅是会吃桃子的猴子是没用的,我们也可以把输出改成掌握七十二变,但是这样每次都要修改非常麻烦。而且也破坏了原来的猴子孙悟空。
因此我们用另外一段代码来装饰猴子孙悟空
去找菩提老祖学技术,掌握了七十二变:
def 拜菩提老祖(func):
print('出发去师傅房间')
def 三更传道(*args, **kwargs):
print('掌握七十二变了')
return func(*args, **kwargs)
return 三更传道
怎样使用这个装饰功能呢?
①第一种(本质):
七十二变孙悟空 = 拜菩提老祖(孙悟空)
七十二变孙悟空()
# 输出:
# 出发去师傅房间
# 掌握七十二变了
# 吃桃子
本质是把孙悟空这个函数作为参数传给拜菩提老祖函数,结果输出了一个新的函数:七十二变孙悟空
②第二种(语法糖):
但是这样写有点麻烦,因此Python官方出了一种叫做装饰器的快捷代码,也就是语法糖,用了语法糖就变成了下面这样:
@拜菩提老祖
def 孙悟空():
print('吃桃子')
孙悟空()
# 输出:
# 出发去师傅房间
# 掌握七十二变了
# 吃桃子
孙悟空不断进化,被很多东西装饰
def 龙宫走一趟(func):
print('出发去东海龙宫')
def 大闹龙宫(*args, **kwargs):
print('得到如意金箍棒了')
return func(*args, **kwargs)
return 大闹龙宫
def 炼丹炉(func):
print('出发去太上老君实验室')
def 烈火焚身(*args, **kwargs):
print('有火眼金睛了')
return func(*args, **kwargs)
return 烈火焚身
*args, **kwargs是参数列表,这里的函数没有传参数,写上也不影响,建议都写上
先学七十二变,再去龙宫拿金箍棒,最后炼得火眼金睛。
# @炼丹炉
# @龙宫走一趟
# @拜菩提老祖
def 被装饰的孙悟空():
print('吃桃子')
孙悟空()
# 输出:
# 出发去师傅房间
# 出发去东海龙宫
# 出发去太上老君实验室
# 有火眼金睛了
# 得到如意金箍棒了
# 掌握七十二变了
# 吃桃子
可以观察到尽管我们按时间顺序叠加了装饰器,但是得到东西的顺序似乎和孙悟空去每个地点的顺序不一致。
彩蛋:chatgpt搞不清楚这三个事件的顺序
发生了什么?
那么这里涉及到叠加装饰器的运行顺序,以及装饰器究竟是怎么运行的,在哪个阶段被运行的。我们逐渐分析:
关于执行顺序:根据装饰器的本质,这里实际上发生的是:
七十二变孙悟空 = 拜菩提老祖(孙悟空)
七十二变金箍棒孙悟空 = 龙宫走一趟(七十二变孙悟空)
七十二变金箍棒火眼金睛孙悟空 = 炼丹炉(七十二变金箍棒孙悟空)
龙宫走一趟装饰的是拜菩提老祖的装饰结果,
炼丹炉装饰的又是龙宫走一趟装饰的结果,嵌套。
总之,装饰的顺序参考装饰器的本质,从内部一层层被外部封装的,写多个装饰器的时候,先写的装饰器在外层,后写的在内层。
关于多个装饰器修饰一个函数总结要点:
-
当一个函数被多个装饰器装饰时,装饰器的加载顺序是从内到外的(从下往上的)。其实很好理解:装饰器是给函数装饰的,所以要从靠近函数的装饰器开始从内往外加载
-
外层的装饰器,是给里层装饰器装饰后的结果进行装饰。相当于外层的装饰器装饰的函数是里层装饰器的装饰原函数后的结果函数(装饰后的返回值函数)。
装饰器的本质
nothing but a syntactic sugar(语法糖)
装饰器不是任何额外实现的新机制,只是对已有的函数定义函数这个固定流程的一种简易封装。所以一切装饰器的原理和过程都应该还原成褪去语法糖的原始代码来思考
语法糖(Syntactic sugar):
也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。 通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。
python装饰器能成立是因为python中一切皆对象,函数也只不过是一个普通的FunctionObject。所以函数本身可以作为返回值,装饰器就是一个以函数作为输入,也以函数作为输出的函数。
-- 码农高天教程
装饰器的执行流程
第一个问题:装饰器在代码运行的哪个阶段被运行?
省流:在模块导入时,运行时开始之前就运行了
函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。这突出了Python 程序员所说的导入时和运行时之间的区别。
本质:导入时和运行时(importTime,runTime)
看个代码就懂了:
decreator.py
#猴子孙悟空()
def 龙宫走一趟(func):
print('出发去东海龙宫')
def 大闹龙宫(*args, **kwargs):
print('得到如意金箍棒了')
return func(*args, **kwargs)
return 大闹龙宫
def 拜菩提老祖(func):
print('出发去师傅房间')
def 三更传道(*args, **kwargs):
print('掌握七十二变了')
return func(*args, **kwargs)
return 三更传道
def 炼丹炉(func): # func就是‘孙悟空’这个函数
print('出发去太上老君实验室')
def 烈火焚身(*args, **kwargs):
print('有火眼金睛了')
return 烈火焚身
@炼丹炉
@龙宫走一趟
@拜菩提老祖
def 被装饰的孙悟空():
print('吃桃子')
#等同于下面的语法糖
def test():
return
if __name__ == '__main__':
test()
# 输出
# 出发去师傅房间
# 出发去东海龙宫
# 出发去太上老君实验室
可以看到,把这个函数decreator.py当做脚本运行时只运行了一个直接返回的函数test(),但是装饰器的代码已经被加载了。
因此,当装饰器与被装饰的函数放在同一个文件时,装饰器先于所有的函数而运行。也就是在导入时运行
如果从外部引入的模块里含有装饰器,那么装饰器会被全部运行
如果从把外部把decreator.py当作包来import,什么也不干也会输出 :
# 出发去师傅房间
# 出发去东海龙宫
# 出发去太上老君实验室
这证明了python在导入模块阶段就运行了装饰器的代码
流畅的Python:
装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即Python 加载模块时)。也就是说只定义装饰器和被装饰的函数,不运行被装饰的函数,装饰器里的代码都会运行。
第二个问题:多个装饰器堆叠的时候,执行顺序如何?
这部分还不太懂,我猜要从字节码和虚拟机栈帧去解释,这里有一篇好文,先存着:
https://blog.csdn.net/shahuzi/article/details/81254557
Stack Overflow
- When the interpreter is evaluating the decorated method definition the decorators are evaluated from bottom --> top 当解释器评估一系列堆叠装饰器的定义时,装饰器定义的评估顺序:底部 -> 顶部。这应该就是importTime导入时
- When the interpreter calls the decorated method the decorators are called from top --> bottom. 当解释器调用被装饰的方法时,这个方法的一系列装饰器的调用顺序:顶部 --> 底部。这应该就是runtime运行时
print("========== Definition ==========")
def decorator(extra):
print(" in decorator factory for %s " % extra)
extra = " %s" % extra
def inner(func):
print(" defining decorator %s " % extra)
def wrapper(*args, **kwargs):
print("before %s -- %s" % (func.__name__, extra))
func(*args, **kwargs)
print("after %s -- %s" % (func.__name__, extra))
return wrapper
return inner
@decorator('first')
@decorator('middle')
@decorator('last')
def hello():
print(' Hello ')
print("\n========== Execution ==========")
hello()
以上程序的输出结果:
========== Definition ==========
in decorator factory for first
in decorator factory for middle
in decorator factory for last
defining decorator last
defining decorator middle
defining decorator first
========== Execution ==========
before wrapper -- first
before wrapper -- middle
before hello -- last
Hello
after hello -- last
after wrapper -- middle
after wrapper -- first
装饰器的其他应用场景:后端开发的权限验证
from django.contrib.auth.decorators import login_required
@login_required(login_url='/userprofile/login/')
def article_create(request):
意思是从外部库里import login_required这个函数,然后把这个函数作为装饰器修饰我们本来的文章创建视图函数,使得只有登录的用户才有权限新增文章(而不是把权限控制嵌入原来的代码,这样没有任何复用性可言)
如果要叠多重验证,只需把后加的验证加在原来的装饰器前面逐层装饰即可。
闭包
- 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。
维基上的解释:
- 在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。
闭包就是能够读取其他函数内部变量的函数
闭包,又称闭包函数或者闭合函数,其实和前面讲的嵌套函数类似,不同之处在于,闭包中外部函数返回的不是一个具体的值,而是一个函数。一般情况下,返回的函数会赋值给一个变量,这个变量可以在后面被继续执行调用。
只有涉及嵌套函数时才有闭包问题。因此,很多人是同时知道这两个概念的。
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
理解闭包和nonlocal流畅的python上的编程实践:
任务:
- 设计一个可以存储历史数据的累积平均值计算器avg
初学者的思路:
- 写一个类,然后把数据存在类的属性里面,用类的方法去访问这个属性,然后做累积平均
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)
使用闭包的性质,可以写出一种不需要类的avg:
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
但是累积的数据存在哪里呢?
自由变量
在make_averager 函数中,series是它的局部变量,因为那个函数的定义体中初始化了
series:series = []。可是,调用avg(10) 时,make_averager 函数已经返回了,而它的本地作用域也一去不复返了。
在averager 函数中,series 是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量,参见下图。
averager 的闭包延伸到那个函数的作用域之外,包含自由变量series 的绑定
审查返回的averager 对象,我们发现Python 在__code__ 属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
series 的绑定在返回的avg函数的__closure__属性中。avg.__ closure__ 中的各个元素对应于avg.__ code__.co_freevars 中的一个名称。这些元素是cell 对象,有个cell_contents 属性,保存着真正的值。
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
所以,答案是:
闭包函数中自由变量的绑定保存在闭包函数的avg.__ closure__属性中,而且这个属性里的cell对象实实在在地保存了这些自由变量真正的值
但是这样写的代码每次要对series做求和操作,很费事,考虑这个问题的本质,其实只需要一个整数就可以存下累积的和了,那么代码应该怎么写?
def make_averager_nonlocal():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
# 这句不加会报错
# UnboundLocalError: local variable 'count' referenced before assignment
count += 1
total += new_value
return total/count
return averager
这里加上了nonlocal操作,如果不把count和total声明为nonlocal变量,则会报错:
UnboundLocalError: local variable 'count' referenced before
当count 是数字或任何不可变类型时,count += 1 语句的作用其实与count =count + 1 一样。因此,我们在averager 的定义体中为count 赋值了,这会把count 变成局部变量。total 变量也受这个问题影响。
前一种闭包实现,没遇到这个问题,因为我们没有给series赋值,我们只是调用series.append,并把它传给sum和len。也就是说,我们利用了列表是可变的对象这一事实。
(挖坑:python基础变量的属性)
但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如count = count + 1,其实会隐式创建局部变量count。这样,count 就不是自由变量了,因此不会保存在闭包中。
为了解决这个问题,Python 3 引入了nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。
global/nonlocal/local/free variable的区别
global和non local的异同:
-
两个关键词都用于允许在一个局部作用域中使用外层的变量。
-
global 表示将变量声明为全局变量
-
nonlocal 表示将变量声明为外层变量/自由变量(外层函数的局部变量,而且不能是全局变量)
原理
python 在访问一个变量时,先要去定位这个变量来源于哪里。
python引用变量的顺序如下:
- 当前作用域局部变量(local variable/用var关键字声明)
- 外层作用域变量(闭包、free variable)
- 当前模块中的全局变量(global variable)
- python内置变量(builtins)
即优先从局部作用域中查找这个变量,如果没有的话,再去外层找,如果到了最后还没找到,则报错。
装饰器和闭包有什么关系?
装饰器就是一种的闭包的应用,只不过其传递的自由变量是函数:
若想真正理解装饰器,需要区分导入时和运行时(importTime,runTime),还要知道变量作用域、闭包和新增的nonlocal声明。掌握闭包和nonlocal不仅对构建装饰器有帮助,还能协助你在构建GUI程序时面向事件编程,或者使用回调处理异步I/O。
生成器
文件处理
import .abc这个表示导入当前文件夹下的一个包(而不是导入其他文件夹的包)。
python中面向对象编程(OOP)三特性(封装、继承、多态)的体现:
override和overload有什么区别?重写和重载
重写是子类把父类的原有方法颠覆
重载是另一个对象也想用同样的语法实现一种形似但实质不同的功能(比如字符串加法和整数加法都用的是+运算符)
override->重写(=覆盖)、overload->重载、polymorphism -> 多态
override是重写(覆盖)了一个方法,以实现不同的功能。一般是用于子类在继承父类时,重写(重新实现)父类中的方法。
重写(覆盖)的规则:
1、重写方法的参数列表必须完全与被重写的方法的相同,否则不能称其为重写而是重载.
2、重写方法的访问修饰符一定要大于被重写方法的访问修饰符(public>protected>default>private)。
3、重写的方法的返回值必须和被重写的方法的返回一致;
4、重写的方法所抛出的异常必须和被重写方法的所抛出的异常一致,或者是其子类;
5、被重写的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行重写。
6、静态方法不能被重写为非静态的方法(会编译出错)。
overload是重载,一般是用于在一个类内实现若干重载的方法,这些方法的名称相同而参数形式不同。
重载的规则:
1、在使用重载时只能通过相同的方法名、不同的参数形式实现。不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样);
2、不能通过访问权限、返回类型、抛出的异常进行重载;
3、方法的异常类型和数目不会对重载造成影响;
多态的概念比较复杂,有多种意义的多态,一个有趣但不严谨的说法是:继承是子类使用父类的方法,而多态则是父类使用子类的方法。
一般,我们使用多态是为了避免在父类里大量重载引起代码臃肿且难于维护。
————————————————
版权声明:本文为CSDN博主「Erik明」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ericbaner/article/details/3857268
python oop解说:
https://www.cainiaojc.com/python/python-object-oriented-programming.html
继承性
继承是一种创建新类的方法,用于在不修改现有类的细节的情况下使用它。新形成的类是一个派生类(或子类)。类似地,现有类是基类(或父类)。
python继承和多态:
https://www.liaoxuefeng.com/wiki/1016959663602400/1017497232674368
继承的应用
后端开发继承models,views,然后自己添加新特性,并复用那些写好的方法
测试框架unittest中继承testcase,方便地使用写好的各种assert方法
可封装性
在Python中使用OOP,我们可以限制对方法和变量的访问。这样可以防止数据直接修改(称为封装)。在Python中,我们使用下划线作为前缀来表示私有属性,即单“ _”或双“ __”。
多态性
父类和子类的实例化对象调用同名的方法,表现出不同的性质
多态的概念其实很广:
https://www.cainiaojc.com/python/python-polymorphism.html
什么是多态?
多态性的字面意思是指以不同形式出现的条件。
多态是编程中非常重要的概念。它指的是使用单个类型实体(方法,运算符或对象)来表示不同场景中的不同类型。
面向对象里的多态叫做‘类多态’
多态性是一种功能(在OOP中),可以将公共接口用于多种形式(数据类型)。
假设我们需要给一个形状上色,有多个形状选项(矩形,正方形,圆形)。但是,我们可以使用相同的方法为任何形状着色。这个概念称为多态。
java多态:
https://www.runoob.com/java/java-polymorphism.html
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。
Python虚拟机和字节码
Python 的虚拟机实际上是在模拟操作系统运行可执行文件的过程
CSAPP说:“虚拟机是对整个计算机的抽象”
如果对可执行文件的运行机理有一个大致的了解,那么对于理解Python 虚拟机的运行机理是相当有益的。
函数是一个对象,甚至一段代码也是对象
funtion Object, code object
对于一个函数而言,其所有对局部变量的操作都在自己的栈帧中完成,而函数之间的调用则通过创建新的栈帧完成。
在Python真正执行的时候,它的虚拟机实际上面对的并不是一个PyCodeObject对象,而是另一个对象——PyFrameObject。它就是我们所说的执行环境,也是Python对x86平台上栈帧的模拟。
字节码
pyc文件
Python 将代码对象序列化并保存到pyc文件,可以理解为字节码.
- pyc文件是由.py文件经过编译后生成的字节码文件,其加载速度相对于之前的.py文件有所提高,而且还可以实现源码隐藏,以及一定程度上的反编译。比如,Python3.3编译生成的.pyc文件,Python3.4就别想着去运行啦!→_→.
- pyo文件也是优化(注意这两个字,便于后续的理解)编译后的程序(相比于.pyc文件更小),也可以提高加载速度。但对于嵌入式系统,它可将所需模块编译成.pyo文件以减少容量。
调用方式:python -m py_compile example.py
为什么要有pyc文件?
简言之,从pyc或pyc运行文件并不比从py快,只是作为module被load的时候比py快。
因此,当.py文件第一次被导入时,它会被汇编为字节代码,并将字节码写入同名的.pyc文件中。后来每次导入操作都会直接执行.pyc 文件(当.py文件的修改时间发生改变,这样会生成新的.pyc文件),在解释器使用-O选项时,将使用同名的.pyo文件,这个文件去掉了断言(assert)、断行号以及其他调试信息,体积更小,运行更快。(使用-OO选项,生成的.pyo文件会忽略文档信息)
A program doesn't run any faster when it is read from a ‘.pyc’ or ‘.pyo’ file than when it is read from a ‘.py’ file; the only thing that's faster about ‘.pyc’ or ‘.pyo’ files is the speed with which they are loaded.
When a script is run by giving its name on the command line, the bytecode for the script is never written to a ‘.pyc’ or ‘.pyo’ file. Thus, the startup time of a script may be reduced by moving most of its code to a module and having a small bootstrap script that imports that module. It is also possible to name a ‘.pyc’ or ‘.pyo’file directly on the command line.
拓展,为什么加__init__py就变成包了
1、 有了__init__.py,那么这个文件夹 就变成了一个Python包
2、init.py里边的函数在import的时候,会被自动执行。
理解虚拟机和字节码的常用python代码
命名空间与作用域
python的命名空间用dict实现,基本上就是名字与对象的键值对,因此python的dict对于这个语言的效率来说非常重要,因此真正实现了O(1)查找,而C++的STL里的map没有这样的要求,它是用基于红黑树实现,红黑树具有自动排序的功能,因此map内部所有的数据,在任何时候,都是有序的。所以复杂度为 O(LogN)。
内置命名空间(Build-in)
内置(built-in),Python语言内置的名称,比如函数名abs、char和异常名称BaseException、Exception等等。
全局命名空间(Global)
全局(global),模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。
局部命名空间(Local)
局部(local),函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是)
我们在函数中引用一个变量时,Python 将隐式地默认该变量为全局变量。但是,一旦变量在没有global关键字修饰的情况下进行了赋值操作,Python 会将其作为局部变量处理。
闭包命名空间(Enclosing)
nonlocal
当一个变量在函数中被赋值时,Python 默认将其作为全局变量,既不是局部变量,也不是这里提到的闭包空间变量。所以,当我们在实际运行 bar 方法时,同样会得到 UnboundLocalError 异常。在这里如果想要使用 foo 函数中的 number 变量的话,需要使用 nonlocal 关键字进行修饰,
栈帧
PyFrameObject
在创建PyFrameObject对象时,额外申请的那部分内存中有一部分是给PyCodeObject对象中存储的那些局部变量的、co_freevars(自由变量,闭包里额外保存的变量)、co_cellvars使用的,而另一部分才是给运行时栈使用的。所以,PyFrameObject对象中的栈的起始位置(也就是栈底)是由f_valuestack维护的,而f_stacktop维护了当前的栈顶。
栈帧与闭包
当语句需要查找变量 X 时,将会按照 Local -> Enclosing -> Global -> Builtin 的顺序进行查找,俗称 LEGB规则。
GIL
线程安全
线程安全是指一段代码在多线程环境下仍然能够正常运行。
线程安全是多线程编程时的计算机程序代码中的一个概念。 在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
GIL让python的容器线程安全
从list.append()源码看GIL和线程安全:
在 Python 层面,list 、dict 等内建对象是线程安全的,这是最基本的常识。研究 list、dict 等内建对象源码时,我们并没有看到任何 互斥锁 的痕迹,这多少有点令人意外。
Python 线程调度实现方式参考了操作系统进程调度中 时间片 的思路,只不过将时间片换成 字节码 。当一个线程取得 GIL 全局锁并开始执行字节码时,对已执行字节码进行计数。
Python 线程调度实现方式参考了操作系统进程调度中 时间片 的思路,只不过将时间片换成 字节码 。当一个线程取得 GIL 全局锁并开始执行字节码时,对线程已执行字节码进行计数。
当执行字节码达到一定数量(比如 100 条)时,线程主动释放 GIL 全局锁并唤醒其他线程。其他等待 GIL 全局锁的线程取得锁后,将得到虚拟机控制权并开始执行。因此,虚拟机就像一颗软件 CPU ,Python 线程交替在虚拟机上执行:
对于 Running 线程,如果已执行字节码达到一定数量,则自动让出 GIL 并唤醒其他线程,状态变更为 Ready ;如果执行 IO 操作,在执行阻塞型系统调用前先让出 GIL ,状态变更为 IO Blocked 。
当 Ready 线程取得 GIL 后,获得虚拟机控制权并开始执行字节码,状态变更为 Running 。
IO Blocked 线程一开始阻塞在系统调用上,当系统调用返回后,状态变更为 Ready ,再次等待 GIL 以便获得虚拟机执行权。
绿色running为当前正在虚拟机中执行的线程;蓝色ready为等待到虚拟机上执行的线程;黄色为阻塞blocked在 IO 操作上的线程。
GIL带来了什么?又失去了什么?
带来了线程安全,没影响IO密集型程序的效率,失去了计算密集型程序的效率
根据程序分别处于 Running 以及 IO Blocked 两种状态的时间占比,可分为两种:
计算密集型 ,程序执行时大部分时间处于 Running 状态;
IO密集型 ,程序执行时大部分时间处于 IO Blocked 状态;
IO密集型 程序受 GIL 影响相对有限,因为线程在等待 IO 处理时可以让出 GIL 以便其他线程拿到虚拟机执行权
元编程
try, except, finally
try的代码块报异常
进入except处理
在except退出之前执行finally的逻辑,然后再执行except的return语句。
如果finally逻辑有return的话,会直接return退出,except的return语句是不会被执行到。
try + except + finally:
try 中无异常,不执行except
try中遇到异常,就停止当前语句, 去执行except中的语句
finally 的作用是,
try 和 except 执行中:
-
执行完最后一个语句后(如最后一条语句不是return)
-
return Value 前
都会触发finally的执行.
而在finally中的语句执行后,同样有两种情况:
1.如果finally中有return ,那么代码就在finally中返回。
2.如果finally中没有return ,那么代码会返回到触发finally的语句处,继续执行。
————————————————
https://docs.python.org/3/reference/compound_stmts.html#the-try-statement
https://stackoverflow.com/questions/11551996/why-do-we-need-the-finally-clause-in-python
If finally is present, it specifies a ‘cleanup’ handler. The try clause is executed, including any except and else clauses. If an exception occurs in any of the clauses and is not handled, the exception is temporarily saved. The finally clause is executed. If there is a saved exception it is re-raised at the end of the finally clause. If the finally clause raises another exception, the saved exception is set as the context of the new exception. If the finally clause executes a return, break or continue statement, the saved exception is discarded:
try {
// something
} finally {
// guaranteed to run if execution enters the try block
}
myfile = open("test.txt", "w")
try:
myfile.write("the Answer is: ")
myfile.write(42) # raises TypeError, which will be propagated to caller
finally:
myfile.close() # will be executed before TypeError is propagated
TCP_server.py
try:
while True:
print('Main Process, waiting for client connection...')
# client_sock是专为这个客户端服务的socket,client_addr是包含客户端IP和端口的元组
client_sock, client_addr = server_sock.accept()
print('Client {} is connected'.format(client_addr))
try:
while True:
# 接收客户端发来的数据,阻塞,直到有数据到来
# 事实上,除非当前客户端关闭后,才会跳转到外层的while循环,即一次只能服务一个客户
# 如果客户端关闭了连接,data是空字符串
data = client_sock.recv(4096)
if data:
print('Received {}({} bytes) from {}'.format(data, len(data), client_addr))
# 返回响应数据,将客户端发送来的数据原样返回
client_sock.send(data)
print('Sent {} to {}'.format(data, client_addr))
else:
print('Client {} is closed'.format(client_addr))
break
finally:
# 关闭为这个客户端服务的socket
# finally语句保证这段代码怎么样都会被执行,即使try里raise exception
client_sock.close()
finally:
# 关闭监听socket,不再响应其它客户端连接
server_sock.close()
内存管理
描述python虚拟的内存结构和内存管理策略
Python 虚拟机内存结构由两个部分组成:栈和堆。
栈是虚拟机内部的数据结构,用于存储函数调用的上下文。它是一个后进先出(LIFO)的数据结构,具有较高的内存分配和释放速度。
堆是一个用于存储动态分配的内存的数据结构。它在 Python 中用于存储 Python 对象和数据结构,例如字符串,列表,字典等。
Python 的内存管理策略基于引用计数。这意味着每个 Python 对象都有一个引用计数,该计数跟踪有多少变量和数据结构引用了该对象。当引用计数为零时,对象就可以被安全地回收。
除了引用计数,Python 还使用了一种名为垃圾回收的算法来额外管理内存。垃圾回收算法会周期性地扫描堆,回收那些没有任何引用的对象。
此外,Python 还提供了一个内存管理的 API,允许开发人员手动管理内存。例如,开发人员可以使用 del 语句删除引用,以便在未来的垃圾回收过程中回收该对象。
Python 虚拟机的堆与栈和cpython进程中的堆和栈有什么区别
CPython 进程中的堆和栈与 Python 虚拟机的堆和栈不同,但它们的概念相同。
CPython 进程的堆是操作系统的内存管理结构,用于动态分配内存。它是一个全局的内存池,在整个进程中可用,与 Python 虚拟机无关。
CPython 进程的栈是操作系统的内存管理结构,用于存储函数调用的上下文。它是一个后进先出(LIFO)的数据结构,在进程的生命周期内可用,与 Python 虚拟机无关。
相比之下,Python 虚拟机的堆和栈是 Python 解释器内部的数据结构,用于管理 Python 程序的内存。它们的作用范围只限于当前的 Python 解释器实例,并不适用于整个进程。
总的来说,CPython 进程的堆和栈是操作系统的内存管理结构,而 Python 虚拟机的堆和栈是 Python 解释器内部的内存管理结构。
Python 虚拟机的堆和栈都在CPython解释器进程中的堆空间中吗
是的,Python 虚拟机的堆和栈都在 CPython 解释器进程中的堆空间中。
Python 虚拟机是一个软件组件,它被实现为 CPython 解释器的一部分,运行在 CPython 进程内存空间中。因此,虚拟机的堆和栈都是在 CPython 进程的内存空间中创建的。
这是因为,虚拟机的主要任务是执行 Python 代码,并管理对象的生存期和内存使用。因此,它需要在 CPython 进程中的内存空间中维护堆和栈,以便管理对象的生存期和内存使用。
在 CPython 解释器中,内存被分为两个区域:堆和栈。堆是一个存储对象的动态内存分配区域,是用于存储长期存在的对象的。栈是一个存储函数调用栈的静态内存分配区域,是用于存储短期存在的对象的。
对于 Python 虚拟机来说,它的堆和栈也在 CPython 解释器进程中的堆空间中,这些内存空间由 Python 虚拟机自己管理。
通俗地说,Python 虚拟机是在 CPython 解释器进程的堆空间中模拟出了自己的堆和栈,以实现 Python 语言的功能。
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器