Python从进阶到高档—浅显易懂版

# ==================================
# Author : Mikigo
# Time   : 2021/12/23
# ==================================

一、简介

Python 进阶是我一向很想写的,作为自己学习的记载,曩昔自己在看一些代码的时分常常会困惑,看不懂,然后自己去查材料、看书本,渐渐的一个个弄懂,常常沉浸其间。关于 Python 高档语法的材料、书本不少,具体是具体,可是总感觉写的太杂乱,学习有难度,而且不能使人形象深刻。

TLDR” 是流行的互联网行话,意思是“太长不读( to long didn’t read )”。其实许多内容的核心常识就那么一点,细枝末节的东西蛮多,描绘词句也很官方很晦涩,很难读懂,给人感觉便是每个字我都知道,怎样放到一同就不知道了。浅显易懂版便是想用一种比较轻松、简略的办法阐明其间的要点且常用的内容,在写作的过程中我也常常劝诫自己要克制,别整杂乱了。

Python 是一门很简略入门的言语,可是要进阶其实需求花费大量的时刻和精力,而且还需求不断的操练运用,或许你现已花了两个月时刻学习了 Python 根底并能够写一些小脚本,或许你现已达到必定高度能独立编写大型项目,可是学习永无止境,咱们都还有许多需求学习提高的当地。以下内容绝大部分都是我在项目中用过的,许多描绘是我自己的了解,或许会和官方有必定出入,可是信任大差不差,也欢迎有心人不吝赐教。

内容还会持续增加,包括一些简略好用的标准库、三方库都会持续加进来,期望看到的同学能够多多提意见。

二、类和目标

1、鸭子类型

“当看到一只鸟走起来像鸭子、游水起来像鸭子、叫起来也像鸭子,那么这只鸟就能够被称为鸭子。” 这是百科上对它的解说。

鸭子类型(duck typing)是动态类型的一种风格,鸭子类型关于 Python 编码来讲十分重要,了解它能让你真实了解什么是全部皆目标,更有助于咱们了解这门言语的规划思想和完成原理,而不是仅仅浮于表面的念经 “全部皆目标”。

鸭子类型始终贯穿于 Python 代码当中,一个目标它是什么类型取决于它完成了什么协议,因而能够说 Python 是一种依据协议的编程言语。

那这些协议是什么,又有哪些协议?这儿的协议,更多的时分咱们称为魔法函数或魔法办法,由于它具有许多奇特的法力,坊间因而称之为魔法函数。

在 Python 里边,一切以双下划线最初,且以双下划线结束的函数都是魔法函数,就像 __init__ 这种,它们是 Python 言语天然自带的,不是经过某个类去承继而来的,咱们也不要随意去自界说一个这样的函数,当心着魔。

魔法函数有许多,可是常常用到的也没多少,常用的一些魔法函数在后边的内容会逐渐介绍到。

2、类型判别

在判别数据类型的时分常见的有两种办法:isinstancetype

isinstance("123", str)  # 回来布尔值
type("123")  # 直接回来类型

isinstance 首要用于判别目标的类型。这个好了解,不多讲。

type 能够检查类型,但它能做的远不止于此,它首要用于动态的创立类。

t = type("Mikigo", (), {"name": "huangmingqiang"})
T = t()
print(t)
print(T.name)
print(type(t))
<class '__main__.Mikigo'>
huangmingqiang
<class 'type'>

你看,咱们界说了一个类并赋值给 t,类名为 Mikigo,t 是类目标的引证,name 是其间的特色,Python 中全部都是目标,类也是目标,只不过是一种特殊的目标,是 type 的目标。

这个当地有点绕哈,你细品。

我看到网上许多讲 type 函数,准确讲 type 是一个类,仅仅用法像函数。在源码中:(经过 Pycharm 按住 Ctrl 点击进入)

class type(object):
    def __init__(cls, what, bases=None, dict=None): # known special case of type.__init__
        """
        type(object_or_name, bases, dict)
        type(object) -> the object's type
        type(name, bases, dict) -> a new type
        # (copied from class doc)
        """
        pass

有同学要问了,为什么源码里边有 pass,你没看错,源码里边便是写的 pass,这种实际上是由于底层是由 C 言语完成的(本文内容都是依据 CPython),一般的操作是看不到源码的,之所以能看到是由于 Pycharm 给咱们供给的功用(其他编辑器不知道哈,没咋用过其他的),相当于以代码的形式看文档,所以咱们看到的不是真实的源码,可是最接近于源码的源码,姑且称之为源码吧。

type 的参数阐明:

  • 当 type() 只需一个参数时,其效果便是回来变量或目标的类型。

  • 当 type() 有三个参数时,其效果便是创立类目标:

    • 参数 1:what 表明类称号,字符串类型;
    • 参数 2:bases 表明承继目标(父类),元组类型,单元素运用逗号;
    • 参数 3:dict 表明特色,这儿能够填写类特色、类办法、静态办法,选用字典格式,key 为特色名,value 为特色值。
    @staticmethod
    def my_static():
        print("this is static")
    t = type("Mikigo", (), {"name": "huangmingqiang", "static": my_static})
    T = t()
    t.static()
    T.static()
    
    this is static
    this is static
    

    这样就增加了一个静态办法,很清楚哈,关于静态办法是什么咱们后边会讲到,这儿只需求知道 type 创立类的办法就好了。

经过上面 type 的源码能够看到,type 是承继了 object 的,咱们知道一切类的顶层类都是承继的 object,那 object 又是从哪里来的?打印看一下:

print(type(object))
<class 'type'>

好家伙,object 也是由 type 创立的,前面说了 type 承继了 object,这俩哥们儿完美闭环了,我直接好家伙,了解起来有点更绕了哈。

你也能够说 type 自己创立了自己,这儿要细细的品。实际上假设你了解指针的概念,这儿其实也不难了解,不便是自己指向自己嘛,所以说 type 创立了一切类,由于他连他自己都不放过,还有什么工作做不出来。

