Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

Python进阶

2022/05/26 Python
Word count: 21,128 | Reading time: 85min

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
2
3
4
5
6
7
class p():
s = 2
def __getattr__(self, item):
return "hello"
d = p()
print(d.s) # ==> 2
print(d.p) # ==> hello

注:如果属性不存在,则不管是否有__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
2
3
4
5
6
7
8
9
10
11
12
13
14
class TestDes:
def __get__(self, instance, owner):
print(instance, owner)
return 'TestDes:__get__'

class TestMain:
des = TestDes()

if __name__ == '__main__':
t = TestMain()
# des虽然被定义成类变量,但t.des还是跟实例相关的,对其增删改都会走到其描述器对象的__get__、__set__、__delete__中处理
print(t.des)
print(TestMain.des)
# 其中TestDes定义了__get__方法,在TestMain中,定义了一个类属性des,是TestDes的一个实例,我们访问t.des或者TestMain.des的时候访问的就是访问了TestDes的__get__方法。

▲描述器往往以装饰器的方式被使用,导致二者常被混淆。描述器类和不带参数的装饰器类一样,都传入函数对象作为参数,并返回一个类实例,所不同的是,装饰器类返回 callable 的实例,描述器则返回描述器实例

Q:如果实例中有和描述器重名的属性 x 怎么办?

A:资料和非资料描述器的区别在于,相对于实例字典的优先级不同。当描述器和实例字典中的某个属性重名,按访问优先级,资料描述器 > 同名实例字典中的属性 > 非资料描述器 or (数据描述符 > 实例变量 > 非数据描述符),优先级小的会被大的覆盖。==>类的方法实际就是一个仅实现了 __get__() 的非资料描述器,所以如果实例 c 中同时定义了名为 foo 的方法和属性,那么 c.foo 访问的是属性而非方法。

描述器more:Python 描述器解析

针对描述器的说明: 描述器是被__getattribute__调用的,如果重写了这个方法,将会阻止自动调用描述器,资料描述器总是覆盖了实例的__dict__, 非资料描述器可能覆盖实例的__dict__

小结:访问存在的属性,如果是描述器,描述器生效

1
2
3
4
5
6
7
8
9
10
11
12
13
class TestDes:
# 方法的第一个参数是实际拥有者的实例,如果没有则为None,第二个参数是实际所属的类。
def __get__(self, instance, owner):
print(instance, owner)
return 'TestDes:__get__'

class TestMain:
des = TestDes()

