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、类型判别
在判别数据类型的时分常见的有两种办法:isinstance
和 type
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 创立类的办法就好了。
- 参数 1:
经过上面 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")
实例办法参数有必要传入 self
,self
表明实例目标自身,实例办法的调用也有必要经过实例目标来调用:
Test().obj_method()
(2)类办法
class Test:
@classmethod
def cls_method(cls):
print("this is class method")
能够经过类目标调用,也能够经过实例目标调用。
Test.cls_method()
Test().cls_method()
留意两点:
- 办法前面有必要加装修器
classmethod
,装修器是Python
中的一种语法糖,后边会讲到,记住这种固定用法,这种写法也是初代装修器的用法。 - 参数传入
cls
,cls
表明类目标,可是留意不是有必要的写法,写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__ki
和 go
办法以外,其他的办法都是魔法函数,即最开端咱们说到的协议,你看随便一个目标就完成了这么多协议,是不是很奇特。
(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
反常。
yield
和 return
都能回来数据,可是有差异,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 回来的是一个目标,实际上是一个迭代器目标。
转列表之后,能够看到,相当于是把元素纵向别离取出来,放到一个元组里边,然后元组组成一个列表。做数据处理的时分常常用到,了解一下。