3、类变量和实例变量

(1)类变量是在类里边直接界说的变量,它能够被类目标拜访和赋值,也能够被实例目标拜访和赋值。

class Test:
    b = 1
    def __init__(self):  # 结构函数
        self.a = 1
T = Test()
print(T.b)
print(Test.b)
T.b = 2  # 经过实例目标赋值
print(T.b)
Test.b = 2 # 经过类目标赋值
print(Test.b)
1
1
2
2

b 是类变量,都能被拜访和赋值,没问题哈。

(2)实例变量是在结构函数里边界说的变量,它能够被实例目标拜访和赋值,不能被类目标拜访和赋值。

class Test:
    b = 1
    def __init__(self):
        self.a = 1
T = Test()
print(T.a)
T.a = 2
print(T.a)
print(Test.a)
Test.a = 2
print(Test.a)
1
2
Traceback (most recent call last):
  File "/tmp/pycharm_project_16/123.py", line 12, in <module>
    print(Test.a)
AttributeError: type object 'Test' has no attribute 'a'

a 是实例变量,你看实例目标拜访和赋值正常的,类目标拜访就报错了。

4、类办法、静态办法和实例办法

(1)实例办法又称目标办法,是类中最常见的一种办法。

class Test:
    def obj_method(self):
        print("this is obj method")

实例办法参数有必要传入 selfself 表明实例目标自身,实例办法的调用也有必要经过实例目标来调用:

Test().obj_method()

(2)类办法

class Test:
    @classmethod
    def cls_method(cls):
        print("this is class method")

能够经过类目标调用,也能够经过实例目标调用。

Test.cls_method()
Test().cls_method()

留意两点:

  • 办法前面有必要加装修器 classmethod ,装修器是 Python 中的一种语法糖,后边会讲到,记住这种固定用法,这种写法也是初代装修器的用法。
  • 参数传入 clscls 表明类目标,可是留意不是有必要的写法,写 cls 是一种约定俗成的写法,便当咱们了解,也便是说这儿你写 self 从语法上也是不会有问题的。这便是为什么有时分咱们将一个实例办法改成类办法,直接在办法前面增加了装修器,而没有改 self,依然能正常履行的原因。

(3)静态办法,实际上便是一般的函数,和这个类没有任何联系,它仅仅进入类的称号空间。

class Test:
    @staticmethod
    def static_method():
        print("this is static method")

不需求传入任何参数。相同,能够经过类目标调用,也能够经过实例目标调用。

Test.static_method()
Test().static_method()

我看到一些社区大佬都表现出对静态办法的嫌弃,他们觉得已然静态办法和类没有联系,何不如在类外面写,直接写在模块里边岂不快哉。咱们不予评价,存在即合理。

5、类和实例特色的查找次序

这儿需求引进一个概念:MRO(Method Resolution Order),直译过来便是“办法查找次序”。

咱们知道类是能够承继的,子类承继了父类,子类就能够调用父类的特色和办法,那么在多承继的情况下,子类在调用父类办法时的逻辑时怎样的呢,假设多个父类中存在相同的办法,调用逻辑又是怎样的呢,这便是 MRO

Python2.3 之前的一些查找算法,比方:深度优先(deep first search)、广度优化等,关于一些菱形承继的问题都不能很好的处理。这部分内容比较多且杂,能够自己查阅材料。

Python2.3 之后,办法的查找算法都统一为叫 C3 的查找算法,晋级之后的算法更加杂乱,选用的特技版拓扑排序,这儿也不细讲,能够自己查阅材料,咱们只需求关怀现在办法查找次序是怎样的就行了。

来,这儿举例阐明:

class A:
    pass
class B:
    pass
class C(A, B):
    pass
print(C.__mro__)

__mro__ 能够检查办法的查找次序。

(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)

能够看到,关于 C 来讲,它里边的办法查找次序是 C — A — B,没缺点哈,很清楚。

现在晋级一下承继联系,试试菱形承继:

class A:
    pass
class B(A):
    pass
class C(A):
    pass
class D(B, C):
    pass
print(D.__mro__)
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

D 的查找次序是 D — B — C — A

阐明什么问题?我在这噼里啪啦说了这么多,究竟想说啥?

幻想一下,假设你在 B 和 C 里边都重载了 A 里边的一个办法,此刻假设你想调用的是 C 里边的办法,实际上是无法调用的,由于依据办法的查找次序,会先找到 B 里边的办法。

因而,要点来了:在 Python 中虽然是支持多承继的,可是在实际项目中不主张运用多承继,由于假设承继联系规划得欠好,很简略形成逻辑联系的紊乱,原因便是 MRO

Ruby 之父在《松本行弘的程序世界》书中,讲到三点多承继的问题:

  • 结构杂乱化:假设是单一承继,一个类的父类是什么,父类的父类是什么,都很清晰,由于只需单一的承继联系,可是假设是多重承继的话,一个类有多个父类,这些父类又有自己的父类,那么类之间的联系就很杂乱了。
  • 优先次序模糊:假设我有A,C类一起承继了基类,B类承继了A类,然后D类又一起承继了B和C类,所以D类承继父类的办法的次序应该是D、B、A、C仍是D、B、C、A,或许是其他的次序,很不清晰。
  • 功用冲突:由于多重承继有多个父类,所以当不同的父类中有相同的办法是就会产生冲突。假设B类和C类一起又有相同的办法时,D承继的是哪个办法就不清晰了,由于存在两种或许性。

看看这是大佬说的,不是我说的。

那有同学要问了,我写的功用很杂乱啊,有必要要承继多个类,怎样办,难受!

实际上有一种比较流行且先进的规划形式:Mixin 混合形式,完美处理这个问题。

举个简略的比如:

class Animal:
    pass
# 大类
class Mammal(Animal):
    pass
# 各种动物
class Dog(Mammal):
    pass