if __name__ == '__main__':
t = TestMain()
print(t.des)
print(TestMain.des) # 调用时instance输出None
  • 非资料描述器,也就是只有__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的,所以不会遇到descriptor
  • A.__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. 函数的可变参数

    当函数的参数前面有一个星号*的时候表示这是一个可变的位置参数,两个星号**表示是可变的关键字参数。

    1
    2
    3
    4
    5
    6
    def 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")
  2. unpack参数

    星号*把序列/集合解包(unpack)成位置参数,两个星号**把字典解包成关键字参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    txtOptions = {
    "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**

类比Go中的...语法

装饰器

装饰器(Decorators)是 Python 的一个重要部分,通过他们可以在不用更改原函数的代码前提下,修改、拓展其他函数的功能的函数,他们有助于让我们的代码更简洁。==>切面编程的方式

闭包

Python查找变量会一层层地向外层查找,直到global全局也没有时raise NameError: name 'xxx' is not defined

在装饰器中运用到了闭包的思想。闭包,一句话说就是,在函数中再嵌套一个函数,并且引用外部函数的变量,这就是一个闭包了。(从而使得内部函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起)

执行闭包后,闭包实例将会维持了一个词法环境,其中包含对局部变量的引用,使得原本就应该失效的局部变量仍然存活。

闭包的应用

1
2
3
4
5
6
7
8
9
10
11
function makeAdder(x) {
return function(y) {
return x + y;
};
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。

add5add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 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
      17
      from functools import wraps
      def do_log(func):
      @wraps(func)
      def wrapper(*args, **kwargs):
      print("do before")
      func(*args, **kwargs)
      print("do after")

      return wrapper

      @do_log
      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
      9
      function 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
    30
    def 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):
    @wraps(func)
    def wrapper(*args, **kwargs):
    getattr(logger, level)("do before")
    func(*args, **kwargs)
    getattr(logger, level)("do after")
    return wrapper
    return do_log

    @log_by_level("warning")
    def say(word):
    print(word)

    # 等价于
    do_log = log_by_level("warning") # 作用是提供了一个level=warning的环境
    say = do_log(say)
    say("hello")
  • 可自定义属性的装饰器:

    允许用户提供参数在运行时控制装饰器行为。比如前端设置一个单选,可以控制日志输出级别,此时则需要在运行时修改装饰器行为

  • 可选参数

    既可以不传参数给它,比如 @decorator , 也可以传递可选参数给它,比如 @decorator(x,y,z)

    实现见:9.6 带可选参数的装饰器 — python3-cookbook 3.0.0 文档

  • 类装饰器:

    使用一个装饰器去包装函数,返回的是一个可调用的实例

    类装饰器主要依靠类的__call__方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class ClassDecorator:
    def __init__(func):
    self.func = func
    def __call__(self):
    print("enter")
    self.func()
    print("end")
    @ClassDecorator
    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
    @a
    @b
    @c
    def f ():
    pass
    # 等价于 f = a(b(c(f)))

总结:

理解装饰器应该从①@语法糖到底做了什么;②闭包是什么,有什么作用;③装饰器如何等价表示;来理解

懒加载属性

描述符+类装饰器实现

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
36
37
38
39
40
41
42
43
44
45
46
47

class LazyProperty(object):
def __init__(self, func):
# 初始化func为serverHost函数
self.func = func

def __get__(self, instance, owner):
if instance is None:
return self
else:
value = self.func(instance)
# 会将结果值通过setattr方法存入到instance实例的__dict__中
setattr(instance, self.func.__name__, value)
return value


class ConfigHandler:
def __init__(self):
pass
# 返回一个LazyProperty实例
@LazyProperty
def serverHost(self):
return os.environ.get("HOST", setting.HOST)

setting = namedtuple("setting", ["HOST"])
setting.HOST = "g"

# 测试一
a = ConfigHandler()
print(a.__dict__)
# 1. 注解先是创建了LazyProperty(serverHost)的实例
# 2. 再是语法糖进行了赋值serverHost = LazyProperty(serverHost)
# 3. 当第一次进行调用的时候, instance = configHandler**实例**, self.func(instance实例) == 调用serverHost(instance实例)从而获得了真正值value。而之后的 setattr处将self实例的__dict__中添加了serverHost-value,再次访问self.serverHost时, 已经不再是函数, 而是value值(serverHost不再从ConfigHandler.__dict__中取, 而是实例a.__dict__中取)
print(a.serverHost)
print("say")
print(a.__dict__)

# 测试二
# 如果先执行类的__dict__能看到类的serverHost是一个**描述器对象实例**=> 'serverHost': <__main__.LazyProperty object at 0x0000020A1AEB7FD0>
print(ConfigHandler.__dict__)
# 通过__dir__能见到serverHost为实例的一个方法
print(a.__dir__())
# 此时a.__dict__为空
print(a.serverHost)
# 等到调用过a.serverHost后可以发现a.__dict__中多了serverHost
print(a.__dict__)
# 由于实例__dict__会优先于类的__dict__使用,所以直接返回了value值

重点:

  1. 跟[print(t.des)](#3.object.__get__(self, instance, owner))会触发t.des指向的descriptor实例的__get__一样,通过类__dict__["serverHost"],其也是个描述器实例,因此也会触发LazyProperty object的__get__
  2. 实例__dict__会优先于类的__dict__使用,如果实例__dict__找不到,会往上类__dict__

修饰符(方法装饰器)

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
def lazy_property(func):
# 创建protected属性
attr_name = "_lazy_" + func.__name__
@property
def _lazy_property(self):
# print("done")
if not hasattr(self, attr_name):
# print("set")
setattr(self, attr_name, func(self))
return getattr(self, attr_name)
return _lazy_property


class Circle(object):
def __init__(self, radius):
self.radius = radius

@lazy_property
def area(self):
return 3.14 * self.radius ** 2

# 当解析Circle类、定义area方法的时候,会将Circle.area = @property修饰的_lazy_property函数
c = Circle(4)
print('before calculate area')
print(c.__dict__)
# 当调用c.area时,会输出done, 此时会执行_lazy_property内的具体函数, 此次会进行setattr
print(c.area)
# 此次不会调用setattr
print(c.area)
print('after calculate area')
print(c.__dict__)
c.radius = 5

借鉴:Python中的lazy property

由单例元类引发的知识点

看开源代码时,看到了下面一段代码,于是对withMetaclass产生了好奇,经过了解发现其作用是six对python2和python3使用元类兼容的写法。

1
2
3
4
# Python2和3兼容使用元类写法
class ConfigHandler(withMetaclass(Singleton)):
def __init__(self):
pass

因此,上述代码在Python3中相当于

1
2
3
4
# Python3元类使用写法
class ConfigHandler(metaclass=Singleton):
def __init__(self):
pass

那么,问题来了,withMetaclass到底是怎么实现兼容的呢?下面是其实现代码

1
2
3
4
5
def withMetaclass(meta, *bases):
class MetaClass(meta):
def __new__(cls, name, this_bases, d):
return meta(name, bases, d)
return type.__new__(MetaClass, 'temporary_class', (), {})

可以看到其中出现了不少我们很少看到的使用方法。接下来我们就仔细的学习上述写法为什么可以成功。

元类使用可以参考: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class demoClass:
instances_created = 0
def __new__(cls, *args, **kwargs):
# __new__(): <class '__main__.demoClass'> ('abc',) {}
print("__new__():", cls, args, kwargs)
# 1. 通过父类__new__生成一个实例: 调用父类object.__new__生成实例(Create and return a new object.)
instance = super().__new__(cls)
# 2. 自己重写要实现的逻辑
instance.number = cls.instances_created
cls.instances_created += 1
# 3. 将父类生成的实例返回
return instance

def __init__(self, attribute):
# __init__(): <__main__.demoClass object at 0x00000185A6466EB0> abc
print("__init__():", self, attribute)
self.attribute = attribute

test1 = demoClass("abc")
test2 = demoClass("xyz")
print(test1.number, test1.instances_created)
print(test2.number, test2.instances_created)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 定义一个元类,继承type。因为只有继承type才能通过重写__new__来拦截创建过程
# ▲注意,继承type后__new__能拿到的参数信息跟不继承type的有天壤之别,原因继续看下去
class FirstMetaClass(type):
# cls代表元类的类: FirstMetaClass
# name代表自定义类的类名: CLanguage
# bases代表被动态修改的类的所有父类
# attr代表被动态修改的类的所有属性、方法组成的字典
def __new__(cls, name, bases, attrs):
# 动态为该类添加一个name属性
attrs['name'] = "C语言中文网"
attrs['say'] = lambda self: print("调用 say() 实例方法")
return super().__new__(cls,name,bases,attrs)

# 定义类时,指定元类
class CLanguage(object, metaclass=FirstMetaClass):
pass

clangs = CLanguage()
print(clangs.name)
clangs.say()

可以看到,在创建类时,通过在标注父类的同时指定元类(格式为metaclass=元类名),则当 Python 解释器在创建该类实例时,FirstMetaClass(type)元类中的__new__方法就会被调用,其中bases和attrs能拿到自定义类的参数,从而实现动态修改类属性或者类方法的目的。

元类和父类的区别:

在定义子类的时候,我们有两个选择:①是传需要继承的父类;②自定义的元类。

  • 父类是子类的模板,子类的功能是跟父类紧耦合的,子类和父类一般是一一对应的
  • 元类是子类的修饰器,可以为该子类和其他子类都添加自定义功能,并且不在继承关系中(Class.__mro__查看),子类和元类是一对多的关系。元类并不是特地为某个子类服务的
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
class TestMeta3(type):
def __new__(cls, name, bases, attrs):
print(cls) # 当前类
print("name", name) # 如果是通过metaclass触发的,此处为调用metaclass的类的类型
print("bases", bases) # 如果是通过metaclass触发的,此处为调用metaclass的类的父类
print("attrs", attrs) # 如果是通过metaclass触发的,此处为调用metaclass的类的属性
return type.__new__(cls, name, bases, attrs)

class Pa3:
pass

# python3中可以直接通过metaclass关键字参数来指定类的元类
class Eg3(Pa3, metaclass=TestMeta3):
@classmethod
def get(self):
kkk = []
kkk.append(self.__skiless__)
return kkk

def acc2(self):
return 'a2'
"""
输出
<class '__main__.TestMeta3'>
name Eg3
bases (<class '__main__.Pa3'>,)
attrs {'__module__': '__main__', '__qualname__': 'Eg3', 'get': <classmethod object at 0x00000263511C6FA0>, 'acc2': <function Eg3.acc2 at 0x00000263511C5310>}
"""

在定义的时候,发现竟然有输出。因为定义的时候,python解释器会在当前类中查找metaclass[3],如果找到了,就使用该metaclass创建Eg3类。所以打印出来的name、bases、attrs都和Eg3有关。

with_metaclass

由于python2和python3中元类使用方法的不同,我们需要使用一种兼容的方式[1],如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def withMetaclass(meta, *bases):
"""Create a base class with a metaclass."""
class MetaClass(meta):
# 如果删除__new__,则类.__mro__中能看到meta类
def __new__(cls, name, this_bases, d):
# 因为meta是类,所以这边是在调用meta的__call__()。▲bases,d为ConfigHandler的父类和属性
return meta(name, bases, d)
# 返回一个新类型, type.__new__()要求第一个必须是type的子类
return type.__new__(MetaClass, 'temporary_class', (), {})
# 下面两句话等价,withMetaclass是为了兼容python2和python3,2中没有metaclass关键字
# 其在创建实例时,__new__方法会被MetaClass拦截(其实就是子类没定义__new__,走了父类的__new__)
class ConfigHandler(withMetaclass(Singleton))
# python3写法
class ConfigHandler(metaclass=Singleton)

# 因为ConfigHandler相当于继承了 type.__new__返回的类MetaClass,所以在c = ConfigHandler()实例化的时候,会触发Metaclass的__new__然后调用meta.__call__从而返回一个对象

with_metaclass返回的临时类中,本身无任何属性,但包含了元类和基类的所有信息,并在下一步定义类时将所有信息解包出来[1]。

见:★Python 元类及with_metaclass

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
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
36
37
38
# 注意这边继承了type, 所以下面的__call__是重写type的__call__,即创建实例的方法
class Singleton(type):
"""
Singleton Metaclass
"""
_inst = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._inst:
cls._inst[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._inst[cls]

def withMetaclass(meta, *bases):
"""Create a base class with a metaclass."""
# 这需要一点解释:基本思想是为一个级别的类实例化创建一个虚拟元类,用实际的元类替换它自己。

# KeyPoint1. 继承meta类
class MetaClass(meta):
# 实际上下面的__new__不影响
def __new__(cls, name, this_bases, d):
# cls为withMetaclass; name为使用者的类型; this_bases为使用者的父类们; d为使用者的属性
return meta(name, bases, d)
# KeyPoint2. type.__new__创建一个名称叫temporary_class,类型为MetaClass的类
# ▲注意type.__new__中的类类型必须是type的子类
return type.__new__(MetaClass, 'temporary_class', (), {})

class ConfigHandler(withMetaclass(Singleton)):
def __init__(self):
print("__init__")
pass
@LazyProperty
def serverHost(self):
return os.environ.get("HOST", setting.HOST)
# res=withMetaclass(Singleton)的类型为<class 'util.six.withMetaclass.<locals>.MetaClass'>
# 将其传给ConfigHandler作为父类,在定义 ConfigHandler 时会触发MetaClass.__new__, 于是调用meta(name, bases, d),此处的meta为Singleton,而name为ConfigHandler类, bases为空, d为ConfigHandler的属性和方法。

# print(type(ConfigHandler)) ==> <class 'util.singleton.Singleton'>
# 因此 c = ConfigHandler() ==> Singleton的__call__方法,
当c = ConfigHandler()时会因为type.__new__(MetaClass, 'temporary_class', (), {})去找MetaClass的__call__进行调用,MetaClass没有__call__则找到了其父类meta(Singleton)的__call__

注:类也是对象,是元类的对象,即我们实例化一个类时,调用其元类的__call__(cls, *args, **kwargs)方法进行创建对象。

__call__

一个非常特殊的实例方法,即__call__()。该方法的功能是在类中重载了对象的 () 运算符,使得类实例对象可以像调用普通函数那样,以“对象名()”的形式使用。

实际上,如果不重写__call__的话,Class.__call__(*args, **kwargs)还承担着产生类实例的功能(会调用父类(可以通过Class.__class__来查看父类)的type.__call__其会返回一个实例)

案例一:

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
# 默认继承的是object, 而不是type
class Meta:
def __init__(self, name):
print("init")
self.name = name
def __call__(self, *args, **kwargs):
print("call")
# 当没有重写__call__时,无论是显式的调用__call__,还是通过()运算符调用,都会调用type.__call__返回一个实例
res = Meta.__call__("asd")
print(res, type(res))
res = Meta("asd")
print(res, type(res))
"""
init
<__main__.Meta object at 0x0000014FB5115EE0> <class '__main__.Meta'>
init
<__main__.Meta object at 0x0000014FB5115A90> <class '__main__.Meta'>
上述两种都能创建对象
"""
# 当重写__call__以后, __call__()返回实例的效果就失效了==>因为上述代码没有return
# 此时 Meta()与Meta.__call__()不再等价
"""
call
None <class 'NoneType'>
init
<__main__.Meta object at 0x0000016CC2745EE0> <class '__main__.Meta'>
"""

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
2
>>> Foo.__class__
<class 'type'>

所以Foo是类型type的一个对象,并且调用type类的__call__(self, *args, **kwargs)返回一个Foo类的对象。让我们看下type中的__call__方法是什么样的。这个方法相当的复杂,但是我们将其C代码转成Python代码,并尝试尽量简化它,结果如下。

1
2
3
4
5
6
7
8
9
10
11
class type(object):
# 这边的obj_type跟cls一样
def __call__(obj_type, *args, **kwargs):
# 通过__new__创建一个空的类实例,如果obj_type没有__new__则使用type.__new__
obj = obj_type.__new__(*args, **kwargs)
# 进行类型检查
if obj is not None and issubclass(obj, obj_type):
# 对类进行__init__初始化
obj.__init__(*args, **kwargs)
# 返回类实例
return obj

可见__new__方法为对象分配了内存空间,构建它为一个“空"对象然后__init__方法被调用来初始化它。

那我们定义了一个具体类来讲解这个过程。首先明确一点:Foo相对于产生了一个type实例化对象

1
2
3
4
class Foo(object):
def __init__(self, x, y=0):
self.x = x
self.y = y

获得实例化对象**Foo(*args, **kwargs)也可以看作是type对象()调用了type中()运算符的触发的函数type.__call__从而创建一个Foo的实例**

  • 至于type.__call__发生了什么就是上面抽象代码中介绍的那般,调用type.__new__(Foo, *args, **kwargs)然后返回一个对象实例obj。
  • obj随后通过调用obj.__init__(*args, **kwargs)被初始化。
  • objtype.__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
2
3
4
5
6
7
class Singleton(type):
_inst = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._inst:
# super(Singleton, cls).__call__调用的是type.__call__(自定义类类名name, 自定义类父类bases, 自定义类属性attrs)
cls._inst[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._inst[cls]

函数和模块的特殊属性__annotations__

众所周知,Python是一种动态类型语言,也是强类型语言。在Python语言中,使用变量之前不需要声明其类型,直接赋值即可创建变量,变量初始类型取决于等号右侧表达式的值的类型。创建之后,变量的类型可以随时发生变化,但在任何时刻,每个变量都有确定的类型

同理,在定义函数和类的方法时,也不需要声明形参类型,完全取决于实参类型.

因此很多从其他语言转过来的朋友很不习惯这样的方式,还是习惯于声明变量和参数的类型。虽然Python不支持声明,但是允许在定义函数时使用“注解”的形式来标注形参和返回值的类型,但这种注解的形式在运行时并不会对形参进行任何约束和检查,在实际调用函数时,即使实参不符合形参的类型标注,一样能够正常传递(IDE会进行检查,在编译之前提醒传参不一致,但也不会报错)

1
2
3
# 标注了形参s的类型为str或者dict; 也标注了返回值为str类型
def extract_value(s: Union[str, dict]) -> str:
pass

在Python中,函数会维护一个特殊属性__annotations__,这是一个字典,其中的“键”是被注解的形参名,“值”为注解的内容。使用时并不要求注解的内容是Python中的类型,可以是任意内容。例如,

从官方文档来看,函数的__annotations__属性只包含形参和返回值的注解,即使在函数体中有类似的注解,但这并不等价于C语言中的变量声明,①这样的注解不会创建变量,②也不会被收集到这个特殊属性__annotations__中。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def demo(param1: int, param2: int)->int:
return param1 * param2
print(demo.__annotations__)
# {'param1': <class 'int'>, 'param2': <class 'int'>, 'return': <class 'int'>}

def demo(a: 3, b: int) -> int:
c: 5
d: 8
return a * b * c * d

print(demo.__annotations__)
# {'a': 3, 'b': <class 'int'>, 'return': <class 'int'>}
demo(3, 8)
# Traceback (most recent call last):
# return a * b * c * d
# UnboundLocalError: local variable 'c' referenced before assignment

另外,在模块中也有个特殊属性__annotations__用于收集模块中变量的注解,但这些注解同样也不会创建对应的变量。例如,在下面的代码中,并没有创建变量e、f、g。

用途:修改dataclass构造

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from dataclasses import dataclass, field


@dataclass
class Goods:
# 商品上架时间
addTime: str
# 商品上架创建者
addUserId: str
# 商品更新操作者
updateUserId: str
# 商品id
goodsId: str
# 商品详情描述
content: str
# 商品名称
goodsName: str
# 尺寸
specification: str
# 作者
author: str
# 展示图片链接
imgUrl: str
# 钻石价格
price: int
# 现金价格
cashPrice: int
# 绑定的对应订单ID
bindingOrderId: int
# 拍卖者用户id
userId: int
#
alrHandNum: int
#
deliveryMoney: float
#
silver: int
#
startNum: int
#
endNum: int

@classmethod
def from_kwargs(cls, **kwargs):
# split the kwargs into native ones and new ones
native_args, new_args = {}, {}
for name, val in kwargs.items():
# __annotations__是上述标注的属性
if name in cls.__annotations__:
native_args[name] = val
else:
new_args[name] = val
# use the native ones to create the class ...
ret = cls(**native_args)
# ... and add the new ones by hand
for new_name, new_val in new_args.items():
# 这些额外的属性会被存放在self.__dict__中, 但是dataclass只会根据标注的属性来定义__repr__, 因此这些new_agrs参数不会在__repr__中出现
setattr(ret, new_name, new_val)
return ret

Dataclass(数据类)

python3.7的新特性dataclass,其之前类似的实现是@attr.s,主要用来描述数据对象,其含义为“一个带有默认值的可变的namedtuple”,广义的定义就是有一个类,它的属性均可公开访问,可以带有默认值并能被修改,而且类中含有与这些属性相关的类方法,那么这个类就可以称为dataclass,再通俗点讲,dataclass就是一个含有数据及操作数据方法的容器。

乍一看可能会觉得这个概念不就是普通的class么,然而还是有几处不同:

  1. 相比普通class,dataclass通常不包含私有属性,数据可以直接访问
  2. dataclass的repr方法通常有固定格式,会打印出类型名以及属性名和它的值
  3. dataclass拥有__eq____hash__魔法方法
  4. dataclass有着模式单一固定的构造方式,或是需要重载运算符,而普通class通常无需这些工作

因为数据对象类有些固定的行为,也正适合程序为我们自动生成,因此dataclasses模块诞生了。

dataclasses模块中提供了一些常用函数供我们处理数据类。

  • 使用dataclasses.asdictdataclasses.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
2
3
4
5
6
@dataclass
class C:
# 实际上等于 age: int = field()
age: int

mylist: List[int] = field(default_factory=list)
  • 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返回的这个对象。

▲举个例子,对于list,当复制它时只是复制了一份引用,所以像dataclass里那样直接复制给实例的做法的危险而错误的,为了保证使用list时的安全性,应该这样做:

1
2
3
@dataclass
class C:
mylist: List[int] = field(default_factory=list)
  • **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不再是一个可访问的数据对象。

参考:https://www.cnblogs.com/apocelipes/p/10284346.html、https://docs.python.org/zh-cn/3/library/dataclasses.html

dataclasses嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

def nested_dataclass(*args, **kwargs):
def wrapper(cls):
cls = dataclass(cls, **kwargs)
original_init = cls.__init__

def __init__(self, *args, **kwargs):
for name, value in kwargs.items():
field_type = cls.__annotations__.get(name, None)
if is_dataclass(field_type) and isinstance(value, dict):
new_obj = field_type(**value)
kwargs[name] = new_obj
if isinstance(value, list) and is_dataclass(field_type[0]):
res = []
for c in value:
new_obj = field_type[0](**c)
res.append(new_obj)
kwargs[name] = res
original_init(self, *args, **kwargs)

cls.__init__ = __init__
return cls

return wrapper(args[0]) if args else wrapper

**解包得到的额外信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 额外信息
@classmethod
def from_kwargs(cls, **kwargs):
# split the kwargs into native ones and new ones
native_args, new_args = {}, {}
for name, val in kwargs.items():
if name in cls.__annotations__:
native_args[name] = val
else:
new_args[name] = val

# use the native ones to create the class ...
ret = cls(**native_args)

# ... and add the new ones by hand
for new_name, new_val in new_args.items():
setattr(ret, new_name, new_val)
return ret

生成器、迭代器、容器

在使用Python的过程中,经常会和列表/元组/字典(list/tuple/dict)、容器(container)、可迭代对象(iterable)、迭代器(iterator)、生成器(generator)等这些名词打交道,众多的概念掺杂到一起难免会让人一头雾水,这里我们用一张图来展现它们之间的关系——from: Python迭代器和生成器详解

迭代器-生成器

Python中的容器

容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个迭代获取,可以用 in,not in 关键字判断元素是否包含在容器中。

我们常用的 string、set、list、tuple、dict 都属于容器对象。

尽管大多数容器都提供了某种方式获取其中的每一个元素,但这并不是容器本身提供的能力,而是可迭代对象赋予了容器这种能力,当然并不是所有容器都是可迭代的。

可迭代对象与Iterator(迭代器)

可迭代对象

可以返回一个迭代器的对象都可以称之为可迭代对象。

1
2
3
4
5
>>> x = [1, 2, 3]
>>> a = iter(x)
>>> b = iter(x)
>>> next(a)
1

a 和 b 是两个独立的迭代器,迭代器内部有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代时获取正确的元素。迭代器有一种具体的迭代器类型,比如 list_iteratorset_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def foo():
try:
yield 1
except ValueError:
print('捕获到 ValueError')
f = foo()
print(next(f))
f.throw(ValueError)
"""
1
捕获到 ValueError
Traceback (most recent call last):
File "D:\python3.6\1.py", line 9, in <module>
f.throw(ValueError)
StopIteration
"""

显然,一开始生成器函数在 yield 1 处暂停执行,当执行 throw() 方法时,它会先抛出 ValueError 异常,然后继续执行后续代码直至找到下一个 yield 语句,在执行过程中ValueError被try捕捉,而之后由于程序后续不再有 yield 语句,因此执行到最后会抛出一个 StopIteration 异常。

案例见:http://c.biancheng.net/view/7090.html

分析一个最简单的生成器:

1
2
3
4
5
6
7
8
def foo():
bar_a = yield "hello"
bar_b = yield bar_a
yield bar_b
f = foo()
print(f.send(None))
print(f.send("C语言中文网"))
print(f.send("http://c.biancheng.net"))

分析一下此程序的执行流程:

  1. 首先,构建生成器函数,并利用器创建生成器(对象)f 。
  2. 使用生成器 f 调用无参的 send() 函数,其功能和 next() 函数完全相同,因此开始执行生成器函数,即执行到第一个 yield “hello” 语句,该语句会返回 “hello” 字符串,然后程序停止到此处(注意,此时还未执行对 bar_a 的赋值操作)。
  3. 下面开始使用生成器 f 调用有参的 send() 函数,首先它会将暂停的程序开启,同时还会将其参数“C语言中文网”赋值给当前 yield 语句的接收者,也就是 bar_a 变量。程序一直执行完 yield bar_a 再次暂停,因此会输出“C语言中文网”。
  4. 最后依旧是调用有参的 send() 函数,同样它会启动餐厅的程序,同时将参数“http://c.biancheng.net”传给 bar_b,然后执行完 yield bar_b 后(输出 http://c.biancheng.net),程序执行再次暂停。

yield和yield from

在进入协程之前,我们需要明白yieldyield from,其是官方实现协程的关键.

yield from 是在Python3.3才出现的语法

  • yield是每次“惰性返回”一个值
  • yield from是“从什么(生成器)里面返回,实际上就是委托给了另一个生成器:
    • yield from 后面可以跟的可以是“ 生成器 、元组、 列表、range()函数产生的序列等可迭代对象
    • 从本质上讲,委托给了另一个生成器的意思是: yield from iterable本质上等于 for item in iterable: yield item

当然除了最显而易见的使用区别以外,yield from还对yield的不足进行了改进。

  1. 针对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。

  2. 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调用另一个生成器。

  3. 可以帮助处理StopIteration异常: 我们在调用生成器的时候,一般要么for,要么next。在next的时候我们就得自己去关注生成器什么时候会爆出StopIteration异常,而将其yield from委托给委派生成器后,调用者直接对委派生成器操作,其会自动帮我们处理thorw()send()close()

    任何使用send()方法发给委派生产器(即外部生产器)的值被直接传递给迭代器。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,委派生产器恢复继续执行yield from后面的语句;若迭代器产生其他任何异常,则都传递给委派生产器

  4. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio
import time
# 1. 在普通的函数前面加 async 关键字;
async def visit_url(url, response_time):
"""访问 url"""
# 2. await 表示在这个地方会等待子函数执行完成,再往下执行。(在并发操作中,把程序控制权教给主程序,让他分配其他协程执行。实际就是让出CPU时间片,挂载起任务。) await 只能在带有 async 关键字的函数中声明。
await asyncio.sleep(response_time)
return f"访问{url}, 已得到返回结果"

start_time = time.perf_counter()
task = visit_url('http://wangzhen.com', 2)
# 3, asynico.run() 运行程序
asyncio.run(task)
print(f"消耗时间:{time.perf_counter() - start_time}")

由于上面只有一个任务,不涉及任务切换,因此写一个可以比较的实验代码

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
import asyncio
import time

async def visit_url(url, response_time):
"""访问 url"""
await asyncio.sleep(response_time)
return f"访问{url}, 已得到返回结果"

async def run_task():
"""收集子任务"""
task_1 = visit_url('http://wangzhen.com', 2)
task_2 = visit_url('http://another', 3)
# 当运行到 task_1 中的sleep时会挂载起来,让task2执行
await asyncio.run(task)
# 由于 task_1 被挂起了,因此就运行task2,呆执行到task2中sleep时task2也会被挂起,此时线程只好不断检查是否有可用的协程,直至 task_1 的sleep结束, 继续运行task_1之后的逻辑
await asyncio.run(task_2)

asyncio.run(run_task())
print(f"消耗时间:{time.perf_counter() - start_time}")

# asyncio.run可以改成如下
import asyncio
loop = asyncio.get_event_loop(visit_url('http://wangzhen.com', 2))
# 多个任务的话, 通过asyncio.wait来等待同时完成
res = loop.run_until_complete(asyncio.wait([visit_url('http://wangzhen.com', 2), visit_url('http://another', 3)]))
# 注: run_until_complete传的是 types.CoroutineType 类型, 不能是 types.AsyncGeneratorType
# asyncio.run是 3.7 的新内容

明确的点:

  1. 增加并发,实际上就是减少一些无效的等待,由于一些IO任务会比较耗时,如果一直让CPU等待则大大减低了执行效率,因此await就是碰到耗时任务时,主动告诉cpu让其去做其他事,所以await后面都是耗时函数

  2. 由于await的设计:后面都必须跟的是Awaitable对象,或者是实现了其协议的对象,因此跟一般写法有些许出入,比如time.sleep改成了async.sleep,比如requests.get改成了await aiohttp.ClientSession().get

    awaitable对象必须满足如下条件中其中之一

    • 原生协程对象
      • 通过async def定义的函数是原生的协程对象,async def download(),但如果download内部没有调用异步代码,即使用的是requests.get,阻塞后实际上还是串行的,所以要用异步的aiohttp代替requests
    • types.coroutine()修饰的基于生成器的协程对象,注意不是Python3.4中asyncio.coroutine
    • 实现了__await__ method,并在其中返回了iterator的对象
  3. await只能用在async标明的函数中

参考:

注:

  • 多线程主要用于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
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
36
37
38
39
40
41
42
import asyncio
import time
from concurrent.futures import wait, ALL_COMPLETED
from concurrent.futures.thread import ThreadPoolExecutor

import aiohttp
import requests
times = 100

if __name__ == '__main__':
def tt():
r = requests.get("http://httpbin.org/get")
return r.status_code
# 单线程
start = time.time_ns()
for i in range(times):
tt()
print((time.time_ns() - start)/1e6)
# 46204.7239

# 多线程
start = time.time_ns()
with ThreadPoolExecutor() as f:
task = [f.submit(tt) for i in range(times)]
wait(task, 120, return_when=ALL_COMPLETED)
print((time.time_ns() - start)/1e6)
# 6078.0416

# 协程
start = time.time_ns()
async def asyncGet():
async with aiohttp.ClientSession() as sess:
async with sess.get("http://httpbin.org/get") as resp:
return await resp.json()
async def userth(t):
res = await asyncGet()
print(t)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait([userth(i) for i in range(times)]))
loop.close()
print((time.time_ns() - start)/1e6)
# 997.2545

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
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
36
37
38
39
40
41
42
43
44
45
46
47
import asyncio
import time
from concurrent.futures import wait, ALL_COMPLETED
from concurrent.futures.thread import ThreadPoolExecutor

import aiohttp
import requests

times = 100

if __name__ == '__main__':
start = time.time_ns()
async def asyncGet():
async with aiohttp.ClientSession() as sess:
async with sess.get("http://httpbin.org/get") as resp:
return await resp.json()
async def userth(t):
res = await asyncGet()
# print(t)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait([userth(i) for i in range(times)]))
loop.close()
print((time.time_ns() - start)/1e6)
# 7488.398822

# 使用uvloop
try:
import uvloop
except ModuleNotFoundError:
print("无法启用uvloop")
else:
print("启用uvloop")
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
start = time.time_ns()
async def asyncGet():
async with aiohttp.ClientSession() as sess:
async with sess.get("http://httpbin.org/get") as resp:
return await resp.json()
async def userth(t):
res = await asyncGet()
# print(t)
loop = asyncio.get_event_loop()
done, _ = loop.run_until_complete(asyncio.wait([userth(i) for i in range(times)]))
# print([t.result() for t in done]) # 从request-html源码中看到
loop.close()
print("uvloop", (time.time_ns() - start)/1e6)
# uvloop 4617.166035

注: 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_looploop.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是无序的所以这也就是我们的任务不是顺序执行的原因。

    1. wait的返回值是一个元组,包括两个集合:donepending,分别表示已完成的协程和超时未完成的task,想知道done的结果需要通过.result来获得resultg结果
    2. wait第二个参数为一个超时值,达到这个超时时间后,未完成的任务状态变为pending,当程序退出时还有任务没有完成此时就会看到错误提示。
    1
    2
    3
    4
    5
    6
    7
      for 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()}"))
  • gather的作用和wait类似不同的是:

    1. gather任务无法取消。

    2. 返回值是一个结果列表

    3. 按照传入协程的顺序,会顺序保存的对应协程的执行结果输出。

      实验输出可见: Python:asyncio.wait 和 asyncio.gather 的异同

    1
    2
    3
    4
    5
    6
    7
    8
       for 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个例子:

    1. 需要拿到封装好的Task,以便取消或者添加成功回调等
    2. 业务上需要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_futurefuture = 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_taskasyncio.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

因此,根据这种设计,可以将项目结构组织成两种

  1. 不依赖子模块的放在root目录的top接口:需要被其他文件引用的列入文件夹中作为库使用,不需要被引用的可以直接放顶层

    1
    2
    3
    4
    5
    # helper/runner.py
    # handler/ 在.下作为包导入
    from handler imoprt ConfigHandler
    # settings.py 在.下作为模块导入
    from settings import USER_AGENT

    topFile

  2. ★将入口放在最外层,核心内容作为单独一个文件夹(模块):由于feiyu是个整体的包,所以在feiyu下的任意Python文件中都可以通过from feiyu.xxx import yyy来导入。

oneEntry

根据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文件。

注:可以看到无论是pyinstallerpyi-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
    5
    def 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
    16
    class 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dict =defaultdict( factory_function)

dict1 = defaultdict(int)
dict2 = defaultdict(set)
dict3 = defaultdict(str)
dict4 = defaultdict(list)

for i in range(1, 5):
d = getattr(__import__("__main__"), f"dict{i}")
print(d["hello"])
"""
0
set()

[]
"""
# 弹出hello的v, 如果没有, 则返回[]
d.pop("hello", [])

其作用是在获得不存在的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
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
36
37
38
39
40
41
42
import selectors
import socket
# selectors模块默认会用epoll,如果你的系统中没有epoll(比如windows)则会自动使用select
sel = selectors.DefaultSelector() # 生成一个select对象


def accept(sock, mask):
# ②当有客户端进行sock.connect的时候会进入accept处理函数, 然后通过accept()获得连接信息
conn, addr = sock.accept() # Should be ready
print('accepted', conn, 'from', addr)
conn.setblocking(False) # 设定非阻塞
# ③将对于conn的读操作注册到时间循环中,绑定read函数为槽函数
sel.register(conn, selectors.EVENT_READ, read) # 新连接注册read回调函数


def read(conn, mask):
# ④当conn进行读操作时, 会进入read()函数,对其recv进行处理
data = conn.recv(1024) # Should be ready
if data:
# ⑤如果有数据则说明在正常的通信, 则进行交互
print('echoing', repr(data), 'to', conn)
conn.send(data)
else:
# ⑤如果没有数据了, 则表示断开连接, 此时需要将conn的IO read事件卸载掉
print('closing', conn)
sel.unregister(conn)
conn.close()


sock = socket.socket()
sock.bind(('localhost', 8080))
sock.listen()
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept) # ①把刚生成的sock连接对象注册到select连接列表中,并交给accept函数处理

while True:
events = sel.select() # 默认是阻塞,有活动连接就返回活动的连接列表
# 这里看起来是select,其实有可能会使用epoll,如果你的系统支持epoll,那么默认就是epoll
for key, mask in events:
print("key, mask", key, mask)
callback = key.data # 去调accept函数
callback(key.fileobj, mask) # key.fileobj就是readable中的一个socket连接对象

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

socket

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.

< PreviousPost
开源项目代码阅读记录
NextPost >
JS逆向-webpack打包网站实战
CATALOG
  1. 1. xxxatrr家族
    1. 1.1. hasattr()
    2. 1.2. setattr()
    3. 1.3. getattr()
  2. 2. __get__,__getattr__,__getattribute__及区别
    1. 2.1. 1.object.__getattribute__(self, name)
    2. 2.2. 2.object.__getattr__(self, name)
    3. 2.3. 3.object.__get__(self, instance, owner)
    4. 2.4. __getitem__
  3. 3. __dict__
  4. 4. 可变参数与unpack
  5. 5. 装饰器
  6. 6. 懒加载属性
  7. 7. 由单例元类引发的知识点
    1. 7.1. 预置知识:type和object
    2. 7.2. __new__
    3. 7.3. MetaClass元类
    4. 7.4. type动态创建类
    5. 7.5. 通过元类创建单例类
  8. 8. __call__
  9. 9. 函数和模块的特殊属性__annotations__
    1. 9.1. 用途:修改dataclass构造
  10. 10. Dataclass(数据类)
    1. 10.1. 数据类的基石——dataclasses.field
      1. 10.1.1. dataclasses嵌套
  11. 11. 生成器、迭代器、容器
    1. 11.1. Python中的容器
    2. 11.2. 可迭代对象与Iterator(迭代器)
    3. 11.3. Generator(生成器)
      1. 11.3.1. 分析一个最简单的生成器:
  12. 12. yield和yield from
  13. 13. Python协程
    1. 13.1. 协程,线程和进程的区别
    2. 13.2. 网络请求速度对比
    3. 13.3. gevent和asyncio区别
    4. 13.4. 使用uvloop加速
    5. 13.5. API说明与对比
      1. 13.5.1. wait和gather
      2. 13.5.2. ensure_future和create_task
    6. 13.6. 异步库
  14. 14. 项目结构
  15. 15. 深入理解__file__
  16. 16. 冻结二进制——Pyinstaller
  17. 17. functools.partial用法!
  18. 18. 拓展容器
  19. 19. IO多路复用
    1. 19.1. API