xxxatrr家族
hasattr()
用于判断对象是否包含对应的属性。
hasattr 语法:hasattr(object, name)
setattr()
setattr() 函数对应函数 getattr(),用于设置属性值,该属性不一定是存在的。如果属性不存在会创建一个新的对象属性,并对属性进行赋值。
setattr() 语法:setattr(object, name, value)
getattr()
getattr() 函数用于返回一个对象属性值。
getattr(object, name[, default])
- object – 对象。
- name – 字符串,对象属性。
- default – 默认返回值,如果不提供该参数,在没有对应属性时,将触发 AttributeError。
__get__,__getattr__,__getattribute__
及区别
1.object.__getattribute__(self, name)
无条件被调用,通过实例访问属性、函数(点和getattr函数都会触发)。如果class中定义了__getattribute__()
和__getattr__()
,则只有在显式调用或引发AttributeError异常才会调用__getattr__()
注:
- 只要定义了
__getattribute__
方法,不管你访问一个存在的还是不存在的属性,都由这个方法返回,比如访问t.a
,虽然a存在,但是只要定义了这个访问,那么就不是访问最开始的a了 - 如果
__getattribute__
抛出了AttributeError
异常,并且定了了getattr
函数,那么会调用getattr
这个函数并返回getattr函数的返回值 - 属性访问的一个大致优先级是:
__getattribute__
>__getattr__
>__dict__
2.object.__getattr__(self, name)
为内置方法,当使用点号获取实例属性时,如果属性不存在(找不到attribute)的时候,会调用__getattr__
,返回一个值或AttributeError异常。
1 | class p(): |
注:如果属性不存在,则不管是否有__getattribute__
,都会调用__getattr__
3.object.__get__(self, instance, owner)
Python 内置的
property
函数可以说是最著名的描述器之一,几乎所有讲述描述器的文章都会拿它做例子。而我们可以通过实现__set__
、__get__
、__delete__
来实现自己的描述器。
- 描述器只对新式类起作用;
方法的第一个参数instance是实际拥有者的descriptor实例,如果是Descriptor类(定义了描述器协议的描述器类)则为
None
,第二个参数owner是实际所属的类本身(所有者类)。
一个类只要实现了__get__、 __set__,__delete__
中任意一个方法,我们就可以叫它描述器(descriptor),他是一个可以描述一个属性操作的对象。如果只定义了__get__
我们叫非资料描述器(non-data descriptor),如果__set__、 __delete__
:任意一个或者同时出现,叫资料描述器(data descriptor)。
首先明确一点,拥有__get__
的类,应该**(也可以说是必须)产生一个实例**,并且这个实例是另外一个类的类属性(注意一定是类属性,通过self的方式产生就不属于__get__
范畴了)。也就是说拥有这个方法的类,那么它的实例应该属于另外一个类/对象的一个属性。
owner是所有者的类,instance是访问descriptor的实例,如果不是通过实例访问,而是通过类访问的话,instance则为None。(descriptor的实例自己访问自己是不会触发__get__
,而会触发__call__
,只有descriptor作为其它类的属性才有意义。)(所以下文的d是作为C2的一个属性被调用)
将描述器作为一个独立对象,并不能展现出描述器的魔力,只有在描述器作为另一个对象的属性的时候,描述器的魔力才能真正展现出来。
1 | class TestDes: |
▲描述器往往以装饰器的方式被使用,导致二者常被混淆。描述器类和不带参数的装饰器类一样,都传入函数对象作为参数,并返回一个类实例,所不同的是,装饰器类返回 callable 的实例,描述器则返回描述器实例。
Q:如果实例中有和描述器重名的属性 x
怎么办?
A:资料和非资料描述器的区别在于,相对于实例字典的优先级不同。当描述器和实例字典中的某个属性重名,按访问优先级,资料描述器 > 同名实例字典中的属性 > 非资料描述器 or (数据描述符 > 实例变量 > 非数据描述符),优先级小的会被大的覆盖。==>类的方法实际就是一个仅实现了 __get__()
的非资料描述器,所以如果实例 c
中同时定义了名为 foo
的方法和属性,那么 c.foo
访问的是属性而非方法。
描述器more:Python 描述器解析
针对描述器的说明: 描述器是被__getattribute__
调用的,如果重写了这个方法,将会阻止自动调用描述器,资料描述器总是覆盖了实例的__dict__
, 非资料描述器可能覆盖实例的__dict__
。
小结:访问存在的属性,如果是描述器,描述器生效
1 | class TestDes: |
- 非资料描述器,也就是只有
__get__
,不管是类还是实例去访问,默认都获得的是__get__
的返回值,但是,如果中间有任何一次重新赋值(t.des = 1)
,那么,这个实例获得的是新的值(对象),已经和原来的描述器完全脱离了关系(描述器__get__
函数失效) - 资料描述器,比如有
__set__
方法,后期通过实例对描述器进行赋值,那么访问的是__set__
,并且永远关联起来==>针对上述问题的修复。但是如果通过修改类属性的方式复制(TestMain.des = 1),那么也会被重新获取新的值(对象),即__set__
函数失效。
总结:
- 可以看出,每次通过实例访问属性,都会经过
__getattribute__
函数。而当属性不存在时,仍然需要访问__getattribute__
,不过接着要访问__getattr__
,就好像是一个异常处理函数。 - 每次访问descriptor(即实现了
__get__
的类),都会先经过__get__
函数。 - 需要注意的是,当使用类访问不存在的变量是,不会经过
__getattr__
函数。而descriptor不存在此问题,只是把instance标识为none而已。
至于三者区别,首先关注:a.x时发生了什么?=>属性的lookup顺序如下:
- 如果重载了
__getattribute__
,则调用. a.__dict__
, 实例中是不允许有descriptor的,所以不会遇到descriptorA.__dict__
, 也即a.__class__.__dict__
.如果遇到了descriptor,优先调用descriptor.- 沿着继承链搜索父类.搜索
a.__class__.__bases__
中的所有__dict__
. 如果有多重继承且是菱形继承的情况,按MRO(Method Resolution Order)顺序搜索.
参考:
__getitem__
当实例对象做p[key] 运算时,会调用类中的方法
__getitem__
,得到__getitem__
的返回值
- 点运算符 用于访问任何对象的属性;
- 方括号运算符 表示法用于访问集合的成员;
区别:点运算符,用来获取语言内置的属性、方法等等;而方括号运算符则是用来获取用户定义的数据,一般是字典、列表、元组以及字符串的成员。
__dict__
Python中大多以对象的形式存在,而对象的属性则是存在
__dict__
属性中
- 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在**类
__dict__
**里的 - 对象的
__dict__
中存储了一些self.xxx的一些东西
注意
- 内置的数据类型没有
__dict__
属性,如list、dict、int - 每个类有自己的
__dict__
属性,就算存着继承关系,父类的__dict__
并不会影响子类的__dict__
- 对象也有自己的
__dict__
属性, 存储self.xxx 信息,父子类对象公用__dict__
- 对象的
__dict__
优先于类的__dict__
调用,如self.__dict__['a']=1
,而Class.__dict__['a']="func"
,则c.a输出的是1,而不是func
可变参数与unpack
-
函数的可变参数
当函数的参数前面有一个星号
*
的时候表示这是一个可变的位置参数,两个星号**
表示是可变的关键字参数。1
2
3
4
5
6def saySomething(word, *args, **kwargs):
pass
# word会被赋值成hello word,
# mrli和mrdu被打包进args数组中
# size=15; color=white会被作为kv传到kwargs的字典中
saySomething("hello word", "mrli", "mrdu", size=15, color="white") -
unpack参数
星号
*
把序列/集合解包(unpack)成位置参数,两个星号**
把字典解包成关键字参数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23txtOptions = {
"fontSize": 15,
"color": "white"
}
addtional_text = ("mrli", "mrdu")
# unpack的形式传参数
saySomething("hello word", *addtional_text, **txtOptions)
# 两句话是等价的
saySomething("hello word", "mrli", "mrdu", size=15, color="white")
# 拓展-我们经常在开源库的doc中看到某个函数能接收xxx参数,但是点进去一看,最底层的参数传的是**kwargs。按道理我们是什么参数都可以传的,大不了函数不解析这些无用参数罢了,但是实际上当我们传API中没有写到的参数时会报错。 ==> 这个是由于kwargs能传递的参数受限于最底层的参数需求,以requests.get为例
# requests.py
def get(url, params=None, **kwargs):
return request('get', url, params=params, **kwargs)
def request(method, url, **kwargs):
return session.request(method=method, url=url, **kwargs)
# get中能传的**关键字参数**实际上只能是这些关键字参数
def request(self, method, url,
params=None, data=None, headers=None, cookies=None, files=None,
auth=None, timeout=None, allow_redirects=True, proxies=None,
hooks=None, stream=None, verify=None, cert=None, json=None):
# Python也是通过这两个特性从而实现了**重写overrided**- 至于星号(
*
)还有一个用途是来明确写明位置参数的截断位置,*
后的参数也不能再以位置参数的形式给出,必须以关键字参数的形式给出——见Python3 函数参数列表单独一个星号 * 的作用
- 至于星号(
类比Go中的...
语法
装饰器
装饰器(Decorators)是 Python 的一个重要部分,通过他们可以在不用更改原函数的代码前提下,修改、拓展其他函数的功能的函数,他们有助于让我们的代码更简洁。==>切面编程的方式
闭包
Python查找变量会一层层地向外层查找,直到global全局也没有时raise
NameError: name 'xxx' is not defined
在装饰器中运用到了闭包的思想。闭包,一句话说就是,在函数中再嵌套一个函数,并且引用外部函数的变量,这就是一个闭包了。(从而使得内部函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起)
执行闭包后,闭包实例将会维持了一个词法环境,其中包含对局部变量的引用,使得原本就应该失效的局部变量仍然存活。
闭包的应用
1 | function makeAdder(x) { |
从本质上讲,makeAdder
是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。
add5
和 add10
都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5
的环境中,x
为 5。而在 add10
中,x
则为 10。
more: 5句口诀理解记忆Python闭包和装饰器
[装饰器类型](9.5 可自定义属性的装饰器 — python3-cookbook 3.0.0 文档)
闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。外部函数相当于给内部函数提供了一个额外数据的执行环境,使得内部函数(被装饰函数)拥有了获得外层函数的数据or执行了外层函数。根据参数[或是叫环境不同],可以分成如下几种装饰器:
-
无参数
-
常见的有输出日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from functools import wraps
def do_log(func):
def wrapper(*args, **kwargs):
print("do before")
func(*args, **kwargs)
print("do after")
return wrapper
def say(word):
print(word)
say("hello world")
# 等价于下面两局
# say = do_log(say)
# say("hello world") ===> do_log(say)("hello world")这个可以类比JS
1
2
3
4
5
6
7
8
9function say(word){
console.log(word);
}
play = function(w){
console.log("我要开始笑啦");
say(w);
console.log("我不笑了");
}
// play相当于装饰了一层say, 同比上面的do_log也是。在之后的执行中play和do_log才是真正的执行函数, 只不过Python语法糖@,让do_log又赋值给了say, 所以我们又可以直接使用say,从而增加了函数原本的功能 -
@语法糖:它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作
-
-
默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30def createLogger():
logger = logging.getLogger("decorate")
formatter = logging.Formatter('%(asctime)s-%(name)s-%(levelname)s-%(message)s')
sh = logging.StreamHandler()
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
return logger
logger = createLogger()
logger.warning("hello")
def log_by_level(level):
# 创建新环境:多提供了一个level变量
def do_log(func):
def wrapper(*args, **kwargs):
getattr(logger, level)("do before")
func(*args, **kwargs)
getattr(logger, level)("do after")
return wrapper
return do_log
def say(word):
print(word)
# 等价于
do_log = log_by_level("warning") # 作用是提供了一个level=warning的环境
say = do_log(say)
say("hello") -
可自定义属性的装饰器:
允许用户提供参数在运行时控制装饰器行为。比如前端设置一个单选,可以控制日志输出级别,此时则需要在运行时修改装饰器行为
-
可选参数
既可以不传参数给它,比如
@decorator
, 也可以传递可选参数给它,比如@decorator(x,y,z)
。 -
类装饰器:
使用一个装饰器去包装函数,返回的是一个可调用的实例
类装饰器主要依靠类的
__call__
方法1
2
3
4
5
6
7
8
9
10
11
12class ClassDecorator:
def __init__(func):
self.func = func
def __call__(self):
print("enter")
self.func()
print("end")
def bar():
print ('bar')
# bar = ClassDecorator(bar)
# bar() ==> ClassDecorator(bar)() 调用实例==>即触发__call__方法 -
类中装饰器:
在类中定义装饰器,并将其作用在其他函数或方法上
跟无参数的装饰器一样写法,只不过定义在类中;并且在使用的时候是以
@Decorator.decortae
的形式 -
类方法、静态方法装饰器:跟无参数的装饰器一样写法,但是需要在@staticmethod、@classmethod之前标注
▲注意:
-
装饰器只会在函数定义时被调用一次
-
可以看到的是,在装饰器代码中总有一行
@wraps(func)
的代码,其目的:是你写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了---->任何时候你定义装饰器的时候,都应该使用functools库中的@wraps装饰器来注解底层包装函数 -
装饰器的执行是依次的,离函数签名越近,则越先被定义
1
2
3
4
5
6
def f ():
pass
# 等价于 f = a(b(c(f)))
总结:
理解装饰器应该从①@
语法糖到底做了什么;②闭包是什么,有什么作用;③装饰器如何等价表示;来理解
懒加载属性
描述符+类装饰器实现
1 |
|
重点:
- 跟[
print(t.des)
](#3.object.__get__(self, instance, owner)
)会触发t.des指向的descriptor实例的__get__
一样,通过类__dict__["serverHost"]
,其也是个描述器实例,因此也会触发LazyProperty object的__get__
- 实例
__dict__
会优先于类的__dict__
使用,如果实例__dict__
找不到,会往上类__dict__
找
修饰符(方法装饰器)
1 | def lazy_property(func): |
由单例元类引发的知识点
看开源代码时,看到了下面一段代码,于是对withMetaclass产生了好奇,经过了解发现其作用是six对python2和python3使用元类兼容的写法。
1 | # Python2和3兼容使用元类写法 |
因此,上述代码在Python3中相当于
1 | # Python3元类使用写法 |
那么,问题来了,withMetaclass到底是怎么实现兼容的呢?下面是其实现代码
1 | def withMetaclass(meta, *bases): |
可以看到其中出现了不少我们很少看到的使用方法。接下来我们就仔细的学习上述写法为什么可以成功。
元类使用可以参考:Python3 元类(metaclass)
预置知识:type和object
object 和 type的关系很像鸡和蛋的关系,先有object还是先有type没法说,obejct和type是共生的关系,必须同时出现的。
记住一点:在Python里面,所有的东西都是对象的概念,即包括类(类是type的实例对象)
最重要的两点
- object类是所有类的超类(也是type类的父类)
- type是所有类的类(类型,所有类都是type的实例对象,object类型也是type的实例对象;type 创建的对象拥有创建对象的能力(也就是类))–>是所有类的元类
此外:
- type是所有元类的父亲。我们可以通过继承type来创建元类(通过重写
type.__new__
和type.__call__
来拦截自定义类的创建过程)。 - object是所有类的父亲。
- 实例是对象关系链的末端,不能再被子类化和实例化。
了解到这些关键的点后,我们继续看代码中出现的一些内容:
__new__
__new__()
是一种负责创建类实例的静态方法,它无需使用 staticmethod 装饰器修饰,且该方法会优先__init__()
初始化方法被调用。
__new__()
通常会返回该类的一个实例,但有时也可能会返回其他类的实例,其super().__new__(cls)
中会调用object.__init__
来Create and return a new object.
,因此我们可以通过改写子类的__new__
可以添加一些逻辑来控制实例的产生,然后再通过super().__new__(cls)
来生成一个instance并返回。
1 | class demoClass: |
Q:什么情况下重写类的__new__()
呢?答案很简单,在__init__()
不够用的时候。
__new__()
通常会返回该类的一个实例,但有时也可能会返回其他类的实例,如果发生了这种情况,则会跳过对 __init__()
方法的调用。而在某些情况下(比如需要修改不可变类实例(Python 的某些内置类型)的创建行为),利用这一点会事半功倍。比如:http://c.biancheng.net/view/5484.html,对 Python 不可变的内置类型(如 int、str、float 等)进行了子类化,这是因为一旦创建了这样不可变的对象实例,就无法在__init__()
方法中对其进行修改。
注:由于 __new__()
不限于返回同一个类的实例,所以很容易被滥用,不负责任地使用这种方法可能会对代码有害,所以要谨慎使用。
MetaClass元类
承接上文
__new__
,Python中大量使用__new__()
方法且合理的地方,就是 MetaClass 元类。MetaClass元类,并不是某一个类的名字,它是一个概念,是一种Python的思想。当然其本质也是一个类,但和普通类的用法不同,它可以对类内部的定义(包括类属性和类方法)进行动态的修改。可以这么说,使用元类的主要目的就是为了实现在创建类时,能够动态地改变类中定义的属性或者方法。其可以将创建对象的过程拦截下来,从而对这个对象进行自定义(这个需要类继承type,与前文继承object的做区别)。
明确一点:元类可以理解成是自定义类继承的父类(从兼容写法中也能看出),但元类的特点是不会出现在自定义类的继承关系(
__mro__
)之中
举个例子,根据实际场景的需要,我们要为多个类添加一个 name 属性和一个 say() 方法。显然有多种方法可以实现,但其中一种方法就是使用 MetaClass 元类。
1 | # 定义一个元类,继承type。因为只有继承type才能通过重写__new__来拦截创建过程 |
可以看到,在创建类时,通过在标注父类的同时指定元类(格式为metaclass=元类名
),则当 Python 解释器在创建该类实例时,FirstMetaClass(type)
元类中的__new__
方法就会被调用,其中bases和attrs能拿到自定义类的参数,从而实现动态修改类属性或者类方法的目的。
元类和父类的区别:
在定义子类的时候,我们有两个选择:①是传需要继承的父类;②自定义的元类。
- 父类是子类的模板,子类的功能是跟父类紧耦合的,子类和父类一般是一一对应的
- 元类是子类的修饰器,可以为该子类和其他子类都添加自定义功能,并且不在继承关系中(
Class.__mro__
查看),子类和元类是一对多的关系。元类并不是特地为某个子类服务的
1 | class TestMeta3(type): |
在定义的时候,发现竟然有输出。因为定义的时候,python解释器会在当前类中查找metaclass[3],如果找到了,就使用该metaclass创建Eg3类。所以打印出来的name、bases、attrs都和Eg3有关。
with_metaclass
由于python2和python3中元类使用方法的不同,我们需要使用一种兼容的方式[1],如下所示:
1 | def withMetaclass(meta, *bases): |
with_metaclass
返回的临时类中,本身无任何属性,但包含了元类和基类的所有信息,并在下一步定义类时将所有信息解包出来[1]。
type
动态创建类
- type() 函数属于 Python 内置函数,通常用来查看某个变量的具体类型。
type(obj)
- 其实,type() 函数还有一个更高级的用法,即创建一个自定义类型(也就是创建一个类)。
type(name, bases, dict)
:其中 name 表示类的名称;bases 表示一个元组,其中存储的是该类的父类;dict 表示一个字典,用于表示类内定义的属性或者方法。
实际上type(name, bases, dict)
是调用了type类的type.__init__(cls, what, bases=None, dict=None)
方法,创建了一个type的实例(类类型就是一个type实例),类型是<class 'type'>
<class ‘type’>是所有类型的类型。<class ‘object’>也是所有对象的超类(除了它自己,包括type)
▲. 此外type还有type.__new__(*args, **kwargs)
,其作用是Create and return a new object.
,可以写成type.__new__(ClassTpye, name, base, dicts)
,但ClassType必须是type的子类。会返回一个跟ClassType有关系的新类型
通过元类创建单例类
现在让我们正式看,我在开源代码里看到的内容:
1 | # 注意这边继承了type, 所以下面的__call__是重写type的__call__,即创建实例的方法 |
注:类也是对象,是元类的对象,即我们实例化一个类时,调用其元类的__call__(cls, *args, **kwargs)
方法进行创建对象。
__call__
一个非常特殊的实例方法,即
__call__()
。该方法的功能是在类中重载了对象的 () 运算符,使得类实例对象可以像调用普通函数那样,以“对象名()”的形式使用。
实际上,如果不重写__call__
的话,Class.__call__(*args, **kwargs)
还承担着产生类实例的功能(会调用父类(可以通过Class.__class__
来查看父类)的type.__call__
其会返回一个实例)
案例一:
1 | # 默认继承的是object, 而不是type |
Q:我们在实例化一个对象的时候f = Foo(1, y=2)
,可以发现在__init__()
中并没有返回实例,但调用Foo(1, y=2)
确实返回了一个对象,而且,__init__
预期一个self
参数,但是当我们调用Foo(1, y=2)
时这里并没有这个参数。那么类实例化的过程到底是怎么样的呢?
A:构造顺序——理解python的类实例化
首先明确一点,Python中的类也是对象!类、函数、方法以及实例都是对象——类类型是type的对象,并且无论何时你将一对括号放在它们的名字后面时,就会调用type.__call__()
方法。为什么呢?因为type是类型的父类
1 | Foo.__class__ |
所以Foo
是类型type
的一个对象,并且调用type类的__call__(self, *args, **kwargs)
返回一个Foo
类的对象。让我们看下type
中的__call__
方法是什么样的。这个方法相当的复杂,但是我们将其C代码转成Python代码,并尝试尽量简化它,结果如下。
1 | class type(object): |
可见__new__
方法为对象分配了内存空间,构建它为一个“空"对象然后__init__
方法被调用来初始化它。
那我们定义了一个具体类来讲解这个过程。首先明确一点:Foo相对于产生了一个type实例化对象
1 | class Foo(object): |
获得实例化对象**Foo(*args, **kwargs)
也可以看作是type对象()
即调用了type中()运算符的触发的函数type.__call__
从而创建一个Foo的实例**
- 至于
type.__call__
发生了什么就是上面抽象代码中介绍的那般,调用type.__new__(Foo, *args, **kwargs)
然后返回一个对象实例obj。 obj
随后通过调用obj.__init__(*args, **kwargs)
被初始化。obj
被type.__call__
中返回。
▲注意:Foo.__call__
重载的是foo对象
的()运算符,而Foo()
实例化foo对象,则执行的是type对象
的()运算符。
小总结:
- 现在我们能知道为什么元类必须继承type了:因为我们实例化对象
Foo(xxx)
时调用了type.__call__
,而type.__call__
又会调用type.__new__
因此如果type子类重写实现了__new__
(返回的类实例对象的类型作控制)、__call__
(对实例化的流程做控制),则可以对类对象的类型和类属性起到自定义的功能,而重写就必须继承type=>需要元类必须继承type - 所以按照上述的逻辑,如果定义了一个元类让自定义类用的话
class Foo(metaclass=MyMetaClass)
,在其实例化过程中Foo()
会直接调用重写后的MyMetaClass.__call__
,而只要记住在MyMetaClass.__call__
中使用到return super(Singleton, cls).__call__(*args, **kwargs)
就可以把type.__call__
生成的实例返回啦。所以这也是为什么编写元类,一般都是继承了type,然后根据想控制实例化流程就重写__call__
方法,想添加属性就重写__new__
方法就行了。 - ★元类产生影响的时间点是在实例化的时候
注意点:元类继承了type,所以实例化元类是在产生一个类类型,就要以type创建类类型的参数去产生。而元类的使用一般都是自定义类class MyClass(metaclass=元类)
,然后实例化自定义类MyClass(xxx)
总结:看完上述知识点后,我们能知道为什么withclass能起到metaclass的作用(类的__mro__
中不出现指定的元类)了:
- 首先分析流程:
return type.__new__(Metaclass)
返回了一个类型供自定义类继承,由于MetaClass继承的是真正的元类(元类都继承type),所以在自定义类实例化的时候会被Metaclass的__new__
方法拦截,在MetaClass.__new__
里return了一个自定义实例,并把对象加入到了Singleton字典中了。 - 其次讲解为什么MetaClass中没有MetaClass:因为根据
__new__
知识点中讲到的,__new__
控制了实例产生,return type.__new__(Metaclass)
中创建了Metaclass
,但其在__new__
中返回的并不是MetaClass,因此__mro__
中不会出现Metaclass
- 最后还要讲讲Singleton中的执行逻辑:
1 | class Singleton(type): |
函数和模块的特殊属性__annotations__
众所周知,Python是一种动态类型语言,也是强类型语言。在Python语言中,使用变量之前不需要声明其类型,直接赋值即可创建变量,变量初始类型取决于等号右侧表达式的值的类型。创建之后,变量的类型可以随时发生变化,但在任何时刻,每个变量都有确定的类型。
同理,在定义函数和类的方法时,也不需要声明形参类型,完全取决于实参类型.
因此很多从其他语言转过来的朋友很不习惯这样的方式,还是习惯于声明变量和参数的类型。虽然Python不支持声明,但是允许在定义函数时使用“注解”的形式来标注形参和返回值的类型,但这种注解的形式在运行时并不会对形参进行任何约束和检查,在实际调用函数时,即使实参不符合形参的类型标注,一样能够正常传递(IDE会进行检查,在编译之前提醒传参不一致,但也不会报错)
1 | # 标注了形参s的类型为str或者dict; 也标注了返回值为str类型 |
在Python中,函数会维护一个特殊属性__annotations__
,这是一个字典,其中的“键”是被注解的形参名,“值”为注解的内容。使用时并不要求注解的内容是Python中的类型,可以是任意内容。例如,
从官方文档来看,函数的__annotations__属性只包含形参和返回值的注解,即使在函数体中有类似的注解,但这并不等价于C语言中的变量声明,①这样的注解不会创建变量,②也不会被收集到这个特殊属性__annotations__
中。例如
1 | def demo(param1: int, param2: int)->int: |
另外,在模块中也有个特殊属性__annotations__
用于收集模块中变量的注解,但这些注解同样也不会创建对应的变量。例如,在下面的代码中,并没有创建变量e、f、g。
用途:修改dataclass构造
1 | from dataclasses import dataclass, field |
Dataclass(数据类)
python3.7的新特性dataclass,其之前类似的实现是
@attr.s
,主要用来描述数据对象,其含义为“一个带有默认值的可变的namedtuple”,广义的定义就是有一个类,它的属性均可公开访问,可以带有默认值并能被修改,而且类中含有与这些属性相关的类方法,那么这个类就可以称为dataclass,再通俗点讲,dataclass就是一个含有数据及操作数据方法的容器。乍一看可能会觉得这个概念不就是普通的class么,然而还是有几处不同:
- 相比普通class,dataclass通常不包含私有属性,数据可以直接访问
- dataclass的repr方法通常有固定格式,会打印出类型名以及属性名和它的值
- dataclass拥有
__eq__
和__hash__
魔法方法- dataclass有着模式单一固定的构造方式,或是需要重载运算符,而普通class通常无需这些工作
因为数据对象类有些固定的行为,也正适合程序为我们自动生成,因此
dataclasses
模块诞生了。
dataclasses
模块中提供了一些常用函数供我们处理数据类。
- 使用
dataclasses.asdict
和dataclasses.astuple
我们可以把数据类实例中的数据转换成字典或者元组 - 使用
dataclasses.is_dataclass
可以判断一个类or实例对象是否是数据类
数据类的基石——dataclasses.field
函数原型: dataclasses.field(*, default=MISSING, default_factory=MISSING, repr=True, hash=None, init=True, compare=True, metadata=None)
, 通常我们无需直接使用,装饰器会根据我们给出的类型注解自动生成field,但有时候我们也需要定制这一过程,这时dataclasses.field
就显得格外有用了。
1 |
|
- default和default_factory参数将会影响默认值的产生,它们的默认值都是None,意思是调用时如果为指定则产生一个为None的值。
- 其中default是field的默认值,比如
age: int = field(default=2)
,那么c=C()
得到的c实例中的age就为2;(等价于age: int = 2
) - default_factory控制如何产生值,它接收一个无参数或者全是默认参数的
callable
对象,然后用调用这个对象获得field的初始值,之后再将default(如果值不是MISSING)复制给callable
返回的这个对象。
- 其中default是field的默认值,比如
▲举个例子,对于list,当复制它时只是复制了一份引用,所以像dataclass里那样直接复制给实例的做法的危险而错误的,为了保证使用list时的安全性,应该这样做:
1 |
|
- **
field.init
**参数如果设置为False,表示不为这个field生成初始化操作,dataclass提供了hook——__post_init__
供我们利用这一特性:__post_init__
在__init__
后被调用,我们可以在这里初始化那些需要前置条件的field。 dataclasses.InitVar
: 如果指定一个field的类型注解为dataclasses.InitVar
,那么这个field将只会在初始化过程中(__init__
和__post_init__
)可以被使用,当初始化完成后访问该field会返回一个dataclasses.Field
对象而不是field原本的值,也就是该field不再是一个可访问的数据对象。
dataclasses嵌套
1 |
|
**
解包得到的额外信息
1 | # 额外信息 |
生成器、迭代器、容器
在使用Python的过程中,经常会和列表/元组/字典(list/tuple/dict)、容器(container)、可迭代对象(iterable)、迭代器(iterator)、生成器(generator)等这些名词打交道,众多的概念掺杂到一起难免会让人一头雾水,这里我们用一张图来展现它们之间的关系——from: Python迭代器和生成器详解
Python中的容器
容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个迭代获取,可以用 in,not in 关键字判断元素是否包含在容器中。
我们常用的 string、set、list、tuple、dict 都属于容器对象。
尽管大多数容器都提供了某种方式获取其中的每一个元素,但这并不是容器本身提供的能力,而是可迭代对象赋予了容器这种能力,当然并不是所有容器都是可迭代的。
可迭代对象与Iterator(迭代器)
可迭代对象
可以返回一个迭代器的对象都可以称之为可迭代对象。
1 | 1, 2, 3] x = [ |
a 和 b 是两个独立的迭代器,迭代器内部有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代时获取正确的元素。迭代器有一种具体的迭代器类型,比如 list_iterator
,set_iterator
。可迭代对象实现了 __iter__()
方法,该方法返回一个迭代器对象。
在循环遍历自定义容器对象时,会使用python内置函数iter()调用遍历对象的**__iter__()
获得一个迭代器**,之后再循环对这个迭代器使用next()
调用迭代器对象的__next__()
。__iter__()
只会被调用一次,而__next__()
会被调用 n 次。
Iterator(迭代器)
迭代器是一个带状态的对象,它能在你调用next()
方法时返回容器中的下一个值,任何实现了__iter__()
和__next__()
方法的对象都是迭代器,__iter__()
返回迭代器自身,__next__()
返回容器中的下一个值,如果容器中没有更多元素了,则抛出StopIteration
异常。
▲迭代器与列表的区别在于,构建迭代器的时候,不像列表把所有元素一次性加载到内存,而是以一种延迟计算(lazy evaluation)方式返回元素,这正是它的优点:节省空间。因为它并没有把所有元素装载到内存中,而是等到调用next()
方法的时候才返回该元素(按需调用 call by need 的方式,本质上 for 循环就是不断地调用迭代器的next()
方法)。
Generator(生成器)
普通函数用return
返回一个值,还有一种函数用yield
返回值(yield是每次“惰性返回”一个值),这种函数叫生成器函数。
函数被调用时会返回一个生成器对象。生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅,它不需要像普通迭代器一样实现__iter__()
和__next__()
方法了,只需要一个yield
关键字。生成器一定是迭代器(反之不成立),因此任何生成器也是一种懒加载的模式生成值。
yield
就是return
返回一个值,并且记住这个返回的位置,下次迭代就从这个位置后(下一行)开始。next
方法和send
方法都可以返回下一个元素,区别在于send
可以传递参数给yield
表达式,这时传递的参数会作为yield表达式的值,而yield的参数是返回给调用者的值。
send()
方法:
- send方法有一个参数value ,该参数value指定的是上一次被挂起的yield语句的返回值。send(value) 会把 value作为
yield express
的返回值赋值给接收者(即xxx =
yield yyy`将value赋值给等号左边xxx)。 - 在使用send()方法前,程序必须被挂起,不然会报错——所以生成器未启动之前send()中不能传非None的值
注:
- 在用生成器时,第一次要用
next
启动、或者是send(None)
, 注意,虽然 send(None) 的功能是 next() 完全相同,但更推荐使用 next(),不推荐使用 send(None)。 - send和next的执行很像,只是send可以和生成器互动,传入一个值。两者的执行也都是从上一个
yield exp
等号左边的位置再往后执行到下一个yield
的位置
throw(
)方法
throw() 方法的功能是,在生成器函数执行暂停处,抛出一个指定的异常,之后程序会继续执行生成器函数中后续的代码,直到遇到下一个 yield 语句。需要注意的是,如果到剩余代码执行完毕没有遇到下一个 yield 语句,则程序会抛出 StopIteration 异常。
1 | def foo(): |
显然,一开始生成器函数在 yield 1 处暂停执行,当执行 throw() 方法时,它会先抛出 ValueError 异常,然后继续执行后续代码直至找到下一个 yield 语句,在执行过程中ValueError被try捕捉,而之后由于程序后续不再有 yield 语句,因此执行到最后会抛出一个 StopIteration 异常。
案例见:http://c.biancheng.net/view/7090.html
分析一个最简单的生成器:
1 | def foo(): |
分析一下此程序的执行流程:
- 首先,构建生成器函数,并利用器创建生成器(对象)f 。
- 使用生成器 f 调用无参的 send() 函数,其功能和 next() 函数完全相同,因此开始执行生成器函数,即执行到第一个 yield “hello” 语句,该语句会返回 “hello” 字符串,然后程序停止到此处(注意,此时还未执行对 bar_a 的赋值操作)。
- 下面开始使用生成器 f 调用有参的 send() 函数,首先它会将暂停的程序开启,同时还会将其参数“C语言中文网”赋值给当前 yield 语句的接收者,也就是 bar_a 变量。程序一直执行完 yield bar_a 再次暂停,因此会输出“C语言中文网”。
- 最后依旧是调用有参的 send() 函数,同样它会启动餐厅的程序,同时将参数“http://c.biancheng.net”传给 bar_b,然后执行完 yield bar_b 后(输出 http://c.biancheng.net),程序执行再次暂停。
yield和yield from
在进入协程之前,我们需要明白
yield
和yield from
,其是官方实现协程的关键.
yield from
是在Python3.3才出现的语法
yield
是每次“惰性返回”一个值yield from
是“从什么(生成器)里面返回,实际上就是委托给了另一个生成器:- yield from 后面可以跟的可以是“ 生成器 、元组、 列表、range()函数产生的序列等可迭代对象”
- 从本质上讲,委托给了另一个生成器的意思是:
yield from iterable
本质上等于for item in iterable: yield item
当然除了最显而易见的使用区别以外,yield from还对yield的不足进行了改进。
-
针对yield无法获取生成器return的返回值:在使用yield生成器的时候,如果使用for语句去迭代生成器,则不会显式的出发StopIteration异常,而是自动捕获StopIteration异常,所以如果遇到return,只是会终止迭代,而不会触发异常,故而也就没办法获取return的值。
for迭代语句不会显式触发异常,故而无法获取到return的值,迭代到2的时候遇到return语句,隐式的触发了StopIteration异常,就终止迭代了,但是在程序中不会显示出来。
yield from具有以下几个特点:
(1)上面的my_generator是原始的生成器,main是调用方,使用yield的时候,只涉及到这两个函数,即“调用方”与“生成器(协程函数)”是直接进行交互的,不涉及其他方法,即“调用方——>生成器函数(协程函数)”;
(2)在使用yield from的时候,多了一个对原始my_generator的包装函数,然后调用方是通过这个包装函数(后面会讲到它专有的名词)来与生成器进行交互的,即“调用方——>生成器包装函数——>生成器函数(协程函数)”;
(3)yield from iteration结构会在内部自动捕获 iteration生成器的StopIteration 异常。这种处理方式与 for 循环处理 StopIteration 异常的方式一样。而且对 yield from 结构来说,解释器不仅会捕获 StopIteration 异常,还会把return返回的值或者是StopIteration的value 属性的值变成 yield from 表达式的值,即上面的result。
-
yield from所实现的数据传输通道:yield涉及到“调用方与生成器两者”的交互,生成器通过next()的调用将值返回给调用者,而调用者通过send()方法向生成器发送数据;
PEP 380 使用了一些yield from使用的专门术语:
- 委派生成器:包含
yield from <iterable>
表达式的生成器函数;即上面的wrap_my_generator生成器函数.在调用方与子生成器之间建立一个双向通道。 - 子生成器:从
yield from 表达式中 <iterable>
部分被获取的生成器函数;即上面的my_generator生成器函数 - 调用方:调用委派生成器的客户端代码;即上面的main生成器函数
(1)yield from主要设计用来向子生成器委派操作任务,但yield from可以向任意的可迭代对象委派操作;
(2)委派生成器(group)相当于管道,所以可以把任意数量的委派生成器连接在一起—一个委派生成器使用yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个生成器。
- 委派生成器:包含
-
可以帮助处理StopIteration异常: 我们在调用生成器的时候,一般要么for,要么next。在next的时候我们就得自己去关注生成器什么时候会爆出StopIteration异常,而将其yield from委托给委派生成器后,调用者直接对委派生成器操作,其会自动帮我们处理
thorw()
、send()
、close()
任何使用send()方法发给委派生产器(即外部生产器)的值被直接传递给迭代器。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,委派生产器恢复继续执行yield from后面的语句;若迭代器产生其他任何异常,则都传递给委派生产器
-
yield 返回一个、yield from 返回一堆
其实yield from最重要的作用就是提供了一个“数据传输的管道”,打开双向通道,把最外层的调用方法与最内层的子生成器连接起来,这两者就可以进行发送值和返回值了。例子见:https://blog.csdn.net/qq_27825451/article/details/85244237
yield from使用案例: 使用 yeild from 写一个异步爬虫
Python协程
随着Go的普及,越来越多人听到了协程的概念,也就是用户级线程,相比于线程而言,由于不涉及用户态和内核态的切换,因此性能更好,此外也不会出现竞态条件,对锁的要求也少很多。对于受限于GIL的Python而言更是如此,对此,Python也不断在优化并发,比如比较熟为人知的thread 模块多线程和 multiprocessing 多进程,后来慢慢引入基于 yield 关键字,逐渐引入了协程的概念,但之后这样的协程写法也不太被官方推荐,而是在Python3.5后使用特地提供的关键字: async/await
并发指的是 一个 CPU 同时处理多个程序,但是在同一时间点只会处理其中一个。虽然一个时刻只能处理一个任务,但是因为程序切换的速度非常快,1 秒钟内可以完全很多次程序切换,肉眼无法感知,所以对于用户而言就相当于是同时运行的。最典型的就是操作系统上运行N个应用程序,其实也是多进程并行的,但给人的感觉却是同时的。
所以,我们也能看出并发的核心在于,如何让程序更快的进行切换。
在此顺便普及并发、并行、同步和异步概念区别:
- 并发指的是 一个 CPU 同时处理多个程序,但是在同一时间点只会处理其中一个。
- 并行指的是多个 CPU 同时处理多个程序,同一时间点可以处理多个。
- 同步:执行 IO 操作时,必须等待任务执行完成得到返回结果后,再执行后一个任务。
- 异步:执行 IO 操作时,不必等待执行完成的返回结果,就执行下一个任务。
协程,线程和进程的区别
- 多进程通常利用的是多核 CPU 的优势,同时执行多个计算任务。每个进程有自己独立的内存管理,所以不同进程之间要进行数据通信比较麻烦。
- 多线程是在一个 cpu 上创建多个子任务,当某一个子任务休息的时候其他任务接着执行。多线程的控制是由 python 自己控制的。 子线程之间的内存是共享的,并不需要额外的数据通信机制。但是线程存在数据同步问题,所以要有锁机制。
- 协程的实现是在一个线程内实现的,相当于流水线作业。由于线程切换的消耗比较大,所以对于并发编程,可以优先使用协程。
写一个简单的协程:
1 | import asyncio |
由于上面只有一个任务,不涉及任务切换,因此写一个可以比较的实验代码
1 | import asyncio |
明确的点:
-
增加并发,实际上就是减少一些无效的等待,由于一些IO任务会比较耗时,如果一直让CPU等待则大大减低了执行效率,因此await就是碰到耗时任务时,主动告诉cpu让其去做其他事,所以await后面都是耗时函数
-
由于await的设计:后面都必须跟的是Awaitable对象,或者是实现了其协议的对象,因此跟一般写法有些许出入,比如time.sleep改成了async.sleep,比如requests.get改成了await
aiohttp.ClientSession().get
awaitable对象必须满足如下条件中其中之一
- 原生协程对象
- 通过async def定义的函数是原生的协程对象,
async def download()
,但如果download内部没有调用异步代码,即使用的是requests.get,阻塞后实际上还是串行的,所以要用异步的aiohttp代替requests
- 通过async def定义的函数是原生的协程对象,
types.coroutine()
修饰的基于生成器的协程对象,注意不是Python3.4中asyncio.coroutine- 实现了
__await__
method,并在其中返回了iterator的对象
- 原生协程对象
-
await只能用在async标明的函数中
参考:
- ★. Python Async/Await入门指南——写法,对比,Awaitable具体实现
- python教程:使用 async 和 await 协程进行并发编程——概念
- 怎么掌握asyncio? - 灵剑的回答 - 知乎 https://www.zhihu.com/question/294188439/answer/555273313——★协程框架设计
注:
- 多线程主要用于IO密集型任务、多进程主要用于CPU密集型任务。因此协程的主要应用场景是 IO 密集型任务:①网络请求,比如吧虫,大量使用aiohttp②文件读取,aiofile ③web框架,aiohttp,fastapi④数据库查询,asyncpg、databases
- 协程也是需要上锁的:深入探讨,协程是否是线程安全(协程安全)的?——协程的安全指的是,在你交出控制权之前是安全的,交出以后当然不安全啊。因此针对改变公共资源中间切换协程的情况需要上锁。例子中,self.a的加和减都必须在一起,中间不能await让出(如果run里面不用await交出控制权,或者统一在await之后进行变量的操作,可以实现协程安全)----自己的话理解一下就是:用户在await让出控制权之前操作都是事务安全的,但是如果在await前后都对公共资源进行修改了(比如self.a),那么肯定会出错:因为执行顺序不可控,所以执行快(不可控)的协程可能会在任意时间进行await后的self.a-=1,导致每次的结果不一致
网络请求速度对比
1 | import asyncio |
gevent和asyncio区别
-
asyncio是Python 3.4版本引入的标准库,直接内置了对异步IO的支持,不需要第三方的支持,我们从asyncio模块中直接获取一个EventLoop的引用,然后把需要执行的协程扔到EventLoop中执行,就实现了异步IO。
asycio 需要自己在代码中让出CPU,控制权在自己手上
-
gevent是第三方库,通过greenlet实现协程,其基本思路是:当一个greenlet遇到IO操作时,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。
gevent 用会替换标准库,你以为调用的是标准库的方法实际已经被替换成gevent自己的实现,遇到阻塞调用,gevent会自动让出CPU
使用uvloop加速
uvloop基于libuv,libuv是一个使用C语言实现的高性能异步I/O库,uvloop用来代替asyncio默认事件循环,可以进一步加快异步I/O操作的速度。
uvloop的使用非常简单,只要在asnycio获取事件循环前设置asnycio.set_event_loop_policy(uvloop.EventLoopPolicy())
,就将asyncio的事件循环策略设置为uvloop的事件循环策略。
1 | import asyncio |
注: 2022年4月26日目前不支持在windows上安装uvloop
附: aiohttp利用协程批量下载图片: https://pythondict.com/scrapy/python-file-download/——可以通过asyncio创建事件循环,而不是session.run(这个也会创建事件循环)
API说明与对比
asyncio.run
:asyncio.run()
在 Python3.7才提出,可以省去显式的定义事件循环的步骤asyncio.get_event_loop
和loop.run_util_complete
:创建时间循环并执行协程任务task.add_done_callback(callback)
,callback为一个函数
见:Python3.7的新API:asyncio.run()
wait和gather
asyncio.wait
asyncio.gather
:先把所有的协程任务事先创建好,然后一次性交给asyncio.gather(*tasks)
,gather会将协程任务再都加入到事件循环中
asyncio.gather 和asyncio.wait区别:
共同点:
- 两个方法的参数都是接受多个future或coro组成的列表,其作用都是吧把所有 Task 任务结果收集起来
不同点
-
wait:
在内部wait()使用一个set保存它创建的Task实例。因为set是无序的所以这也就是我们的任务不是顺序执行的原因。
- wait的返回值是一个元组,包括两个集合:
done
和pending
,分别表示已完成的协程和超时未完成的task,想知道done的结果需要通过.result
来获得resultg结果 - wait第二个参数为一个超时值,达到这个超时时间后,未完成的任务状态变为pending,当程序退出时还有任务没有完成此时就会看到错误提示。
1
2
3
4
5
6
7for i in range(5):
task = asyncio.create_task(async_func(i))
task_list.append(task)
# 不需要拆包, 当设置return_when=asyncio.tasks.FIRST_COMPLETED, pending有值
done, pending = await asyncio.wait(task_list, timeout=None)
for done_task in done:
print((f"[{current_time()}] 得到执行结果 {done_task.result()}")) - wait的返回值是一个元组,包括两个集合:
-
gather的作用和wait类似不同的是:
-
gather任务无法取消。
-
返回值是一个结果列表
-
按照传入协程的顺序,会顺序保存的对应协程的执行结果输出。
1
2
3
4
5
6
7
8for i in range(5):
task = asyncio.create_task(func(i))
task_list.append(task)
# 这边需要拆包
results = await asyncio.gather(*task_list)
# ▲可以看到跟上述不同的是,这里不需要通过`.result()`来获得结果值
for result in results:
print((f"[{current_time()}] 得到执行结果 {result}"))★在大部分情况下,用asyncio.gather是足够的,如果你有特殊需求,可以选择asyncio.wait,举2个例子:
- 需要拿到封装好的Task,以便取消或者添加成功回调等
- 业务上需要
FIRST_COMPLETED / FIRST_EXCEPTION
即返回的
-
详细实现:从头造轮子:python3 asyncio之 gather (3)、ensure_future(确保参数为future对象)和create_task(创建Task对象)
注:使用搭配:loop.run_until_complete(asyncio.wait([userth(i) for i in range(times)]))
或者asyncio.run(main()、task1 = asyncio.create_task(func())
ensure_future和create_task
-
asyncio.ensure_future
:future = asyncio.Future()、future.set_result('data')、await future
证明future对象都是awaitable,这也是协程中主要操作的对象coroutine,另一个是Task(实际上是Future的子类)作用为:
Wrap a coroutine or an awaitable in a future.
,即将一个协程或者是Awaitable对象转换成Future对象(确保这个是一个Future对象)==>参数需要为:TypeError: An asyncio.Future, a coroutine or an awaitable is required
asyncio.wait
中就调用了,实际上在执行协程的过程中都得转成Future对象
📖Future执行逻辑讲解:https://zhuanlan.zhihu.com/p/27258289
asyncio.create_task
:Python 3.7提出的统一的、更高阶的创建Task方法
asyncio.create_task
与asyncio.ensure_future
loop.create_task
接受的参数需要是一个协程,- 但是
asyncio.ensure_future
除了接受协程,还可以是Future对象或者awaitable对象:- 如果参数是协程,其实底层还是用的
loop.create_task
,返回Task对象 - 如果是Future对象会直接返回
- 如果是一个awaitable对象会await这个对象的__await__方法,再执行一次
ensure_future
,最后返回Task或者Future
- 如果参数是协程,其实底层还是用的
使用async修饰的函数调用时会返回一个协程对象,await只能放在async修饰的函数里面使用,await后面必须要跟着一个协程对象或Awaitable,await的目的是等待协程控制流的返回,而实现暂停并挂起函数的操作是yield。
异步库
- asyncio : 标准库
- 除了asyncio之外,curio和trio是更加轻量级的替代物,而且也更容易使用
- aiohttp: http库
- aiofiles: 文件库
- requests-html:支持异步访问
- aiomysql:mysql异步库
注意:
- 判别函数是否使用了协程: 可能针对整个程序是异步的。但是对于
main()
,它的for
循环还是阻塞的——for循环的任务并没有被添加到事件循环中,事件循环中实际上就只有main一个协程任务。具体案例见:https://zhuanlan.zhihu.com/p/65212327
▲. 可以对比看看 Go的协程 —— [译] 通过插图学习 Go 的并发
项目结构
关于项目结构组织。由于Python在执行程序时,会自动将
.
即当前路径加入到库搜索路径,所以当下路径下的包(文件夹下有__init__.py
)都能被检测到使用,如下面的handler、helper都可以直接在其他文件里通过helper.xxx
来调用,但是顶层的settings.py
不能通过feiyu_shoot.settings
来调用,即使feiyu_shoot工程下也有__init__.py
,因为项目工程的上层目录并没有加入到Python环境搜索中,所以他实际不知道谁是feiyu_shoot
因此,根据这种设计,可以将项目结构组织成两种
-
不依赖子模块的放在root目录的top接口:需要被其他文件引用的列入文件夹中作为库使用,不需要被引用的可以直接放顶层
1
2
3
4
5# helper/runner.py
# handler/ 在.下作为包导入
from handler imoprt ConfigHandler
# settings.py 在.下作为模块导入
from settings import USER_AGENT -
★将入口放在最外层,核心内容作为单独一个文件夹(模块):由于feiyu是个整体的包,所以在feiyu下的任意Python文件中都可以通过
from feiyu.xxx import yyy
来导入。
根据Python打包利器:auto-py-to-exe中打包计算机程序的方式,更推荐第二种,这样代码资源文件位置更统一,也更加清晰一些。
more: Python中模块、包、库定义
深入理解__file__
我们在定义一些配置文件、数据文件位置时,通常是基于脚本的位置,但是如果直接通过相对路径很容易出错,因此一般是通过
__file__
获得脚本的绝对路径后再进行相对的路径操作,常见代码:os.path.dirname(os.path.abspath(__file__))
从而获得当前脚本所在目录的绝对路径。(现在关于路径相关的操作是提倡用pathlib.path代替os.path,对应操作为Path(__file__).resolve().parent
)
But: pyinstaller打包后使用
__file__
报错有问题, https://github.com/pyinstaller/pyinstaller/issues/6719、https://github.com/pyinstaller/pyinstaller/pull/6616
os.path.dirname(__file__)
返回的是当前脚本的所在路径,使用pycharm和直接点击运行py文件,这个路径均为脚本的所在路径,而生成exe之后点击运行,这个路径变为exe释放路径C:Users...AppDataLocalTemp_MEI***
,所以log文件生成在这个路径下,在结束运行后,这个路径文件夹会被删除。
冻结二进制——Pyinstaller
自我学Python以来推荐的就是这个,经过时光变迁,这个仍然是主流。
- PyInstaller是一个跨平台的Python应用打包工具,支持 Windows/Linux/MacOS三大主流平台,能够把 Python 脚本及其所在的 Python 解释器打包成可执行文件,从而允许最终用户在无需安装 Python 的情况下执行应用程序。
- PyInstaller 制作出来的执行文件并不是跨平台的,如果需要为不同平台打包,就要在相应平台上运行PyInstaller进行打包。
- PyInstaller打包的流程:读取编写好的Python项目–>分析其中条用的模块和库,并收集其文件副本(包括Python的解释器)–>将副本和Python项目文件(放在一个文件夹//封装在一个可执行文件)中。
安装就不细说了,主要讲用法:
-
通过命令行命令打包
pyinstaller -F main.py
,常用参数- -F: 表示生成单个可执行文件;对应的是-D: 生成文件夹形式的可执行程序(默认)
- -w: 表示去掉控制台窗口,这在GUI界面时非常有用。不过如果是命令行程序的话那就把这个选项删除吧!
- -p: 表示你自己自定义需要加载的库路径,一般情况下用不到
- -i: 表示可执行文件的图标
-
通过
.spec
打包定义文件来打包,对应命令行的参数,.spec
文件都会有对应的生成内容。首先根据main文件生成spec文件:
pyi-makespec -D main.py
、填写好后再pyinstaller main.spec
- -D是让spec中多一个coll的实例,从而变成文件夹
-i
相当于.spec
中EXE中icon=".\\debugs\\favicon.ico"
- …(更多spec文件参数选择见:https://blog.csdn.net/tangfreeze/article/details/112240342)
- -p: 相当于Analysis实例中的
pathex
,就是填入自己的模块 - –hiden-import: 相当于Analysis实例中的hiddenimports
实际上,根据命令行的参数会生成对应的.spec
文件。
注:可以看到无论是pyinstaller
、pyi-makespec
后面都是main.py
,因为其是main函数入口文件
附:根据feiyu,在此列两个可行的:
-
F:\aDevelopment\Python\ShowYourCode\env\Scripts\pyinstaller.exe main.py --add-data="feiyu\push_config.ini;.\feiyu" --add-data="feiyu\user_config.toml;.\feiyu" -i debugs\favicon.ico
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35# -*- mode: python ; coding: utf-8 -*-
block_cipher = None # 此处在使用--key= 会有变化
a = Analysis(['ai\\main.py'],
pathex=['C:\\Users\\Admin\\Downloads\\marsai-master'],
binaries=[],
datas=[],# 此处可以添加静态资源,例如你有个图片文件夹imgs,可以这样写[('imgs','imgs'),('test.txt','.')],打包以后会有一个一样的文件夹,点表示当前文件夹。
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='main', # 生成的exe的名字
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True, # 打包的时候进行压缩,False表示不压缩
console=True # 是否显示黑窗口,刚开始打包的时候一般都会有问题,建议设为True,解决所有问题后可以设置为False)
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='main' # 文件夹的名字)
pyinstaller全都是控制台的命令,因此没那么直观,有人做了GUI相对直白、简单些,见Python打包利器:auto-py-to-exe,其关于计算器程序与Additional Files
的使用会让人对项目该如果布置结构和使用-p
参数有个更确信的认识(导入自己些的模块)。
functools.partial用法!
functools.partial
(偏函数)是用来包装一个函数的。并且针对的是函数的部分的参数。
其主要作用有两个:
-
在不修改别人代码的基础上, 给部分参数绑定值固定住,变成"新"的函数
1
2
3
4
5def show(name: str, age: int):
print(f"your name is {name} and age is {age}")
showWithA = functools.partial(show, "mrli")
showWithA(22)从而
showWithA
变成了只需要填写age
参数,但拥有show相同执行逻辑的"新"函数了 -
给不方便传递参数的函数,加上默认参数 —— 比如
requests-html
中的session.run
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class AsyncHTMLSession(BaseSession):
def run(self, *coros):
""" Pass in all the coroutines you want to run, it will wrap each one
in a task, run it and wait for the result. Return a list with all
results, this is returned in the same order coros are passed in. """
tasks = [
asyncio.ensure_future(coro()) for coro in coros
]
done, _ = self.loop.run_until_complete(asyncio.wait(tasks))
return [t.result() for t in done]
async def getDataByChromeDriver(index: Union[int, str]):
pass
# run方法中传的都是 asnyc def 函数(协程对象)
# session.run(asyncGetDataByChromeDriver, asyncGetUrls) 无法指定参数
session.run(functools.partial(asyncGetDataByChromeDriver, 1))可见,默认使用的话,只能传递函数地址,并不能指定要执行异步函数的参数。此时便可以通过
functools.partial
来给这些参数绑定默认值,从而实现了指定参数与此类似的还有处理回调函数: https://www.cnblogs.com/wxys/p/13756552.html。
⭐️本质上都是内部函数
func()
调用时,参数没办法在框架代码中指定,只能在传入函数地址的时候,将参数指定好,这样func()
调用时,就是想要运行的参数了
拓展容器
defaultdict
defaultdict接受一个工厂函数作为参数,如下来构造:
1 | dict =defaultdict( factory_function) |
其作用是在获得不存在的key时提供默认值,比如dict1["hello"]
返回的是0
heap
IO多路复用
selectors库:python3.4版本中引进的,它封装了IO多路复用中的select和epoll,能够更快,更方便的实现多并发效果。
它的功能与linux的epoll,还是select模块,poll等类似;实现高效的I/O multiplexing, 常用于非阻塞的socket的编程中; 简单介绍一下这个模块,更多内容查看 python文档:https://docs.python.org/3/library/selectors.html
模块定义了一个 BaseSelector的抽象基类, 以及它的子类,包括:SelectSelector, PollSelector, EpollSelector, DevpollSelector, KqueueSelector. 另外还有一个DefaultSelector类,它其实是以上其中一个子类的别名而已,它自动选择为当前环境中最有效的Selector,所以平时用 DefaultSelector类就可以了,其它用不着。
模块定义了两个常量,用于描述 event Mask
- EVENT_READ : 表示可读的; 它的值其实是1;
- EVENT_WRITE: 表示可写的; 它的值其实是2;
API
-
register(fileobj, events, data=None)
作用:注册一个文件对象。参数:
- fileobj——即可以是fd 也可以是一个拥有fileno()方法的对象;
- events——上面的event Mask 常量;
- data: 绑定的data属性,可以是处理函数。在获得SelectorKey时,通过SelectorKey.data来获得
返回值: 一个SelectorKey类的实例——一般用这个类的实例 来描述一个已经注册的文件对象的状态, 这个类的几个属性常用到:
- fileobj: 表示已经注册的文件对象;
- fd: 表示文件对象的描述符,是一个整数,它是文件对象的 fileno()方法的返回值;
- events: 表示注册一个文件对象时,我们等待的events, 即上面的event Mask, 是可读呢还是可写呢!!
- data: 注册时我们传入的参数,可以是任意值,绑定到一个属性上,方便之后使用。
-
unregister(fileobj)
作用: 注销一个已经注册过的文件对象;返回值:一个SelectorKey类的实例;
-
select(timeout=None)
作用: 用于选择满足我们监听的event的文件对象;返回值: 是一个(key, events)的元组, 其中key是一个SelectorKey类的实例, 而events 就是 event Mask(EVENT_READ或EVENT_WRITE,或者二者的组合)
-
close
() 作用:关闭 selector。 最后一定要记得调用它, 要确保所有的资源被释放; -
get_key(fileobj)
:返回与已注册文件对象关联的密钥。这将返回与此文件对象关联的SelectorKey实例,如果未注册文件对象,则返回KeyError。
-
get_map()
返回文件对象到 selectors 键的 Map。
1 | import selectors |
Socket编程
创建套接字的函数是socket(),函数原型为:socket.socket(family=-1, type=-1, proto=-1, fileno=None)
- AF_UNIX(本机通信)
- AF_INET(TCP/IP – IPv4)
- AF_INET6(TCP/IP – IPv6)
其中 “type”参数指的是套接字类型,常用的类型有:
- SOCK_STREAM(TCP流)
- SOCK_DGRAM(UDP数据报)
- SOCK_RAW(原始套接字)
最后一个 “protocol”一般设置为“0”,也就是当确定套接字使用的协议簇和类型时,这个参数的值就为0,但是有时候创建原始套接字时,并不知道要使用的协议簇和类型,也就是domain参数未知情况下,这时protocol这个参数就起作用了,它可以确定协议的种类。
默认为AF_INET、SOCK_STREAM、protocol=0
Author: Mrli
Link: https://nymrli.top/2022/03/27/Python进阶/
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.