class Bat(Mammal):
    pass

现在动物们没有任何技术,咱们需求给动物们增加一下技术:

class RunnableMixIn:
    def run(self):
        print('Running...')
class FlyableMixIn:
    def fly(self):
        print('Flying...')

留意 Mixin 的类功用是独立的,命名上也应该运用 MixIn 结束,这是一种标准。

需求 Run 技术的动物:

class Dog(Mammal, RunnableMixIn):
    pass

需求 Fly 技术的动物:

class Bat(Mammal, FlyableMixIn):
    pass

有点感觉了没,Mixin 类的特色:

  • 功用独立、单一;
  • 只用于拓展子类的功用,不能影响子类的首要功用,子类也不能依赖 Mixin
  • 自身不应该进行实例化,仅用于被子类承继。

Mixin 规划思想简略讲便是:不与任何类关联,可与任何类组合。

6、破解私有特色

私有特色便是在类的内部能拜访,外部不能拜访。

在 Python 中没有专门的句子进行私有化,而经过在特色或办法前面加“两个下划线”完成。

举例:

class Test:
    def __init__(self):
        self.__mi = "Mikigo" 
    def __ki(self):
        print("Mikigo")
    def go(self):
        print(self.__mi)
Test().go()
Mikigo

你看,在类的内部拜访私有特色是能够正常拿到的,办法也是相同的。

现在咱们拜访私有特色试试:

Test().__mi
Traceback (most recent call last):
  File "/tmp/pycharm_project_609/123.py", line 6, in <module>
    print(Test().__mi)
AttributeError: 'Test' object has no attribute '__mi'

从外部进行私有特色拜访是不可的,人家是私有的。

Test().__ki()
Traceback (most recent call last):
  File "/tmp/pycharm_project_609/123.py", line 9, in <module>
    Test().__ki()
AttributeError: 'Test' object has no attribute '__ki'

私有办法也无法拜访,没问题哈。

有同学要问了,我便是想拜访,越是私有的我越想看,怎样才能看到别人的隐私,快说!

泄露天机了哈,这是 Python 一种很奇妙的结构化处理,为什么说是结构化处理,实际上 Python 拿到双下划线之后,对其进行了变形,在前面加了一个下划线和类名,咱们经过这种办法能够拜访:

print(Test()._Test__mi)
Test()._Test__ki()
Mikigo
Mikigo

你看,这样就能够正常拜访了,可是已然作者不期望运用者调用这个办法,咱们也尽量不要去强行运用它,强扭的瓜不甜。

所以说,从言语的视点是没有肯定的安全,任何言语都是这样,更多的是一种编程上的束缚。

通常在大多数实践中,咱们更倾向于运用一个下划线来表明私有特色,这不是真实的私有,而是一种更友爱的编程标准,社区称之为 “受维护的”特色,它向运用着表达了这是一个私有的办法,可是你依然能够运用它,这便是社区,这便是开源,respect~。

7、目标的自省机制

自省(introspection),即自我检讨,而目标的自省实际上便是检查目标完成了哪些特色或办法。

简略讲便是,告诉别人:我是谁,我能干啥

Python 的常用的自省函数有四个:dir()、type()、 hasattr()、isinstance()

(1)isinstance() 和 type() 前面也说到过,这儿不讲了。

(2)dir() 是最为常用的一个自省函数:

引证前面的 Test 类

print(dir(Test))
['_Test__ki', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'go']

除了 _Test__kigo 办法以外,其他的办法都是魔法函数,即最开端咱们说到的协议,你看随便一个目标就完成了这么多协议,是不是很奇特。

(3)hasattr() 首要用于判别目标中是否包括某个特色,回来布尔值。

print(hasattr(Test, "go"))
print(hasattr(Test, "wo"))
True
False

很简略,不多讲哈。

其他还有一些自省函数能够了解一下,偶尔用到也挺好的:

  • __doc__ 获取到文档字符串;
  • __name__ 获取目标的称号;
  • __dict__ 包括了类里可用的特色名-特色的字典;
  • __bases__ 回来父类目标的元组;但不包括承继树更上层的其他类。

8、super

super 函数是用于调用父类的一个办法。

class A:
    def mi(self):
        print("=== mi ===")
class B(A):
    def ki(self):
        super().mi()
B().ki()
=== mi ===

super 的运用办法是很简略的,可是假设涉及到多承继的情况下,就要当心处理。

准确的讲它不是调用父类的办法,而是调用的 MRO 次序上的下一个办法。

9、上下文管理器

在讲到上下文管理器的时分,常常有同学一脸懵,然后我说 with 的时分,就会信口开河 with open

没错,with 句子用得最多的也是这个,它是 Python 供给的一种处理资源收回的奇特办法,假设没有 with 咱们或许需求多写许多代码。

咱们都知道翻开一个文件之后是需求封闭的,可是在操作文件的过程中很简略报错,这时分咱们需求进行反常处理,要确保无论是否存在反常的情况下,文件都能正常的被封闭,咱们几乎只能运用try里边的finally来处理:

f = open("test.txt", "w")
try:
    f.write(some_txt)
except:
    pass
finally:
    f.close()

假设用 with 句子处理就会很简略:

with open("test.txt", "w") as f:
    f.write(some_txt)

比照起来,哪个更好不必多说,自己品。

在《流畅的 Python》这本书里边说到:

在任何情况下,包括CPython,最好显式封闭文件;而封闭文件的最牢靠办法是运用with句子,它能确保文件必定会被封闭,即使翻开文件时抛出了反常也无妨。

那咱们怎样完成一个上下文管理器呢?

  • 依据类完成上下文管理器

要完成上下文管理器,需求完成两个魔法函数:__enter____exit__

看称号就知道了,enter 便是进入的时分要做的工作,exit 便是退出的时分要做的工作,很好记有没有。

class Context:
    def __init__(self, file_name):
        self.file_name = file_name
        self.f = None
    def __enter__(self):
        print("进入 with")
        self.f = open(self.file_name, "r")
        return self.f
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("退出 with")
        if self.f:
            self.f.close()

然后咱们就能够运用 with 句子

with Context("test.txt") as f:
    print(f.read())
进入 with
我是一个测验文件
退出 with

完美哈,一个上下文管理器的类就轻松搞定。

  • 依据 contextlib 完成上下文管理器

还有种经过标准库完成上下文管理器的办法:

from contextlib import contextmanager
@contextmanager
def context_test(file_name):
    print("进入 with")
    try:
        f = open(file_name, "r")
        yield f
    finally:
        print("退出 with")
        f.close()

来用 with 游玩一下

with context_test("test.txt") as f:
    print(f.read())
进入 with
我是一个测验文件
退出 with

运用生成器的原理,yield 之前是进入,yield 之后是退出,相同能够完成一个上下文管理器,稍微了解一下哈。

上下文管理器是 Python 供给给咱们的一个十分便当且有趣的功用,常常被用在翻开文件、数据库衔接、网络衔接、摄像头衔接等场景下。假设你常常做一些固定的开端和结束的动作,能够测验一下。

10、装修器

装修器便是运用 @ 符号,像帽子相同扣在函数的头上,是 Python 中的一种语法糖。

前面讲类办法和静态办法的时分说到过,运用办法十分简略。

原理实际上便是将它所装修的函数作为参数,最后回来这个函数。

@classmethod
def mikigo():
    print("My name is mikigo")

这样的写法等同于

def mikigo():
    print("My name is mikigo")
mikigo = classmethod(mikigo)

比照一下,运用装修器可读性很高,很高雅是吧,语法糖便是给你点糖吃,让你上瘾。

界说一个装修器

  • 不带参数的装修器

举个比如:

def logger(func):
    def wrapper(*args, **kw):
        print('我要开端搞 {} 函数了'.format(func.__name__))
        func(*args, **kw)  # 函数履行
        print('搞完了')
    return wrapper

这是一个简略的装修函数,用途便是在函数履行前后别离打印点日志。

有2点需求留意:

(1)装机器是一种高阶函数,在函数内层界说函数,并回来内层函数目标,多层级同理。

(2)最外层函数传入的参数是被装修函数的函数目标。

@logger
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))

来,试试看

add(5, 10)
我要开端搞 add 函数了
5 + 10 = 15
搞完了
  • 带参数的装修器
from functools import wraps
def logger(say_some):
    @wraps
    def wrapper(func):
        def deco(*args, **kw):
            print("搞之前我先说两句:{}".format(say_some))
            print('我要开端搞 {} 函数了:'.format(func.__name__))
            func(*args, **kw)  # 函数履行
            print('搞完了')
        return deco
    return wrapper

你看,都是外层函数回来内层函数目标,参数放在最外层。@wraps 可加可不加,它的用途首要是保留被装修函数的一些特色值。

@logger("别整,不得劲儿~")
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))

履行试试

add(5, 10)
搞之前我先说两句:别整,不得劲儿~
我要开端搞 add 函数了:
5 + 10 = 15
搞完了

很奈斯,就这点儿东西。

这是最常见的完成办法,现在咱们搞点不相同的。

依据类完成装修器

依据类装修器的完成,有必要完成 __call____init__ 两个魔法函数。

  • 不带参数的类装修器
class logger:
    def __init__(self, func):
        self.func = func
    def __call__(self, *args, **kwargs):
        print('我要开端搞 {} 函数了'.format(self.func.__name__))
        f = self.func(*args, **kwargs)
        print('搞完了')
        return f

不带参数的类装修,func 是经过 init 函数里边结构的。

试试看

@logger
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))
add(5, 10)
我要开端搞 add 函数了
5 + 10 = 15
搞完了

so easy 哈,鸭子类型,完成了装修器协议,便是装修器目标。

  • 带参数的类装修器
class logger:
    def __init__(self, say_some):
        self.say_some = say_some
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print("搞之前我先说两句:{}".format(self.say_some))
            print('我要开端搞 {} 函数了'.format(func.__name__))
            func(*args, **kwargs)
            print('搞完了')
        return wrapper

带参数的类装修器,func 是在 call 函数里边,参数是经过 init函数传入的,这儿差异比较大哈。

@logger("别整,真的不得劲儿~")
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))
add(5, 10)
搞之前我先说两句:别整,真的不得劲儿~
我要开端搞 add 函数了
5 + 10 = 15
搞完了

这类属于装修器的高阶用法了,在一些优秀的结构源码里边比较常见。

三、自界说序列

1、可切片目标

切片咱们都很了解,在 Python 根底里边是必学的,对列表运用中括号取值,正切、反切、加步长等都没问题,这儿咱们首要讲怎样完成一个可切片目标。

隆重请出魔法函数:__getitem__ ,它是咱们完成可切片目标的要害。

class AutoTest:
    def __init__(self, name_list):
        self.name_list = name_list
    def __getitem__(self, item):
        return self.name_list[item]
AT = AutoTest(["mikigo", "lt", "jjb", "hhz"])

咱们对实例目标切片试试

print(AT[1])
print(AT[2])
lt
jjb

几乎没难度哈,目标能够切片了。

这儿再弥补一点没有用的小常识,完成了 __getitem__ 办法实际上也是一个可迭代的目标了,也便是说能够运用 for 循环。

for i in AT:
    print(i)
mikigo
lt
jjb
hhz

这其实是可迭代目标的一种退而求其次的处理,它找不到迭代协议,可是找到了 __getitem__ ,也能够进行迭代,这点信任 99% 的同学都不知道,没联系哈,关于可迭代目标和迭代器咱们后边会专门讲。

2、列表推导式

列表推导是 Python 供给的一种独有特性,能够用一行代码生成一个列表。

一般操作:

my_list = []
for i in range(10):
    my_list.append(i)

这样生成一个列表,至少需求3行,来看看列表推导式:

my_list = [i for i in rang(10)]

一行就搞定,多么的简练高雅,而且可读性和功能都十分高,爱了。

还能够加一些逻辑判别和数据处理,以下是项目实例:

app_id_list = [int(_id) for _id in app_id if _id]  # to int

这儿要提醒一下,不要为了推导而推导,假设你的逻辑很杂乱,加了多重判别和处理,不主张运用推导式,老老实实分开写,由于这样写出来的表达式会很杂乱,就失去了咱们编码最重要的一点,便是可读性。

3、生成器表达式

前面讲了列表推导式,是用中括号里边写表达式,那把中括号换成小括号是什么呢?许多同学聪明的小脑袋肯定想到了,元组推导式 … 。

留意元组是不可变序列,没法推导的,小括号的表达式实际上是生成器表达式。

my_gen = (i for i in range(10))

验证一下:

from collections.abc import Generator
print(isinstance(my_gen, Generator))
print(my_gen)
True
<generator object <genexpr> at 0x7f5676c57390>

你看,的确是一个生成器吧。生成器细节,咱们也放到后边讲哈。

4、字典推导式

了解了列表推导式,再来看字典推导式就很简略了。

my_dict = {i: None for i in range(10)}

第一个元素便是字典的 key 和 value,留意字典的key 是唯一的(可哈希),值无所谓。

打印看下

print(my_dict)
{0: None, 1: None, 2: None, 3: None, 4: None, 5: None, 6: None, 7: None, 8: None, 9: None}

就这,几乎没难度,仍是要留意一点,代码可读性哈,别整杂乱了。

tag_dict = {f"{int(i[0]):0>3}": i[1:] for i in ReadCsv.read_csv_by_str(csv_dict.get(app), from_data=False)}

这是主动化测验项目中的一个实例,感触下,假设再杂乱点就主张拆开写了。

四、目标引证

1、变量究竟是什么

在 Python 中变量究竟是什么,有一个比方我觉得十分好,变量就像便当贴。

为什么这么讲,咱们界说一个数据,比方界说一个字符串或许整数,在内存中都会分配一个空间来保存,这个内存空间相当于一个小盒子,咱们运用等号将这个数据赋值给一个变量时,实际上就像用便当贴贴到这个小盒子上,便当贴上还写了称号,便是变量名。所以说,变量和数据的联系仅仅一个指向的联系。

一个数据能够赋值给多个变量,相当于这个小盒子上面贴了多个便当贴;一个变量也能够被重新赋值,相当于把这个盒子上的便当贴撕了,贴到另一个盒子上。

变量和数据的联系,便是盒子和便当贴的联系,了解起来很简略。

函数名也是变量,是能够传参的变量,也相同是便当贴。

2、== 和 is 是相同的吗

这两个在编程中常常用到,许多同学常常搞不清楚应该用哪个。

  • == 是比较两头的“值”是否相等;
  • is 是判别是否为同一个目标,即 id 是否相同。
a = 1000
b = 1000
print(a == b)
print(a is b)
print(id(a), id(b))
True
True
140689217239312 140689217239312

这儿有个很奇特的当地,别离界说了两个变量a, b,他们的值相等,可是这样界说应该是分配了2个内存空间,更有意思的是,假设你经过命令行履行以上代码,成果会不相同:

有这个符号的 >>> 表明是在命令行履行。

>>> a = 1000
>>> b = 1000
>>> print(a == b)
True
>>> print(a is b)
False
>>> print(id(a), id(b))
140601647494256 140601647494448

上面是运用 Pycharm 履行的,实际上Python解说器现已对常常运用到的小整数做了特殊处理,解说器会提早将 256 以内的整数恳求内存空间,不会收回,以提高履行效率,所以在这个范围内的整数 id 永远是相同的。

>>> a = 256
>>> b = 256
>>> print(id(a), id(b))
9095360 9095360
>>> a = 257
>>> b = 257
>>> print(id(a), id(b))
140601647494512 140601647494384

Pycharm 在解说器的根底之上做了进一步的优化。

a = 1000000
b = 1000000
print(id(a), id(b))
140061167311120 140061167311120

你看,这么大的数字 id 也是相同的,Pycharm 便是这么酷。

3、del句子和废物收回

在 Python 中的废物收回机制是:引证计数(Reference Counting)。

简略讲便是每个目标内部有一个引证计数器,目标被创立或许被引证就会 +1,目标被毁掉或许被赋予新的目标就会 -1

del 句子是效果在变量上,不是数据目标上。

a = 1
b = a
del a

打印 b 看下

print(b)
1

再打印 a 看下

print(a)
NameError: name 'a' is not defined

很显着,a 被删掉了。

之前看到国外的一个大佬讲 open 的这种写法不必封闭:

open("test.txt", "r").read()

很有意思是吧,这点没有用的小常识,信任你在网上应该查不到。当时觉得不太了解,后边了解废物收回之后才明白,运用 open 翻开的文件目标创立之后,没有被其他引证,所以会被内存收回的,因而不必封闭也不影响。

邪门歪道哈,用 open 仍是老老实实用 with 吧。

五、元类编程

1、动态特色和特色描绘符

有些同学或许知道 @property ,它的首要用于将一个办法变成特色,拜访的时分直接经过称号拜访,不需求加括号。留意加了 @property 函数不能有参数,你想嘛,人家调用的时分都不必括号,怎样传参,对吧。

举个小比如:

class Mikigo:
    @property
    def age(self):
        return "我晕,本年30了"
print(Mikigo().age)
我晕,本年30了

你看,调用 age 办法没加括号吧,那我要修正 age 的值怎样做呢?

class Mikigo:
    def __init__(self):
        self._age = 30
    @property
    def age(self):
        return self._age
    @age.setter
    def age(self, value):
        if not isinstance(value, int):
            raise ValueError
        self._age = value
mi = Mikigo()
mi.age = 25
print(mi.age)
25

留意上例中装修器的写法,setter 是固定写法,setter 前面是你界说的函数名。

没什么问题哈,做了参数的类型检查,整体看起来不算杂乱,其实了解到这儿现已差不多了。可是,假设咱们还有其他特色要处理,就得写许多个这样的,挺费力不说,要害是不可高雅。

这时分就需求请出特色描绘符。

这儿又要介绍两个魔法函数:__get____set__

举个比如,解说其用法:

class UserAttr:
    def __init__(self, user_age):
        self._age = user_age
    def __get__(self, instance, owner):
        print("get_instance:", instance)
        print("get_owner:", owner)
        return self._age
    def __set__(self, instance, value):
        print("set_instance:", instance)
        print("gse_value:", value)
        if not isinstance(value, int):
            raise ValueError
        self._age = value

真实运用的类:

class Mikigo:
    age = UserAttr(30)
mi = Mikigo()
print(mi.age)
get_instance: <__main__.Mikigo object at 0x7fb4eff50e10>
get_owner: <class '__main__.Mikigo'>
30

在目标拜访 age 的时分,首要是进入了 __get__ 办法,由于先打印了 get_instance 和 get_owner,instance 是 Mikigo 实例目标,也便是 mi,owner 是 Mikigo 类目标。

因而,到这儿,咱们知道了第一个小常识,在拜访值的时分,调用的是 __get__

再赋值看看:

mi.age = 25
print(mi.age)
set_instance: <__main__.Mikigo object at 0x7fc7be222470>
set_value: 25
get_instance: <__main__.Mikigo object at 0x7fc7be222470>
get_owner: <class '__main__.Mikigo'>
25

第二个小常识,赋值是调用的 __set__ 办法,一般为了使特色描绘符成为只读的,应该一起界说 __get__()__set__() ,并在 __set__() 中引发 AttributeError

还有一个魔法函数 __delete__ 也是特色描绘符,运用 del 会调用,由于不咋运用,不讲了,还有网上许多差异数据描绘符和非数据描绘符的,我感觉不必管也没必要,咱们是浅显易懂版,不整那些。

2、特色阻拦器

特色阻拦器便是在拜访目标的特色时要做的一些工作,你想嘛,阻拦便是拦路抢劫,阻拦下来肯定要搞点工作才放你走。

首要介绍 2 个魔法函数:__getattr____getattribute__

这两个函数特别奇特,两个函数功用相反,一个是找到特色要做的事,另一个是没找到特色要做的事。

class Mikigo:
    def __init__(self):
        self.age = 30
    def __getattribute__(self, item):
        print(f"找到{item},我先搞点工作")
    def __getattr__(self, item):
        print(f"没找到{item},我想想能搞点啥工作")

界说了一个特色 age ,先来试试拜访它

mi = Mikigo()
print(mi.age)
找到age,我先搞点工作
30

找到特色,会先调用 __getattribute__ ,并没有调用 __getattr__

好,现在拜访一个不存在的特色:

mi.name
找到name,我先搞点工作
没找到name,我想想能搞点啥工作

这儿就需求留意了,拜访一个不存在的特色,首要仍是会进入 __getattribute__ ,阐明它是无条件进入的,然后才是调用 __getattr__

再扩展一个 __setattr__ 用于修正特色值的:

class Mikigo:
    def __init__(self):
        self.age = 30
    def __setattr__(self, key, value):
        print(f"修正{key}的值为{value}")
        self.__dict__[key] = value
mi = Mikigo()
mi.age = 25
print(mi.age)
修正age的值为30
修正age的值为25
25

你看,age 的值被修正了,可是 __setattr__ 貌似被调用了 2 次,那是由于在类实例化的时分就会进入一次,第一次是将 __init__ 里边的值增加到类实例的 __dict__ 特色中,第二次修正再次进入,将 __dict__ 特色中的值修正掉。

特色阻拦必定要谨慎运用,一般情况下不主张运用,由于假设处理欠好,会形成类里边特色联系的紊乱,抛反常往往不简略定位。

项目实例,config 文件里边用到:

class Config:
    default = {
        # for cases
        "SMB_URL": "SMB://10.8.10.214",
        "SMB_IP": "10.8.10.214",
    }
    def __getattr__(self, key):
        try:
            return Config.default[key]
        except KeyError:
            raise AttributeError(f"{key} is not a valid option!") from KeyError
    def __setattr__(self, key, value):
        if key not in Config.default:
            raise AttributeError(f"{key} is not a valid option!") from KeyError
        Config.default[key] = value

试着剖析下他们的效果吧,逻辑很简略的,你必定能看懂。

3、自界说元类

元类(metaclass)便是生成类的类,先界说metaclass,就能够创立类,最后创立实例。

其实最开端讲 type 的时分现已有所触摸了,type 生成了一切类,它便是顶层元类,metaclass 也是要承继 type的,排行顶多老二,是不是应该叫“元二类”,或许“元类二”,爱谁谁吧。

来,咱们界说一个元类,用途是增加一个特色 age :

class AutoTestMetaClass(type):
    def __new__(cls, name, bases, dct):
        x = super().__new__(cls, name, bases, dct)
        x.age = 30
        return x

这儿有 2 个常识点:

  • __new__ 也是结构函数,和 __init__ 有差异,__new__ 是用来结构类目标的,你看它的参数是 cls,有必要 return 一个目标。
  • name, bases, dct 这三个参数和 type 的三个参数是一个意思,不清楚能够回看前面讲 type 的章节。

元类有了,咱们运用一下,已然元类是用来生成类的类,那咱们就来生成一个类:

class Mikigo(metaclass=AutoTestMetaClass):
    ...
mi = Mikigo()
print(mi.age)
print(Mikigo.age)
30
30

咱们界说一个类除了省略号没有任何特色,省略号也是一个目标,你也能够用 pass,可是依然能够拜访 age 特色。由于咱们是经过元类,向 Mikigo 这个类增加了一个特色,元类有时称为类工厂。

六、迭代器和生成器

1、迭代协议

迭代便是能够运用循环将数据挨个挨个取出来,这个好了解是吧,比方,咱们常见的对一个列表进行迭代:

for i in [1, 2, 3]:
    print(i)

成果不必讲肯定是挨着取出列表里边的数字了。

那列表里边究竟是完成了什么协议,或许说一个目标完成什么魔法函数就能够迭代呢,这便是迭代协议:__iter__

一个类只需完成了魔法函数 __iter__ 便是可迭代的(Iterable),可是它还不是迭代器(Iterator),品一下差异。

class IterTest:
    def __iter__(self):
        ...

来验证一下:

from collections.abc import Iterable
from collections.abc import Iterator
print("是否可迭代:", isinstance(IterTest(), Iterable))
print("是否为迭代器:", isinstance(IterTest(), Iterator))
是否可迭代:True
是否为迭代器:False

你看完成了迭代协议,便是可迭代的,想起鸭子类型了吗。

2、迭代器和可迭代目标

咱们现在知道一个目标只需完成了 __iter__ 便是一个可迭代的目标,现在咱们来试试对一个可迭代目标运用 for 循环进行迭代,放个简略的列表进去看看:

class IterTest:
    def __iter__(self):
        return [1, 2, 3]
for i in IterTest():
    print(i)

__iter__ 函数里边回来一个列表,列表是一个可迭代的目标,但不是迭代器。

Traceback (most recent call last):
  File "/tmp/pycharm_project_609/123.py", line 11, in <module>
    for i in IterTest():
TypeError: iter() returned non-iterator of type 'list'

运转报错了,说 iter 回来了一个不是迭代器的目标。阐明在 __iter__ 里边需求回来一个迭代器,对吧,其他的先不论,咱们放一个迭代器进去,确保程序跑起来不报错。

放一个生成器表达式进去试试:

class IterTest:
    def __iter__(self):
        return (i for i in range(3))
for i in IterTest():
    print(i)
0
1
2

唉,这下对了,没报错,而且也能迭代出来了。

可是,此刻依然还不是一个迭代器,要完成迭代器,还有必要要完成别的一个魔法函数:__next__

class IterTest:
    def __iter__(self):
        return (i for i in range(3))
    def __next__(self):
        ...

验证一下

print("是否为迭代器:", isinstance(IterTest(), Iterator))
是否为迭代器: True

你看,完成 __next__ 之后,便是一个迭代器了。那 __next__ 应该怎样写,前面咱们现已看到, __iter__ 里边是不担任逻辑处理的,它只管回来,逻辑处理需求在 __next__ 里边去做。

运用经典的斐波那契数列来举例:

class Fib:
    def __init__(self, n):
        self.a, self.b = 0, 1
        self.n = n
    # 回来迭代器目标自身
    def __iter__(self):
        return self
    # 回来容器下一个元素
    def __next__(self):
        if self.n > 0:
            self.a, self.b = self.b, self.a + self.b
            self.n -= 1
            return self.a
        else:
            raise StopIteration

这儿边 n 是用来限制迭代次数的,否则这个循环将一向进行下去,直到宇宙的止境,抛 StopIteration 反常会被 for 循环主动处理掉。

for i in Fib(10):
    print(i)
1
2
3
5
8
13
21
34
55

这样咱们就完成了一个简略的迭代器。

简略一句话总结一下:迭代器便是使目标能够进行 for 循环,它需求完成 __iter____next__ 两个魔法函数。

有同学要说了,就这?不就用 for 循环嘛,搞这么杂乱嘎哈,我为什么要用迭代器啊?

为什么要运用迭代器

节约资源消耗,迭代器并不会核算每一项的值,它只在你拜访这些项的时分才核算,也便是说它保存的是一种核算办法,而不是核算的成果。能了解吗,相当于迭代器是鱼竿,而不是一池子的鱼,需求鱼的时分钓就行了,而不必把一切鱼都搬回家。

平常或许感触不到哈,当你需求核算一个十分大的数据时,你就能感触到了,这便是“慵懒求值”的魅力。

你能够试试前面的斐波那契数列的列子,比照一个一般的列表,然后给一个很大的数字,差异就很显着了。

3、生成器

生成器也是一种迭代器,特殊的迭代器,它也能够用 for 循环来取值,可是大部分的情况下是运用 next() 函数进行取值。

前面咱们讲生成器表达式现已见识过,这是一种便携的写生成器的办法:

my_gen = (i for i in range(10))
print(next(my_gen))
print(next(my_gen))
print(next(my_gen))
print(next(my_gen))
0
1
2
3

一般这么玩的哈。

前面讲的许多目标都是在类里边界说的,而生成器目标就不是在类里边了,而是在函数里边界说,在一个函数里边只需出现了 yield 它就不是一般函数,而是一个生成器。

def my_gen():
    print("setp 1")
    yield 1
    print("setp 2")
    yield 2
g = my_gen()
next(g)
next(g)
step 1
step 2

yield 的用途是让函数暂停,并保存目标状态在内存中,下次再运用 next 调用同一个目标时,又开端从之前暂停的方位开端履行,直到运转到下一个 yield 又暂停,假设后边没有 yield了,则会抛 StopIteration 反常。

yieldreturn 都能回来数据,可是有差异,return 句子之后的代码是不履行的,而 yield 后边还能够履行。

有同学要问了,生成器函数里边能用 return 吗?好问题,不愧是你。

生成器里边是能够用 return 的,可是,return 后边的数据不会被回来。

举例:

def my_gen():
    yield 1
    yield 2
    return 3
for i in my_gen():
    print(i)
1
2

你看,3 并没有被回来,所以说生成器里边的 return 仅仅一个结束的标志,它不会把后边的值回来给调用者,这跟函数里边的 return 是不相同的。

4、总结

看完前面迭代器和生成器的内容,或许有些同学有点晕了,没联系,多看几遍,常常看,常常晕。

咱们简略总结一下:

  • 迭代器需求完成两个魔法函数:__iter____next__

  • 迭代器允许慵懒求值,只需在恳求下一个元素时迭代器目标才会去生成它,它保存的是一种生成数据的办法;

  • 生成器是迭代器的一种更 Pythonic 的写法,能够在函数里边用 yield 创立一个迭代器;

  • 生成器表达式是生成器的一种更加 Pythonic 的写法。

七、高阶函数

高阶函数是经过组合简略函数成一个杂乱表达式的函数。你能够了解成,函数套函数。函数式编程是一种编程范式,这部分内容能够体现 Python 在函数式编程上的应用。

1、lambda

匿名函数(lambda),这个函数没有函数名,用于一行创立一个函数,并回来一个函数目标,也是一种语法糖。

界说一个匿名函数,功用便是参数加1:

my_lb = lambda x: x + 1

一般函数的写法便是:

def add_one(n):
    return n + 1

你看,的确很简练哈,my_lb 不是函数名哈,有函数名它就不是匿名函数了,而是函数目标,咱能够调用它。

print(my_lb(1))

我个人觉得,匿名函数很为难,基本上都是用在下面几个高阶函数里边的,假设你平常也想用它,大多数情况下是不符合社区标准的。简略的表达式还行,杂乱的表达式可读性太差。

传言 Python 之父 Guido 也不引荐运用它,甚至曾想过移除它,后来放弃了,估量是欠好搞。就像 GIL 相同,咱们都知道欠好,可是这么多年下来太多库都用到了,哪是你想删就能删的,社区不答应,我也不答应。

2、map

map 函数是给一个序列做映射,然后回来成果序列。

简略浅显讲便是:拿到一个序列,给序列中元素一顿操作之后,回来序列。

my_map = map(lambda x: x + 1, [1, 2, 3])
print(my_map)
my_list = list(my_map)
print(my_list)
<map object at 0x7f201238cd68>
[2, 3, 4]

你看,map 回来的是一个目标,转 list 之后每个元素的加了1。

3、reduce

reduce 函数便是对一个序列做累积,行将序列中前一个元素和后一个元素进行逻辑组合,然后成果再和后边一个元素组合。

简略浅显讲便是:拿到一个或多个序列,给序列中元素一顿操作之后,回来操作成果。

from functools import reduce
my_rd = reduce(lambda x, y: x + y, [1, 2, 3])
print(my_rd)
6

你看,把列表中的元素都相加了,留意组合联系不必定是相加,你能够换成相乘试试。

乍一看和上面的 map 是一个意思哈,的确用法相同,差异便是 reduce 函数里边的 lambda 函数有两个参数,而 map 函数参数理论上能够多个,可是每个参数对应一个序列,也便是说,有多少个参数,就要有多少个序列

4、filter

filter 函数用于过滤的,行将序列中的每个元素进行判别,然后回来为 True 的元素。

my_ft = filter(lambda x: x % 2 == 1, [1, 2, 3])
print(my_ft)
my_list = list(my_ft)
print(my_list)
<filter object at 0x7f778f58fd68>
[1, 3]

判别序列中哪些数是奇数,filter 回来的是一个目标,转列表之后,能够看到成果。

5、sorted

sorted 函数用于排序,许多同学或许用过它的参数 reverse=False 升序(默许),reverse=True 降序,可是还有个参数 key 或许没咋用过,这儿能够给表达式。

my_st = sorted([1, 5, 3])
print(my_st)
my_st = sorted([1, 5, 3], reverse=True)
print(my_st)
[1, 3, 5]
[5, 3, 1]

数字排序仍是挺好用的哈,处理简略的字符串也都能够,可是假设是处理比较杂乱字符串排序就有点费力了,不信试试看:

test_list = ["test_mi_001","test_ki_012","test_go_008","test_lt_003"]

我想让这个列表依照结束的序号排序:

my_st = sorted(test_list)
print(my_st)
['test_go_008', 'test_ki_012', 'test_lt_003', 'test_mi_001']

排了个寂寞,无论是升序仍是降序都是不可的。

所以需求运用参数 key,加表达式:

my_st = sorted(test_list, key=lambda x: x.split("_")[-1])
print(my_st)
['test_mi_001', 'test_lt_003', 'test_go_008', 'test_ki_012']

唉,这就对了,咱们在表达式里边将结束的序号取出来,key 便是要害字,意思便是依照我取出来的要害字排序。这儿稍微了解一下哈,里边的表达式比较灵敏,你也能够用正则表达式来做:

import re
my_st = sorted(test_list, key=lambda x: re.findall(r"\d+", x))

也都是能够的哈,没缺点。

它不仅能够对列表排序,只需是可迭代目标都能够,列表目标的内建办法 sort 也能够这样用,但差异是 sort 是对原列表进行排序,不回来新列表。

这儿再弥补一个小常识,咱们常常往一个列表中去增加数据,然后对其进行排序,这样做没啥问题,可是假设数据量大了之后,功能会比较低。

维护一个排序序列,主张运用Python 的标准库 bisect 来做,它是选用二分查找算法,功能较高。

6、zip

zip 便是将多个序列打包成一个个元组,然后回来由这些元组组成的列表。

a = [1, 2, 3]
b = [4, 5, 6]
c = zip(a, b)
print(c)
my_list = list(c)
print(my_list)
<zip object at 0x7f4ada0fa548>
[(1, 4), (2, 5), (3, 6)]

zip 回来的是一个目标,实际上是一个迭代器目标。

转列表之后,能够看到,相当于是把元素纵向别离取出来,放到一个元组里边,然后元组组成一个列表。做数据处理的时分常常用到,了解一下。