目标

  • 区分模块、包;
  • ✅掌握引用模块、包
  • ✅进阶一点

原本以为今晚可以早早的睡觉,谁知道东西还是蛮多的。。。

第一部分 模块与包

在编程语言中,代码块、函数、类、模块,一直到包,逐级封装,层层调用。

模块与包

模块(module)

含义
python中每个python文件就是一个模块,每个python文件中,封装类似功能的变量、函数、类型等等,可以被其他的python模块通过import关键字引入重复使用!

分类

  1. 自定义模块: 如:自己编写的一个py文件;
  2. 内置模块: 如:os、sys、random等
  3. 第三方模块:requests等

好处

  1. 可维护性
  2. 可复用性

包(package)

含义
包含多个python文件/模块的文件夹,并且文件夹中有一个名称为init.py的特殊声明文件;

作用
可以将大量功能相关的python模块包含起来统一管理,同样也可以被其他模块通过import关键字引入重复使用封装的模块和代码。

示例

package_a
├── __init__.py
├── module_a1.py
└── module_a2.py

__init__.py的作用

  1. Python中package的标识,不能删除(包其实是一个目录,为了和目录做区别,使用了init.py)
  2. 定义all用来模糊导入(包的调用中介绍)
  3. 编写Python代码(不建议在init中写python模块,可以在包中在创建另外的模块来写,尽量保证init.py简单)

包名通常为全部小写,避免使用下划线。

第二部分 使用

导入方法

常见的是五种引用方式:

  1. import module_name:本质是将module_name解释一遍,并将解释结果赋值给module_name;
  2. from module_name import name:本质将module_name中的name变量放到当前程序中运行一边,所以调用的时候直接print(name)就可以打印出name变量的值,切记调用模块的时候只需要import模块名,不需要加.py;
  3. import module_name1, module_name2,...:导入多个模块;
  4. from module_name import name as nm:为导入模块取别名
  5. from module_name import * —- (不建议使用该方法)

typescript的很熟悉上边的写的吧;

模块搜索顺序🔍【原理看第三部分】

不管你在程序中执行了多少次import,一个模块只会被导入一次,顺序如下:

import module_name ---> module_name.py ---> module_name.py的路径--->sys.path

  1. 当前执行脚本所在目录
  2. Python的安装目录
  3. Python安装目录里的site-packages目录

示例一 同级目录

引用示例

这个示例很简单,但已经把引用的使用罗列清楚了,因为Python不需要export。不像typescript那样有导入就有导出,所以,我们已经引用模块了。

至于如何自定义个包,发布到GitHub,让后让别人用,我想那是不是现在关心的。

示例二 他级目录

工作区目录结构如下,实现moduleA引用文件夹module下的moduleC

.
├── Pipfile
├── Pipfile.lock
├── module
│ └── moduleC.py
├── moduleA.py
└── moduleB.py

网上有很多实现的例子,但是,为何那样写?

还是再开个第三部分吧

第三部分 深入理解引用

摘录自:Python 3.x可能是史上最详解的【导入(import)】

Python运行机制

理解Python在执行import语句(导入内置(Python自个的)或第三方模块(已在sys.path中))时,进行了啥操作?

  1. 创建一个新的、空的module对象(它可能包含多个module);
  2. 将该module对象 插入sys.modules中;
  3. 装载module的代码(如果需要,需先编译);
  4. 执行新的module中对应的代码。

第二步涉及一个概念—sys.modules

官网解释
sys.modules是一个 将模块名称(module_name)映射到已加载的模块(modules) 的字典。可用来强制重新加载modules。Python一启动,它将被加载在内存中。

当我们导入新modules,sys.modules将自动记录下该module;当第二次再导入该module时,Python将直接到字典中查找,加快运行速度。

它是个字典,故拥有字典的一切方法,如sys.modules.keys()、sys.modules.values()、sys.modules[‘os’]。但请不要轻易替换字典、或从字典中删除某元素,将可能导致Python运行失败。

import sys
print(sys.modules)#打印,查看该字典具体内容。

导入分类

  1. 相对导入: 同一目录下,如第二部分的实例
  2. 绝对导入: 如下侧,不同目录的导入

import分类

  • “标准”import,顶部导入;
  • 嵌套import
    • 顺序导入-import
    • 循环导入/嵌套导入-import
“标准”import,顶部导入

在 moduleA 中引用 moduleC

.
├── Pipfile
├── Pipfile.lock
├── module
│ └── moduleC.py
├── moduleA.py
└── moduleB.py
'''
this is moduleA
'''
import sys, os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
# 当前文件的绝对路径
print(os.path.abspath(__file__))
# 获取目录
print(BASE_DIR)
# 系统路径下加载
sys.path.append(BASE_DIR)
print('\n')
print(sys.path)
print('\n')
print(sys.modules.keys())
print('\n')

from module.moduleC import add

print(add(1, 2))
···


```python
'''
this is moduleC
'''

def add(a, b):
return a + b

第五

第四

嵌套import

有了上侧以及命名空间的知识,相对下侧图解,就容易理解了。

顺序导入-import

10

PS:各个模块的Local命名空间的独立的。即:
test模块 import moduleA后,只能访问moduleA模块,不能访问moduleB模块。虽然moduleB已加载到内存中,如需访问,还得明确地在test模块 import moduleB。实际上打印locals(),字典中只有moduleA,没有moduleB。

循环导入/嵌套导入-import

20

形如from moduleB import ClassB语句,根据Python内部import机制,执行细分步骤:

  1. 在sys.modules中查找 符号“moduleB”;
  2. 如果符号“moduleB”存在,则获得符号“moduleB”对应的module对象;
    从的 dict__中获得 符号“ClassB”对应的对象。如果“ClassB”不存在,则抛出异常“ImportError: cannot import name ‘classB’”
  3. 如果符号“moduleB”不存在,则创建一个新的 module对象。不过此时该新module对象的 dict 为空。然后执行moduleB.py文件中的语句,填充的 dict 。

总结:from moduleB import ClassB有两个过程,先from module,后import ClassB。

30

当然将moduleA.py语句 from moduleB import ClassB改为:import moduleB,将在第二次执行moduleB.py语句from moduleA import ClassA时报错:ImportError: cannot import name ‘classA’

解决这种circular import循环导入的方法:
例比:安装无线网卡时,需上网下载网卡驱动;
安装压缩软件时,从网上下载的压缩软件安装程序是被压缩的文件。

方法1—–>延迟导入(lazy import):把import语句写在方法/函数里,将它的作用域限制在局部。(此法可能导致性能问题)
方法2—–>将from x import y改成import x.y形式
方法3—–>组织代码(重构代码):更改代码布局,可合并或分离竞争资源。
合并—–>都写到一个.py文件里;
分离–>把需要import的资源提取到一个第三方.py文件中。
总之,将循环变成单向。

How to avoid Python circle import error?
代码布局、(架构)设计问题,解决之道是:将循环变成单向。采用分层、用时导入、相对导入(层次建议不要超过两个)

參考

PYTHON中的包和模块

Python 3.x可能是史上最详解的【导入(import)】

目标

  • 回顾Python面向对象
  • ✅掌握pipenv虚拟环境的基本使用

0

在学习过程中也遇到一些疑惑,摸索了不同的形式、方法,有了一些结论,诸如:

  1. 文章的面向群体–有一定编程基础的;属于个人学习📝,不可能面面俱到;
  2. 文章的形式–如果没有递进深入与实例,仅停留在基本;那是刷流氓;
  3. 学习的目标依旧是–向机器学习靠拢,出于好奇,挑战下自我。

也许好奇才是真正让我学习Python的原有;

不论对错,过程中渐渐培养了专注、思考、探索、总结,想必是值得。
勇敢走出舒适区,去做有意思的事,好好学数学 😆

Python基础系列到此就要告一段落了,该部分主要包含了以下几件事

  1. 迈出了开始学习Python的第一步;
  2. 归纳总结常用的数据类型、方法、函数等,可能会穿插些稍微深入些的知识点;
  3. 查漏补缺,在学习过程中更加深入了理解一些概念、原理。

接下来的安排

因为Node.js属于日常工作范畴,工作之余换种心情😆

  1. 继续学习Python相关知识,模块、框架;
  2. 开始试着写项目,至于类型,在过程中再确定吧;
  3. 开始曾经落下的数学📚,可能会分享相关内容,只是写文章太费时间了。。。;
  4. 希望在不久的将来,开始试着学习机器学习。

第一部分 Python 面向对象

第二部分 pipenv虚拟环境

1. pipenv 是什么

官网地址
Pipenv is a production-ready tool that aims to bring the best of all packaging worlds to the Python world. It harnesses Pipfile, pip, and virtualenv into one single command.

大概意思就是:Pipenv 可以为你的项目自动地创建和管理一个虚拟环境。

如果对Node熟悉的话,应该了解:

  1. nvm 管理nodejs的版本,可自由切换;
  2. npm: 管理工具,可方便下载第三方模块;
  3. package.json: 项目文件,记录一些模块依赖信息、配置信息等

大概可以理解为:对比pipenv:

pipenv = nvm + npm;

当然这只是个简单的。便于学习的对比;

如有不对❌,敬请指出

2. pipenv 能解决什么❓

  1. 你不再需要手动创建虚拟环境,Pipenv 为你自动创建。简单地说就是 pipenv 和 virtualenv 一起工作。
  2. 管理 requirements.txt 文件会导致一些问题,所以 Pipenv 用 Pipfile 和 Pipfile.lock 替代 requirements.txt,更适合于一般的使用场景。
  3. 安全。广泛地使用 Hash 校验,能够自动曝露安全漏洞。
  4. 随时查看图形化的依赖关系。
  5. 通过加载 .env 文件简化开发流程。

3. 安装使用

官网地址有对应的安装方法,下边以Mac为例;

前体是已经安装过Python、pip

$ brew install pipenv

4. 实例一

目标

  • 初识环境
  • 掌握基本的操作

多余代码太多,实例一共包含以下几步

1. 初始化一个虚拟环境;
2. 了解下虚拟环境内容;
3. 新建文件,并运行;
# 1. 初始化一个虚拟环境;
hhw-4:pythonEnv hhw$ pipenv install
Creating a virtualenv for this project…
Pipfile: /Users/hhw/Desktop/pythonEnv/Pipfile
Using /usr/local/Cellar/pipenv/2018.11.26_2/libexec/bin/python3.7 (3.7.3) to create virtualenv…
⠋ Creating virtual environment...Already using interpreter /usr/local/Cellar/pipenv/2018.11.26_2/libexec/bin/python3.7
Using real prefix '/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7'
New python executable in /Users/hhw/.local/share/virtualenvs/pythonEnv-Vx1xxtfj/bin/python3.7
Also creating executable in /Users/hhw/.local/share/virtualenvs/pythonEnv-Vx1xxtfj/bin/python
Installing setuptools, pip, wheel...
done.

✔ Successfully created virtual environment!
Virtualenv location: /Users/hhw/.local/share/virtualenvs/pythonEnv-Vx1xxtfj
Creating a Pipfile for this project…
Pipfile.lock not found, creating…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
Updated Pipfile.lock (a65489)!
Installing dependencies from Pipfile.lock (a65489)…
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 0/000:00:00
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
hhw-4:pythonEnv hhw$
# 3. 新建文件,并运行;
# 下载requests模块
hhw-4:pythonEnv hhw$ pipenv install requests
Installing requests…
Adding requests to Pipfile's [packages]…
✔ Installation Succeeded
Pipfile.lock (444a6d) out of date, updating to (a65489)…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
✔ Success!
Updated Pipfile.lock (444a6d)!
Installing dependencies from Pipfile.lock (444a6d)…
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 5/5 — 00:00:02
To activate this project's virtualenv, run pipenv shell.
Alternatively, run a command inside the virtualenv with pipenv run.
hhw-4:pythonEnv hhw$ vim test.py

用 ls 命令查看当前项目目录,你会发现有两个文件:Pipfile 和 Pipfile.lock 。

Pipfile 里有最新安装的包文件的信息,如名称、版本等。用来 在重新安装项目依赖或与他人共享项目时,你可以用 Pipfile 来跟踪项目依赖。
Pipfile.lock 则包含你的系统信息,所有已安装包的依赖包及其版本信息,以及所有安装包及其依赖包的 Hash 校验信息。

# test.py 内容
import requests
response = requests.get('https://httpbin.org/ip')
print('你的 IP 地址是 {0}'.format(response.json()['origin']))
$ pipenv run python main.py
你的 IP 地址是 8.8.8.8

实例一 总结

以上展示了如何创建一个虚拟环境、如何下载一个模块,在虚拟环境中运行;

也许不太直观,画了两个图,虚拟环境自己玩儿自己的。

虚拟环境

按实际需求来

  1. 你的代码给别人,别人怎么正常运行?
  2. pipenv 常用命令有哪些?

问题一:冻结Pipfile;类比package-lock.json

冻结就相当于将项目所使用的第三方库列表进行打包输出,类比package-lock.json
通过更新Pipfile.lock来冻结库名称及其版本,以及其依赖关系的列表。需要使用lock参数:pipenv lock

被人怎么用?类比 npm install
pipenv install
Pipenv会在项目文件夹下自动寻找Pipfile和Pipfile.lock文件,创建一个新的虚拟环境并安装必要的软件包。

问题二: 可选参数

$ pipenv
Usage: pipenv [OPTIONS] COMMAND [ARGS]...

Options:
--update 更新Pipenv & pip
--where 显示项目文件所在路径
--venv 显示虚拟环境实际文件所在路径
--py 显示虚拟环境Python解释器所在路径
--envs 显示虚拟环境的选项变量
--rm 删除虚拟环境
--bare 最小化输出
--completion 完整输出
--man 显示帮助页面
--three / --two 使用Python 3/2创建虚拟环境(注意本机已安装的Python版本)
--python TEXT 指定某个Python版本作为虚拟环境的安装源
--site-packages 附带安装原Python解释器中的第三方库
--jumbotron 不知道啥玩意....
--version 版本信息
-h, --help 帮助信息
# 命令参数
Commands:
check 检查安全漏洞
graph 显示当前依赖关系图信息
install 安装虚拟环境或者第三方库
lock 锁定并生成Pipfile.lock文件
open 在编辑器中查看一个库
run 在虚拟环境中运行命令
shell 进入虚拟环境
uninstall 卸载一个库
update 卸载当前所有的包,并安装它们的最新版本
# 定位项目路径
$ pipenv --where
# 定位虚拟环境
$ pipenv --venv
# 定位Python解释器
$ pipenv --py
# 安装包
$ pipenv install
# 卸载所有的包:
$ pipenv uninstall --all
# 依赖关系
$ pipenv graph
# 生成lock文件
$ pipenv lock
# 安装开发环境依赖:
$ pipenv install pytest --dev

5 实例二

目标

  • vscode 配置

实例一中直接运行,已经完全ok,但是写代码,我还是不会选择文本编辑器,,,

打开vscode,ヾ(。`Д´。)我擦,找不到requests,思考片刻,因为setting.json中已经配置了python解释器【系统级别的】;
但是之后我们都不在这么玩儿了,毕竟会乱的
同样建议再项目目录中配置各自的配置项目




配置完毕

hhw-4:pythonEnv hhw$ python3 test.py 
Traceback (most recent call last):
File "test.py", line 1, in <module>
import requests
ModuleNotFoundError: No module named 'requests'
hhw-4:pythonEnv hhw$ pipenv run python test.py
你的 IP 地址是 112.64.61.61, 112.64.61.61

一直没找到一个合适的图来描述pipenv…

目标

  • 温故面向对象三大特性
    • Python的私有是真的吗?
    • Python的多继承是什么?
  • 了解下魔方方法

第一部分 魔法方法

相信我们初学者都会有个疑问,下边的意思是啥??为啥要写这个?

反正我是先忽略这个问题,打算在学的过程中慢慢明白😆

if __name__ == '__main__' :

知乎的回答
__name__ 是当前模块名,当模块被直接运行时模块名为 __main__ 。这句话的意思就是,当模块被直接运行时,以下代码块将被运行,当模块是被导入时,代码块不被运行。

还有很多类似的,比如:

__init__ :      构造函数,在生成对象时调用
__del__ : 析构函数,释放对象时使用
__repr__ : 打印,转换
__setitem__ : 按照索引赋值
__getitem__: 按照索引获取值
__len__: 获得长度
__cmp__: 比较运算
__call__: 调用
__add__: 加运算
__sub__: 减运算
__mul__: 乘运算
__div__: 除运算
__mod__: 求余运算
__pow__: 幂

使用实例,比如:

>>> len('ABC')
3
>>> 'ABC'.__len__()
3

class Foo:
def __del__(self):
print("我被回收了!")
obj = Foo()
del obj

等用到了,再看下 😆

第二部分 封装继承多态

封装
封装:将内部实现包裹起来,对外透明,提供api接口进行调用的机制

优点

1. 将变化隔离;
2. 便于使用;
3. 提高复用性;
4. 提高安全性;

原则

1. 将不需要对外提供的内容都隐藏起来;
2. 把属性都隐藏,提供公共方法对其访问。

继承
即一个派生类(derived class)继承父类(base class)的变量和方法。

多态:根据对象类型的不同以不同的方式进行处理。

前提:①类的继承关系 ②要有方法重写。

父类替换子类: super(子类名, self).方法名()

以上概念将通过以下几个实例来说明:

实例一 基本、继承

首先试着下边的三个问题与代码的结果

1. Python的私有方法真的私有吗?
2. Python外部是否可以更改私有属性?
3. 关键字property是什么意思?
# 定义基类
class Person():
def __init__(self, sex, age):
self.age = age
# 定义一个私有变量
self.__sex = sex

def speak(self):
print("性别:%s 年龄:%d " % (self.__sex, self.age))


base = Person(1, 100)
print(base.speak())
print(base.age)
# 调用私用变量: _类名__属性
print(base._Person__sex)
# print(base.__sex)
# AttributeError: 'Person' object has no attribute '__sex'

print("===以上区分调用变量====")


class User(Person):
def __init__(self, sex, age, name):
# 调用父类的实例化方法
Person.__init__(self, sex, age)
self.name = name

def __hide(self):
print('示范隐藏的hide方法')

def getHeight(self):
return self.__height

def setetHeight(self, height):
if height <= 0 or height > 300:
raise ValueError('身高必须在0~300cm之间')
self.__height = height

# property 将geter seter实例为类变量
height = property(getHeight, setetHeight)


# 创建User对象
u = User("男", 20, "Python")
print(u.name)
# seter 方法过滤判断
# u.height = 0
'''
File "class.1.py", line 47, in <module>
u.height = 0
File "class.1.py", line 37, in setetHeight
raise ValueError('身高必须在0~300cm之间'
'''
# 走setter方法
u.height = 1
print(u.height)
# 直接更改内部变量
u._User__height = 0
print(u._User__height)
print(u.height)

其实也不难

结果

性别:1 年龄:100 
# 为何是打印None?
None
100
1
===以上区分调用变量====
Python
1
0
0

实例二 多继承

该部分摘录自Python的方法解析顺序(MRO)

先思考几个问题:

  1. 多继承顺序的规则是什么?
  2. Python中MRO的三种方式是什么?
# 模块inspect:检查运行模块的一些基本信息
import inspect
class A:
def show(self):
print("A.show()")

class B(A):
pass

class C(A):
def show(self):
print("C.show()")

class D(B, C):
pass

# __mro__ 等价于 inspect.getmro
# print(D.__mro__)
print(inspect.getmro(D))
x = D()
x.show()

菱形

按照深度遍历,其顺序为 [D, B, A, object, C, A, object],重复类只保留最后一个,因此变为 [D, B, C, A, object]

结果是

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

Python 至少有三种不同的 MRO:

1. 经典类(classic class)的深度遍历。
2. Python 2.2 的新式类(new-style class)预计算。
3. Python 2.3 的新式类的 C3 算法。它也是 Python 3 唯一支持的方式。
  1. 经典类

经典类的遍历顺序为: 从左至右的深度优先遍历。以上述「菱形继承」为例,其查找顺序为 [D, B, A, C, A],如果只保留重复类的第一个则结果为 [D, B, A, C]

  1. 2.2 新式类的执行结果如上例子所示,但其中有问题,

  2. C3 现在Python采用的方式

# 例子
>>> class D(object): pass
>>> class E(object): pass
>>> class F(object): pass
>>> class B(D, E): pass
>>> class C(D, F): pass
>>> class A(B, C): pass
# 结果
>>> A.__mro__
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.E'>, <class '__main__.F'>, <type 'object'>)

L[object] = [object]
L[D] = [D, object]
L[E] = [E, object]
L[F] = [F, object]
L[B] = [B, D, E, object]
L[C] = [C, D, F, object]
L[A] = [A] + merge(L[B], L[C], [B], [C])
= [A] + merge([B, D, E, object], [C, D, F, object], [B], [C])
= [A, B] + merge([D, E, object], [C, D, F, object], [C])
= [A, B, C] + merge([D, E, object], [D, F, object])
= [A, B, C, D] + merge([E, object], [F, object])
= [A, B, C, D, E] + merge([object], [F, object])
= [A, B, C, D, E, F] + merge([object], [object])
= [A, B, C, D, E, F, object]

说实在的没看懂

最近没更新,是在找学数学方面的资源,
很开心,正在学

实例三 多态

这个似乎很好理解

class Animal:
def kind(self):
print("i am animal")

class Dog(Animal):
def kind(self):
print("i am a dog")

class Cat(Animal):
def kind(self):
print("i am a cat")

# 这个函数接收一个animal参数,并调用它的kind方法
def show_kind(animal):
animal.kind()

d = Dog()
c = Cat()

show_kind(d)
show_kind(c)

'''
i am a dog
i am a cat
'''

实例四 super

如果想让父类替换子类,那么既可以用super()

class A:
def __init__(self, name):
self.name = name
print("父类的__init__方法被执行了!")
def show(self):
print("父类的show方法被执行了!")

class B(A):
def __init__(self, name, age):
# super(子类名, self).方法名()
super(B, self).__init__(name=name)
self.age = age

def show(self):
super(B, self).show()

obj = B("jack", 18)
obj.show()

参考链接

Python的方法解析顺序(MRO)

图解Python 【第六篇】:面向对象-类-进阶篇
堆栈
内存管理
内存
深入内存
python内存管理–分层分配】
内存

Python的方法解析顺序(MRO)

目标

  • 温故函数很多概念
  • 掌握 Python 装饰器

如果能准确的表述下侧代码的结果

就不用往下看啦,想必你已经掌握了 Python 装饰器

def outer(func):
def inner():
print("1")
print("foo---%s" % func)
return "2"
return inner

def foo():
print("3")
return "4"

print("===foo====")
print(foo)
print("===foo()====")
print(foo())
print("===outer(foo)====")
print(outer(foo))
print("===outer(foo())====")
print(outer(foo()))
print("===outer(foo())()====")
print(outer(foo())())

先认真回忆下之前函数的相关章节内容
不难但可能有点绕

===foo====
<function foo at 0x7fcae64dbea0>
===foo()====
3
4
===outer(foo)====
<function outer.<locals>.inner at 0x7fcae64dbe18>
===outer(foo())====
3
<function outer.<locals>.inner at 0x7fcae645d598>
===outer(foo())()====
3
1
foo---4
2

没答出来,或对装饰器有疑惑,那就即可开始吧

第一部分 前奏

针对于开头的那块代码逐步分析下,究竟再内存中是如何运行的❓ 执行顺序是什么❓

强烈推荐网址:代码可视化执行过程

这几行代码主要为了加深理解代码的执行过程函数名函数调用等等

结合代码与下侧图示,重温那些我们习以为常的东西。


def outer(func):
def inner():
print("1")
print("foo---%s" % func)
return "2"
return inner

def foo():
print("3")
return "4"

# 将函数 outer、foo 加载到内存中
# 以上代码到此行,未执行

print("===foo====")
# foo【函数名】;
# 执行:对应的【函数信息】;返回:🈚️
print(foo)
print("===foo()====")
# 真正执行 foo函数
# 执行:打印 3; 返回 4
print(foo())
print("===outer(foo)====")
# 将【foo函数名】传给【outer函数】,参数为 【函数名】
# 执行:将inner加载到内存,返回:🈚️
print(outer(foo))
print("===outer(foo())====")
# 调用【foo函数】,将4作为参数传给传给【outer函数】
# 执行:打印3 返回4, 打印outer函数的返回值 inner内存信息
print(outer(foo()))
print("===outer(foo())()====")
# 调用【foo函数】,将4作为参数传给传给【outer函数】,调用inner函数
# 执行:打印3 返回4,进而打印 1 、 4【func()]、2以及inner的返回值2
# over!!!
print(outer(foo())())

我觉得,先把这些看似简单的东西理清楚,对接下来,装饰器的理解更容易些

第二部分 装饰器【基础】

1 装饰器是什么

装饰器(Decorator):可以在不修改原有代码的情况下,为被装饰的对象增加新的功能或者附加限制条件或者帮助输出。原则:开放封闭

分类

  1. 函数装饰器;
  2. 类的装饰器。

最近公司也在用装饰器,不过是再 Nodejs 中用的;
举个别的例子。因为某种原因,需要对每个接口进行认证记录日志,如何快速高效的实现这样的需求 ❓

可能有几种思路

  • 在基类中写相应的方法,如果有的话;
  • 写两个函数【认证的、日志的】,在每个接口中添加;
  • 等等思路

2 简单的装饰器

但今天只能选装饰器,因为不能偏离主题啦,描述了需求就开始干吧!

def outer(func):
print("装饰器启动,自动执行")
def inner():
print("统一认证")
result = func()
print("记录日志信息")
return result
return inner

@outer
def f1():
print("接口1")
f1()

返回值:

装饰器启动,自动执行
统一认证
接口1
记录日志信息

大致和第一部分的例子有点类似,不同的是

  1. inner 函数返回了个函数
  2. 多个@outer

@outer 中的@是注解语法糖;把 f1 作为参数传给 outer,今儿满足我们的开放封闭的原则;

其解释器会解释成下面这样的语句:
*@outer <=> outer(f1)

翻译成函数调用, 我们用新的例子来对比一下,其实就是下侧这样的

def f2():
print("接口2")
outer(f2)()

'''
装饰器启动,自动执行
统一认证
接口1
记录日志信息
'''

了解了上述的调用含义, @outer <=> outer(f1)

注: 传的只是函数名

自动执行

整体的流程

  1. 自上而下执行,将函数outer在内存注册;
  2. 执行到@outer 时,将函数名f1作为参数传给@outer;
  3. 调用 outer 函数,执行打印【装饰器启动,自动执行】;
  4. 函数 inner 在内存注册,内存地址:[xxxx],返回【inner 函数名】,
  5. 将函数f1在内存注册;其实是直接指向 [xxxx]
  6. 调用 f1(), 直接调用第五步的 [xxxx], 也就是第四步的;
  7. 打印【统一认证】;
  8. result = func()执行回调【f1】函数, 打印:接口 1,没有返回值,result = None
  9. 回到inner(),打印记录日志信息;
  10. 返回 None,OVER!

:完整的有 17 步,动图还不会制作,可以前往–>代码可视化网址**,查看完整的步骤。

实在是想不出来,如何用图来表示装饰器,有相应的图示,请介绍。

比较喜欢这段代码, 挺有意思的。O(∩_∩)O 哈哈~
来源

会自动打印结果,因为采用了装饰器

def fuck(fn):
print "fuck %s!" % fn.__name__[::-1].upper()

@fuck
def wfg():
pass

第三部分 装饰器【进阶】

如果没有看明白第二部分,还是这回去,沉下心,专注的、认真的读下去,真的不难,好好分析下流程,一步一个脚印,踩实了!

只介绍最基础的代码,就是耍流氓

1 多个装饰器

下侧代码来源

#当有多个装饰器装饰一个函数时,他们的执行顺序
#观察下方的代码就会发现
def decorator_a(func):
print('Get in decorator_a')
def inner_a(*args, **kwargs):
print('Get in inner_a')
return func(*args, **kwargs)
return inner_a

def decorator_b(func):
print('Get in decorator_b')
def inner_b(*args, **kwargs):
print('Get in inner_b')
return func(*args, **kwargs)
return inner_b

@decorator_b
@decorator_a
def f(x):
print('Get in f')
return x * 2

f(10)

'''
Get in decorator_a
Get in decorator_b
Get in inner_b
Get in inner_a
Get in f
'''

调用时,函数的顺序

  1. 顺序等同于==> decorator_b(decorator_a(f)),返回的是inner_b,所以执行的时候是先执行inner_b
  2. 然后在执行【decorator_a(f)】返回的inner_a .最终在调用f(1)的时候,函数inner_b输出’Get in inner_b’
  3. 执行inner_a输出Get in decorator_a,最后执行func(),即f

2 多参多装饰器

为了更加深入的了解,装饰器,摘录酷 壳 博客上的一段代码,好像官网实例代码也不少;

def makeHtmlTag(tag, *args, **kwds):
def real_decorator(fn):
css_class = " class='{0}'".format(kwds["css_class"]) \
if "css_class" in kwds else ""
def wrapped(*args, **kwds):
return "<"+tag+css_class+">" + fn(*args, **kwds) + "</"+tag+">"
return wrapped
return real_decorator

@makeHtmlTag(tag="b", css_class="bold_css")
@makeHtmlTag(tag="i", css_class="italic_css")
def hello():
return "hello world"

print(hello())

'''
<b class='bold_css'><i class='italic_css'>hello world</i></b>
'''

3 再深入一些

表示还没有研究这段代码,先分享出来
摘录酷 壳 博客上的代码

from functools import wraps
def memo(fn):
cache = {}
miss = object()

@wraps(fn)
def wrapper(*args):
result = cache.get(args, miss)
if result is miss:
result = fn(*args)
cache[args] = result
return result

return wrapper

@memo
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)

Python函数告一段落了

存在即合理
正如明白了x += 1,就会尽量避免x = x + 1;

反观一搜一大把关于闭包的文章,无外乎只是告诉三点

  • 晦涩难懂的概念
  • 给几个简单的实例
  • 结束
    或许还会给个建议 少用

真的是这样么?
我搜索了大量的资料来写这篇文章,只是想尽力把自己说服、说明了。

下边有三到题目以及答案

  • 如果你的预期和答案不符
  • 如果你看不懂
  • 如果你好奇的想一探究竟

请继续

否则就可以关闭这个文章了

文章长且内容多

# 第一题,是用来做什么的?
>>> def isPrime(start, stop):
... return filter(lambda x : not [x % i for i in range(2, x) if x % i == 0], range(start, stop + 1))
# 第二题
i = 0
def new_counter():
i = 10
def count(x=1):
nonlocal i
i += x
return i
return count
a_count = new_counter()
print(a_count())
print(a_count())
print(a_count(3))
# 第三题
def makeActions():
acts = []
for i in range(3):
acts.append(lambda x: i ** x)
return acts
acts = makeActions()
print(acts[0](2))
print(acts[1](2))
print(acts[2](3))

答案

# 第二题
11
12
15
# 第三题
4
4
8

警示:认真的读下去,可能会花费你不少的时间

目标

  • 掌握匿名函数
  • 掌握高阶函数与匿名函数的用法
  • 尽量搞明白闭包

目录结构

第一部分 匿名函数

1 定义

lambda 函数也叫匿名函数,及即没有具体名称的函数,它允许快速定义单行函数,类似于 C 语言的宏,可以用在任何需要函数的地方。

lambda与def对比

区别 def lambda
名称
返回值 函数对象但不给标识符 任意
作用 简单的 简单/复杂
作用范围 不能共享 可共享

2 表达式

# 函数
def funName():
# 表达式
return "返回值"
# 匿名函数
lambda argument1,argument2,...argumentN:expression using arguments

lambda 参数: 表达式

实现同样代码不同方式的实例:

# 函数
>>> def mul(x, y):
... return x*y
...
>>> mul(2, 3)
6
# 匿名函数
>>> lam = lambda x, y: x*y
>>> lam
<function <lambda> at 0x101b11268>
>>> lam(2, 3)
6
>>> lam(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 1 required positional argument: 'y'

3 匿名函数与高阶函数

Python内置对数据处理的函数,效率比for要高,恰好,我们可以结合lambda函数,一块把二者学习了

# 罗列了常用的三种
1. map(function, iterable[, iterable, ...]) -> list

2. filter(function or None, iterable) -> list, tuple, or string

3. reduce(function, sequence[, initial]) -> value

简要介绍:

  • map映射: 对iterable中的item依次执行function(item),执行结果输出为list
  • filter过滤: 对iterable中的item依次执行function(item),将执行结果为True(!=0)的item组成一个List/String/Tuple(取决于iterable的类型)返回,False则退出(0),进行过滤。
  • reduce: iterable中的item顺序迭代调用function,函数必须要有2个参数。要是有第3个参数,则表示初始值,可以继续调用初始值,返回一个值

图解:

map
filter
reduce

实例列表

# ------- map ------- #
>>> map(str, [1, 2, 3, 4])
['1', '2', '3', '4']
>>> map(int, ['1', '2', '3', '4'])
[1, 2, 3, 4]
>>> map(lambda x: x * x, [1, 2, 3, 4]) # 使用 lambda
[1, 4, 9, 16]

# ------- reduce ------- #
# reduce 操作实例
>>> reduce(lambda x, y: x * y, [1, 2, 3, 4]) # 相当于 ((1 * 2) * 3) * 4
24
>>> reduce(lambda x, y: x * y, [1, 2, 3, 4], 5) # ((((5 * 1) * 2) * 3)) * 4
120
>>> f = lambda a, b: a if (a > b) else b # 两两比较,取最大值 【三目运算也在阶段汇总与补充中说明】
>>> reduce(f, [5, 8, 1, 10])
10

# -------- filter ------- #
>>> list = filter(lambda x: x % 2 == 0, [1, 2, 3, 4, 5, 6])
>>> print(*list)
2 4 6

两个综合实例

这个就是开头的那个实例: 找出指定返回内的质子,涉及的内容有点多,包含之前章节中的推导式range函数函数not等相关内容,试着先看下,是否能看明白。

# 质数的定义 只有1和它本身两个因数
>>> def isPrime(start, stop):
... # 取出质数,x从range(start,stop+1) 取的数
... return filter(lambda x : not [x % i for i in range(2, x) if x % i == 0], range(start, stop + 1))
...
>>> isPrime(11, 12)
>>> print(result)
<filter object at 0x101b12400>
>>> print(list(result))
[11]

整个过程的步骤有三七步骤

试了下画个流程图,来作为补充,但是我怕画出来,更难理解了;
所以就用文字描述下,希望描述的清楚:

  • 1 range(start, stop + 1) 生成 [)左闭右开list_1数组
  • 2 依次将list_1中的数【x】传给匿名函数【循环该操作】
    • 3 生成[2, x)的list_2
    • 4 遍历list_2, 依次用 x%list_2[i]取余;*【循环该操作】**
    • 5 如果数组长度等于0,回到2
    • 6 如果余数等于0,返回余数,存进list_new;
    • 7 等整个2走完,判断list_new是否存在数据,得出当前x是否是质数,是否从list_1中过滤掉
    • 8 进入list_1下一个元素的判断

先熟悉下not的用法,【在阶段汇总与补充中说明】

>>> not []
True
>>> not [1]
False
>>> not [0]
False

下边这个求阶乘的,没啥难度,只要还记得

# 思考下这个,在思考下上边关于reduce的图例
reduce(lambda x, y: x * y, [1, 2, 3, 4], 5) # ((((5 * 1) * 2) * 3)) * 4

类比一下

# 计算 5!+4!+3!+2!+1!
>>> def addFactorial(n):
... result = []
... for i in map(lambda x:x + 1, range(n)):
... a = reduce(lambda x, y:x * y, map(lambda x:x + 1, range(i)))
... result.append(a)
... return reduce(lambda x, y:x + y, result)
...
>>> addFactorial(2)
3
>>> addFactorial(3)
9

第二部分 闭包

接下来的内容涉及函数相关内容,之前的两篇文章
Python 基础系列–函数【9】
Python 基础系列之作用域【10】

函数概述图

函数是对象,函数可以作为返回值,

在说闭包定义之前,我们先看下开头的那两段代码

1. 初始闭包

函数是对象,函数可以作为返回值。

# 第二题
i = 0
def new_counter():
i = 10
def count(x=1):
nonlocal i
i += x
return i
return count
a_count = new_counter()
print(a_count())
print(a_count())
print(a_count(3))

闭包

上侧的例子主要实现的是一个计数器的功能每点击一次,在原基础上加1;

结合上图,大致了解下流程:

  • 函数定于与返回:
    • 外层函数new_counter返回了内层函数count作为结果;
    • 内层函数count,中的参数:x有个默认值1;
    • 内层函数count中的 i 利用了nonlocal,获取到10;
  • a_count = new_counter()
    • 将变量a_count指向new_counter()返回的count()
    • count()的type为func,并且i持有10对应的内存地址
  • 第一次调用a_count()时
    • 没有参数,x默认为1;
    • i持有10,经过➕后返回11
  • 第二次调用a_count()时
    • 没有参数,x默认为1;
    • i持有11,经过➕后返回12
  • 第二次调用a_count()时
    • 没有参数,x=3;
    • i持有12,经过➕后返回15

此时我不确定看了上图和说明,是否能够看懂闭包的用法

2 闭包的定义

比较晦涩的专业术语,但还是建议认真读读

都是大神发明的东西

真的很巧妙

下边的文字值得读 N 遍

维基百科上对闭包的解释就很经典:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 Peter J. Landin 在1964年将术语闭包定义为一种包含环境成分和控制成分的实体。

  • 闭包概念:
    • 闭包就是有权访问另一个函数作用域中变量的函数.
  • 分析这句话:
    1. 闭包是定义在函数中的函数.
    2. 闭包能访问包含函数的变量.
    3. 闭包携带状态包
      • 即使包含函数执行完了, 被闭包引用的变量也得不到释放.

接下来看第二个例子

3. 闭包的作用域

# 第三题
def makeActions():
acts = []
for i in range(3):
acts.append(lambda x: i ** x)
return acts
acts = makeActions()
print(acts[0](2))
print(acts[1](2))
print(acts[2](3))

闭包2

当执行结束acts = makeActions()这一行代码。

  1. makeActions()函数内部【i】持有了 2,并且不释放;
  2. acts实际包含了三个函数类型的内容
  3. 当调用acts[x](2), 实际执行函数为lambda x: i ** x,而i持有2
  4. 故当 y 一致时,x 的位置变化不影响结果,(acts[x] (y))

4 加深一点

如果我们就是要错误的结果,【0,1,8】呢?
如何去实现?

还记得在前面🈶个状态的概念吗?

  • 携带状态的闭包的概念
    • 即使包含函数执行完了, 被闭包引用的变量也得不到释放.

思路:假如我们把列表中的位置对应i的状态保存起来,那就不就完事了么?

如果真的从头看到现在,并且一路思考,想必已经花费你不少时间了。

此时的你应该完全理解文中阐述的内容了。

相对真正掌握知识而言,付出的时间的价值是翻倍的

def makeActions():
acts = []
for i in range(3):
def g(param):
newParam = lambda x: param ** x
return newParam
acts.append(g(i))
return acts
acts = makeActions()
print(acts[0](2))
print(acts[1](2))
print(acts[2](3))

第一次保留的状态
三种状态

5 再深入一些

这段代码能让你想到什么❓
最直观的就是:这个是个闭包

但是:这段代码万万不能让我联想到:直线方程斜率截距坐标系等等的概念。

# 第一种写法
def line_conf(a, b):
def line(x):
return a*x + b
return line

line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5), line2(5))

# 第二种写法
def line_conf(a, b, x):
return a*x + b
line_conf(1, 1, 5)
line_conf(4, 5, 5)

当我们明白了:函数是对象函数可以作为返回值闭包携带状态等等的概念后,可能会思考

  1. 为何会有这种东西存在❓
  2. 究竟其用途是什么❓

关于这个疑问,在所搜大量数据的时候,发现了知乎上有个有意思的问答

以下摘录其中的一部分

  1. 简短答案:前者更具有可读性和可移植性。

  2. 较完整答案:闭包(closure)和类(class)有相通之处,带有面向对象的封装思维。而面向对象编程正是为了更佳的可读性和更关键的可移植性;不过这个例子没太体现出面向对象的额外优势。

  3. 升级答案:题主问出这个问题,很可能是现在流行的编程教材知其然不知其所然的风格带来的恶果。如果未来带着这个想法进入IT行业,会非常不适应公司的代码规范等基础要求;即便不入行只是自己写写程序用,也会和很多优秀、可复用等的理念失之交臂。

  4. 我认为一本优秀的教材除了讲基础语法,应该尽量教你代码为什么这么写好那么写不好、那么那么写现在能用但多半以后会出事

以上为摘录其中的一部分

我们所讨厌的行为,却是自己的所作所为。

多问问什么,也许有一天,那些望而生畏会变成理所当然。

参考链接
Python 函数之lambda、map、filter和reduce
内置函数的使用

目标

  1. 尽量掌握 Python 命名空间、作用域
  2. 掌握 Python 匿名函数

本文本属于那种

只可意会不可言传的范畴

原因:

1. 大部分人都直接开撸,因为和想象差不多;
2. 不容易说明白, 除非遇到bug了,断点调试。

第一部分 命名空间

听起来高大上的词汇,或许是简单的.

在理解这个词汇钱,先看一个示例

A 公司有个员工 A1,工号 001;
B 公司有个员工 B1,工号 001;

为何工资不会发错?

这不废话么?

原因在于:虽有同样的工号,但是相对而言的,在不同的公司(空间)内。

1.定义

命名空间(英语:Namespace),也称名字空间、名称空间等,它表示着一个标识符(identifier)的可见范围
一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的。这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中。

空间相当于一个集合包含相对相对范围内所有的内容(对象名)

2. 分类

  1. 内置命名空间(Built-in Namespaces):伴随 Python 的运行而存在,内置函数的命名空间都属于内置命名空间,比如前面的 id(),直接可以用。
  2. 全局命名空间(Module:Global Namespaces):每个模块拥有自己的命名集合;
  3. 本地命名空间(Function&Class: Local Namespaces):局部命名空间,每个函数、类所拥有的命名空间,记录了函数、类中定义的所有变量。

一图胜前言

命名空间

3. 生命周期

  1. 内置命名空间在 Python解释器启动时创建,解释器退出时销毁
  2. 全局命名空间在模块被解释器读入时创建解释器退出时销毁
  3. 局部命名空间,包含函数、类定义。
    • 函数的局部命名空间:在函数调用时创建,函数返回结果或抛出异常时被销毁(每一个递归函数都拥有自己的命名空间);
    • 类定义的命名空间:在解释器读到类定义(class 关键字)时创建,类定义结束后销毁。(*)

4. 存在的意义

为了解决命名冲突。

你去寄快递,说邮寄到南京路 118 号,他肯定会让你写上是哪个省的,否则不就乱了么。

第二部分 作用域

1. 作用域是什么?

可能有点绕,多读几遍,多思考下

可能有点绕,多读几遍,多思考下

命名空间是对象名称集合,可被访问区域,比如 id(),可以全局被调用
作用域是针对命名空间而言的,是作用域定义了命名空间的可用范围

以下注意事项会在第三部分的代码中证实:

  1. 只有函数、类、模块会产生新的作用域,代码块(例如if、for代码块)不会产生新的作用域。
  2. python中变量的作用域是由它在源代码中的位置决定的(*)

先看下分类,然后进一步阐述命名空间与作用域的关系

2. 分类

  1. Local(局部作用域):首先搜索,包含局部名字的最内层(innermost)作用域,如函数/方法/类的内部局部作用域;
  2. Enclosing( 闭包函数外的函数中):根据嵌套层次从内到外搜索,包含非局部(nonlocal)非全局(nonglobal)名字的任意封闭函数的作用域。如两个嵌套的函数,内层函数的作用域是局部作用域,外层函数作用域就是内层函数的 Enclosing 作用域;
  3. Global(全局作用域
    ):倒数第二次被搜索,包含当前模块全局名字的作用域;
  4. Built-in(内建作用域):最后被搜索,包含内建名字的最外层作用域。

查找规则

如图所示

内层可以访问外层,但外层不能访问内层

global_var = 2  # 全局作用域
x = id(global_var) # 内建作用域,调用id()函数
def outer():
out_var = 1 # 闭包函数外的函数中
def inner():
inner_var = 2 # 局部作用域

3. 关系

尽量用自己的理解去阐述明白,如有不对,敬请指正。

第一阶段:在人类诞生初期,是没有族群、部落、国家之分的,此时相当于全人类都是属于一个人类的集合,此时的人类相当于一个人类,没有高低贵贱黑白美丑之分。

第二阶段:族群的出现,代表着某些人有用自己的势力范围,把人类一个整体分为N个族群

第三部分 函数作用域

上一篇是关于函数的,那就以函数作用域为实例,类、模块都是类似的。

1. 整体的例子

x = 1
print("第一步:x的内存地址: ", id(x))

def outer():
x = 2
# 同层 x=2
print("第二步:x的内存地址: ", id(x))
def inner():
x = 3
# 打印同层 x=3
print("第三步:x的内存地址: ", id(x))
def inner_up():
# 打印上层 x=2
print("第三步(up):x的内存地址: ", id(x))
inner()
inner_up()
# 打印的是同层x=2,即outer()下侧的同一作用域的x
print("第四步:x的内存地址: ", id(x))
outer()
print("第五步:x的内存地址: ", id(x))
$ python3 fun.py 
第一步:a的内存地址: 4336156688
第二步:x的内存地址: 4336156720
第三步:x的内存地址: 4336156752
第三步(up):x的内存地址: 4336156720
第四步:x的内存地址: 4336156720
第五步:x的内存地址: 4336156688

2. nonlocal和global区别

  1. global关键字:用来在函数或其它局部作用域中使用全局变量
  2. nonlocal关键字:用来在函数或其它作用域中使用外层(非全局)变量
x = 1
print("第一步:x的内存地址: ", id(x))

def outer():
x = 2
print("第二步:x的内存地址: ", id(x))
def inner():
# 注意这行
global x
x = 3
print("第三步:x的内存地址: ", id(x))
inner()
print("第四步:x的内存地址: ", id(x))
outer()
print("第五步:x的内存地址: ", id(x))

针对outer中x的:

  1. 采用global的结果
    global

  2. 采用nonlocal的结果
    nonlocal

3. 作用域的位置

name ='简单即是美'

def f1():
print(name)

def f2():
name = 'Python'
f1()
f2()

Python函数的作用域取决于其函数代码块在整体代码中的位置,而不是调用时机的位置。调用f1的时候,会去f1函数的定义体查找,对于f1函数,它的外部是name=’简单即是美’,而不是name = ‘Python’。

结果

简单即是美

倒数第二步
倒数第二步

参考链接

Python 中命名空间与作用域使用总结

目标

  1. 掌握 Python 函数–定义、参数类型
  2. 掌握 Python 函数–变量作用域

正确理解 Python 函数,能够更好地理解 Python 装饰器、匿名函数(lambda)、函数式编程等高阶技术。

文章较长,整体内容包含如下思维导图

概述

第一部分 重新认识函数

1. 定义

Python 中函数的定义如下:

def 函数名(参数):
# 内部代码
return 表达式

注意点:

  1. 函数代码块以def关键词开头,一个空格之后接函数标识符名称圆括号(),再接个冒号
  2. 圆括号中间存放参数
  3. 使用 return 结束函数。默认返回 None。
>>> def sum(a, b):
... return a + b
...
>>> sum
# 函数地址
<function sum at 0x10af4f158>
## 函数方法
>>> dir(sum)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> sum.__name__
'sum'
>>> sum.__module__
'__main__'
>>>

2 调用函数

使用函数名后跟圆括号的方式调用函数。
调用的同时要根据函数的定义体,提供相应个数和类型的参数,每个参数之间用逗号分隔。

3. 函数是对象

在 Python 中万物皆为对象,函数也不例外,

  1. 可以作为对象可以赋值给一个变量
  2. 可以作为元素添加到集合对象中
  3. 可作为参数值传递给其它函数
  4. 可以当做函数的返回值

函数身为一个对象,拥有对象模型的三个通用属性:id、类型、和值。

>>> def add(a, b):
... return a + b
...
>>> id(add)
4472644200
>>> type(add)
<class 'function'>
>>> add
<function add at 0x10a972268>

变量赋值

>>> newAdd = add
>>> newAdd(1, 2)
3
>>> add is newAdd
True

其实上侧的,只是不同的名字指向同一个内存地址,同时这块地址被多次引用。

变量

了解了函数是对象,以及上侧的几个性质,更得牢记一切都是对象。
既然是对象,性质都类似,其余的集合、参数、返回值,思考下就明白了

第二部分 参数类型

其实就是下侧那个括号里的数据,但是需要注意的是,传的是什么怎么传

定义

def 函数名(参数):
# 内部代码
return 表达式

完整的语法

func(positional_args, keyword_args, *tuple_nonkw_args, **dict_kw_args)

函数的参数的分类

  1. 位置参数(必选参数)
  2. 默认参数
  3. 可变参数 *args
  4. 可变参数 **kwargs
  5. 必选参数

1. 位置参数

定义:调用时候的传的参数必须与定义时的参数一一对应。
注意事项:
不能多也不能少。

>>> def sum(a, b):
... return a + b
# 一一对应
>>> print(sum(1, 2))
3
# 参数缺失
>>> print(sum(1))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sum() missing 1 required positional argument: 'b'
# 关键字参数,顺序不重要,存在即可
>>> print(sum(b=10, a=4))
14

2. 默认值

定义:在函数声明的时候,可以给某个参数指定默认值
注意事项:
位置:需要放在位置参数之后

默认参数需要在位置参数的后边

def power(x, n = 2):
return x**n
ret1 = power(10) # 使用默认的参数值n=2
ret2 = power(10, 4) # 将4传给n,实际计算10**4的值
ret3 = power(n=1, x=100) # 指定参数名传值

来个例子
主要涉及:内存地址、参数默认值

def func(a=[]):
a.append("A")
return a

print(func())
print(func())
print(func())

三思
三思
三思

答案如下:

['A']
['A', 'A']
['A', 'A', 'A']

如果真正理解了,Python 对象的变量名与内存地址,这个问题还是很简单的。

  1. 默认参数 a 指向的空列表对象就会被创建,假如内存地址为0x123;
  2. 之后每次调用,都会在原地址后追加A;
  3. 可以试着在函数内部打印 a 的内存地址,应该是一致的。

如何避免这种操作?

让 a 的默认值指向一个不可变对象即可,如:数字、None 等

3. 可变参数 *args

在 python 里面,函数在声明的时候,参数中可以使用(变量名)的方式来接受不确定长度的参数,但是在 python 里面大家约定俗成使用`args接受不定长参数,之后将这些参数放到一个tuple`里面,可以通过访问 args 来获取这些不定长参数。

例子一

>>> def changeFunc(*args):
... print(type(args))
... print(args)
...
>>> changeFunc("a", "b", 3)
<class 'tuple'>
('a', 'b', 3)

例子二

>>> changeFunc(["a", "b", 3])
<class 'tuple'>
(['a', 'b', 3],)

为何打印出来是这样?
因为是数组是作为一个整体传过来的。
如何将 list 的元素转换为 tuple 的元素?结果与例子一一致?

答案: 参数前加个星号,解压参数列表.

>>> tmpList = ["a", "b", 3]
>>> print(*tmpList)
a b 3
>>> print(tmpList)
['a', 'b', 3]
# 调用函数时,数组➕星号,解压参数列表
>>> changeFunc(*tmpList)
<class 'tuple'>
('a', 'b', 3)

当参数已经在列表或元组中但需要为需要单独位置参数的函数, 需要解压参数, 然后再一一传进去

针对字典带上*号,解压的是 key

>>> dict = {"a": 1, "b": 2, "c": 3}
>>> print(*dict)
a b c

4. 可变参数 *args

定义:*args参数表示未知的位置参数序列长度,而\*\*kwargs代表包含关键字和值映射关系的字典,它可以包含任意数量的关键字和值映射,并且在函数定义中“*args”必须位于“**kwargs”前面。

*args 必须在**kwargs 之前。
默认值参数必须再必选参数之前。

>>> def kwargs(first, second=2, *args, **kwargs):
... print(first, second)
... print(args)
... print(kwargs)
...
# kwargs为空是因为被*args吃掉了
>>> kwargs(1, 3, "a", "b", "c", {"key1": "value"})
1 3
('a', 'b', 'c', {'key1': 'value'})
{}
>>> list = ["a", "b", "c"]
>>> dict = {"key1": "value"}
# 解压参数,
>>> kwargs(1, 3, *list, **dict)
1 3
('a', 'b', 'c')
{'key1': 'value'}
# 后边不能追加参数了。。
>>> def test(**kw, error):
File "<stdin>", line 1
def test(**kw, error):
^
SyntaxError: invalid syntax

5. 关键字参数

位置参数可以不带参数名,但是星号后边的就必选带上参数名

# 星号后必选加上参数名字
>>> def func(loction, second, *, must):
... print(loction, second, must)
...
>>> func(1, 2, 3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func() takes 2 positional arguments but 3 were given
# 符合预期
>>> func(1, 2, must=3)
1 2 3

来一个汇总的骚操作

>>> def student(name, age=18, *args, sex, classroom, **kwargs):
... print(name, age, args, sex, classroom, kwargs)
...
# *args 之后的一般参数默认更改为关键字参数,sex/classroom未传递,报错
>>> student("howard")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: student() missing 2 required keyword-only arguments: 'sex' and 'classroom'
# 位置参数与关键字参数【必选】,可选参数【可选】
>>> student("howard", sex="1", classroom="python")
howard 10 () 1 python {}
# dict
>>> dict
{'key1': 'value'}
>>> student("howard", sex="1", classroom="python", **dict)
howard 10 () 1 python {'key1': 'value'}

本文重点注意事项:

  1. 牢记函数为第一类对象;
  2. 可选参数注意解压参数;

参考
函数作为参数的问题
Python 中的函数详解

目标

  • 掌握Node.js核心模块之网络模块

文章目录

Node.js标准库提供了 http 模块,是Node.js中非常重要的一个核心模块。

通过http模块,你可以使用其http.createServer方法创建一个http服务器,也可以使用其http.request方法创建一个http客户端。

第一部分: http服务端

1.1 HTTP是什么?

HTTP是一个用来传送数据的应用层协议,在其底层使用TCP传输协议。HTTP是互联网数据通信的基础,要响应客户端请求(如:浏览器访问),首先要布署HTTP服务器。

1.2 HTTP之URL

URL: (Uniform / Universal Resource Locator,常缩写为URL)统一资源定位符,统一资源定位符的标准格式如下:

协议类型://服务器地址(必要时需加上端口号)/路径/文件名

http1-url-structure.png

1.3 http之请求报文

一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成;

2012072810301161.png

举例子:

201411187986714.png

请求报文中关键的字段:

User-Agent:产生请求的浏览器类型;
Accept:客户端可识别的响应内容类型列表;星号 “ * ” 用于按范围将类型分组,用 “ */* ” 指示可接受全部类型,用“ type/* ”指示可接受 type 类型的所有子类型;
Accept-Language:客户端可接受的自然语言;
Accept-Encoding:客户端可接受的编码压缩格式;
Accept-Charset:可接受的应答的字符集;
Host:请求的主机名,允许多个域名同处一个IP 地址,即虚拟主机;
connection:连接方式(close 或 keepalive);
Cookie:存储于客户端扩展字段,向同一域名的服务端发送属于该域的cookie;

1.4 HTTP响应报文

HTTP 响应报文由状态行、响应头部、空行 和 响应包体 4 个部分组成

201446213766964.jpg

状态代码由三位数字组成,第一个数字定义了响应的类别,且有五种可能取值。

1xx:指示信息--表示请求已接收,继续处理。
2xx:成功--表示请求已被成功接收、理解、接受。
3xx:重定向--要完成请求必须进行更进一步的操作。
4xx:客户端错误--请求有语法错误或请求无法实现。
5xx:服务器端错误--服务器未能实现合法的请求。

常见的状态码:

200 OK:客户端请求成功。
400 Bad Request:客户端请求有语法错误,不能被服务器所理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务。
404 Not Found:请求资源不存在,举个例子:输入了错误的URL。
500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。

第二部分 http服务器端

1.1 http.server遵循了node.js的时间模块的规则, 有触发和相应事件;http.Server对象是一个EventEmitter;

创建: http.createServer方法会返回一个http.Server对象实例,requestListener可选参数传入时,将做为http.Server对象’request’事件的监听器。

http.Server对象提供的事件方法有:

1. request: function (request, response) { }每次请求都会触发该方法;
2. connection:当TCP连接建立时,该事件被触发,提供一个参数socket,是net.Socket的实例;
3. close: 当此服务器关闭时触发;
等等..

1.2 http.Server中的方法

2.2.1 server.listen(port[, hostname][, backlog][, callback])

在指定的主机名(hostname)和端口(port)开始接收连接,

1.2.2 server.listen(path, [callback])

监听某个

1.3 http.ServerResponse服务器响应对象

http.ServerResponse是一个由http.Server创建的对象, ,在触发’request’事件后,做事件回调函数的第二个参数传递给回调函数.

response.writeHead(statusCode[, statusMessage][, headers])

向客户端请求发送一个响应头。
statusCode状态码是 3 位数字,如:404。
statusMessage设置状态消息。
最后一个参数headers是响应头。

这个方法只能在当前请求中使用一次,并且必须在response.end()之前调用。

var body = 'hello world';
response.writeHead(200, {
'Content-Length': body.length,
'Content-Type': 'text/plain' });

response.setHeader(name, value)

为默认或者已存在的头设置一条单独的头内容。如果这个头已经存在于 将被送出的头中,将会覆盖原来的内容。如果我想设置更多的头, 就使用一个相同名字的字符串数组

response.write(chunk, [encoding])
向响应流发送一个数据块。这个方法可能被调用多次,以提供响应体内容。

response.end([data], [encoding])

当所有的响应报头和报文被发送完成时这个方法将信号发送给服务器,服务器会认为这个消息完成了。 每次响应完成之后必须调用该方法。

第三部分 http客户端

http模块提供了两个创建HTTP客户端的方法http.requesthttp.get,以向HTTP服务器发起请求。http.get是http.request快捷方法,该方法仅支持GET方式的请求。

一个 IncomingMessage对象是由 http.Server或http.ClientRequest创建的,并作为第一参数分别传递给’request’和’response’ 事件。它也可以被用来访问应答的状态,头文件和数据。

var http = require('http');

http.createServer(function (req, res) {
var content = "";

req.on('data', function (chunk) {
content += chunk;
});

req.on('end', function () {
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("You've sent: " + content);
res.end();
});

}).listen(3000);

data事件会在数据接收过程中,每收到一段数据就触发一次,接收到的数据被传入回调函数。end事件则是在所有数据接收完成后触发。

http.request(options[, callback])
request方法的options参数,可以是一个对象,也可以是一个字符串。如果是字符串,就表示这是一个URL,Node内部就会自动调用url.parse(),处理这个参数。

options对象可以设置如下属性:

host:HTTP请求所发往的域名或者IP地址,默认是localhost。
hostname:该属性会被url.parse()解析,优先级高于host。
port:远程服务器的端口,默认是80。
localAddress:本地网络接口。
socketPath:Unix网络套接字,格式为host:port或者socketPath。
method:指定HTTP请求的方法,格式为字符串,默认为GET。
path:指定HTTP请求的路径,默认为根路径(/)。可以在这个属性里面,指定查询字符串,比如/index.html?page=12。如果这个属性里面包含非法字符(比如空格),就会抛出一个错误。
headers:一个对象,包含了HTTP请求的头信息。
auth:一个代表HTTP基本认证的字符串user:password。
agent:控制缓存行为,如果HTTP请求使用了agent,则HTTP请求默认为Connection: keep-alive,它的可能值如下:
undefined(默认):对当前host和port,使用全局Agent。
Agent:一个对象,会传入agent属性。
false:不缓存连接,默认HTTP请求为Connection: close。
keepAlive:一个布尔值,表示是否保留socket供未来其他请求使用,默认等于false。
keepAliveMsecs:一个整数,当使用KeepAlive的时候,设置多久发送一个TCP KeepAlive包,使得连接不要被关闭。默认等于1000,只有keepAlive设为true的时候,该设置才有意义。

实例

var http = require('http');
http.createServer(function(req, res){
//仅对从http.Server获得到的请求(request)有效.
console.log(req.url);
//请求/响应 头对象.
console.log(req.headers);
//服务器的HTTP版本。
console.log(req.httpVersion);
//接收到的原始请求/响应头字段列表。
console.log(req.rawHeaders);
//仅对从http.Server获得到的请求(request)有效.比如‘GET’、‘DELETE’。
console.log(req.method);
}).listen(3000, '127.0.0.1');

第四部分 实例

1 创建服务器, 实现GET方法

var http = require('http');
//创建一个服务器, 回调函数的参数为响应, 和请求
http.createServer(function(request, response){
//response.writeHead方法用来写入HTTP回应的头信息;
response.writeHead(200, {'Content-Type': 'text/plain'});
//指定HTTP回应的内容。
//response.write('hello world');
//response.end方法用来写入HTTP回应的具体内容,以及回应完成后关闭本次对话。
response.end('hello world');
//监听端口号和主机名
}).listen(3000, '127.0.0.1');

console.log('127.0.0.1:3000');

2. 加载HTML文件

var http = require('http');
var fs = require('fs');
http.createServer(function(req, res){
//读取HTML文件
fs.readFile('./2index.html', 'utf-8',function(err, data){
if(err) throw err;
//为默认或者已存在的头设置一条单独的头内容。
res.setHeader("Content-Type","text/html;charset='utf-8'");
//响应的数据
res.end(data);
});
}).listen(3000, '127.0.0.1');

3 创建服务器, 实现POST方法

一个简单的post请求:

var http = require('http');
var fs = require('fs');

http.createServer(function(req, res) {
//fs模块创建流文件, 名称为post.txt
var file = fs.createWriteStream('post.txt');
////管子 pipe自动调用了data,end等事件 writeStream/readStream
req.pipe(file);
// 当有数据流时,写入数据
req.on('data', function(postData){
console.log(postData);
});

req.on('end', function(){
res.end('end');
});
}).listen(3000);

post请求

参考链接
1.HTTP 协议入门
2.HTTPS 升级指南
3.HTTP请求报文和HTTP响应报文
4.node.js querystring类介绍
5.nodejs中流(stream)的理解
6.简单理解Socket

目标

  1. 理解JS 单线程、 Node.js EventLoop
  2. 理解Node.js事件模块

第一部分 前提 Nodejs EventLoop

1 【堆】【栈】【队列】

堆栈队列

任何一种语言的运行环境都少不了*堆(heap)栈(Stack)队列 (queue) * JS也不例外。

  1. JS的临时变量以及调用时的形参等等数据都是存储在中;
  2. 则是存储实际的对象;
  3. 对象的引用变量名(指针)也是在

而队列则是JS在实时运行环境中创建的消息队列或者事件队列。

JS是单线程,所以队列的实现让JS的异步处理有了可能性。

2 单线程、任务队列

尽信书不如无书,就喜欢这种有理有据的

摘录自【朴灵评注】JavaScript 运行机制详解:再谈Event Loop

为了避免复杂性,JS从一诞生,同一个时间只能做一件事
JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

异步执行的运行机制

(1)所有任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后继续执行后续的任务。
(3)一旦"执行栈"中的所有任务执行完毕,系统就会读取"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。
(4)主线程不断重复上面的第三步。

上面这段初步地在说event loop。但是异步跟event loop其实没有关系。准确的讲,【event loop是实现异步的一种机制】

【上面提到的一系列的手段其实就是实现异步的方法,其中就包括event loop。以及轮询、事件等。】
【所谓轮询:就是你在收银台付钱之后,坐到位置上不停的问服务员你的菜做好了没。】
【所谓(事件):就是你在收银台付钱之后,你不用不停的问,饭菜做好了服务员会自己告诉你。】

【JavaScript运行环境的运行机制,不是JavaScript的运行机制。】

3 事件循环

首先我们看一下nodejs本质结构图

image.png

Node.js 通过 libuv 来处理与操作系统的交互,并且因此具备了【异步、非阻塞、事件驱动】的能力。Node.js 实际上是 Javascript 执行线程的单线程,真正的I/O 操作,底层 API 调用都是通过多线程执行的。

本质也就是:任务接收是单线程,任务执行是多线程。

那么也就是主要依靠libuv,那么本文主要介绍Nodejs事件模块,当然离不开原理

为何是这样?

为何这么吊?

为何又出现那么多回调函数?

带着一些列问题搞明白了理论,至于代码,那也调用哪些大神写的API吧!

膜拜大神三秒钟

技术(艺术)源自生活、高于生活。对吧。

歪瓜仁真会玩

只要开始启动,那么这个姑娘就开始嗨起来了,其实我们事件循环也是这样的!从启动开始,就一直不停止的监听。

【论英文的重要性】

image.png

一图胜千言,但我还是就下图做个简单的梳理吧。我可不想被同为学渣的老铁们骂哈。

image.png

我们程序猿其实工作还是仅限于代码调用编写阶段,主要的核心在于理解内部原理,然后根据需求去编写(复制)代码。

讲个故事吧

JS单身狗(单线程)无异议,但它也是个贪心的黑心老板,在外边一直接活一直接活,根本无视
员工的死活,程序猿们为了改变世界(为了生存)不得不听产品经理的安排(程序猿心中的恶魔),
产品经理承上启下,一直分发任务(闲就不是代码狗了),而为了理想而奋战(别给我嘚瑟四点的晨曦,
老子刚下班)。
世界越来越美好(为奋战的人祈祷安康)!

Nodejs中是怎么个情况,请看下图

event5.png

好了,结束了,接下来就是代码啦。

第二部分 Events模块概述

1 Events模块

Events模块是Node对“发布/订阅”模式(publish/subscribe)的实现。一个对象通过这个模块,向另一个对象传递消息。

Node中的Event模块仅仅提供了一个对象: *EventEmitter, EventEmitter 的核心*就是事件触发与事件监听器功能的封装。

获取EventEmitter对象

//引用模块events, 点语法获取到EventEmitter
var EventEmitter = require('events').EventEmitter;
//初始化一个对象, 这个实例就是消息中心。
var emitter = new EventEmitter;

2 EventEmitter 实例对象的方法

2.1 emitter.on(eventName, listener), 监听事件,如果触发就调用回调函数

1. eventName <String> | <Symbol>: 事件名称, 后边可以跟上函数;
2. listener <Function> : 回调函数;

2.2 emitter.emit(eventName[, …args]), 根据eventName发送通知, 触发事件, 第一个参数为事件名称, 其余的参数会依次传入回调函数

以上两个方法的示例代码

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

//监听函数1, 事件名为--fun1
emitter.on('fun1', function(){
console.log('触发函数1');
});

//定义有参数函数fun2,
var fun2 = function(para){
console.log('触发函数2, 参数为' + para);
};
//监听fun2
emitter.on('fun2', fun2);

//触发事件名
emitter.emit('fun1');
//触发事件名并且传参数
emitter.emit('fun2', 'event');

打印结果为:

$ node 2on.js 
触发函数1
触发函数2, 参数为event

2.3 emitter.once(eventName, listener), 类似on方法, 但回调函数只是执行一次

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

emitter.once('oncefun', function(){
console.log('函数只执行一次');
});
emitter.emit('oncefun');
emitter.emit('oncefun');

console.log('--------')

emitter.on('fun', function(){
console.log('函数');
});
emitter.emit('fun');
emitter.emit('fun');

执行的结果为:

$ node 3once.js 
函数只执行一次
函数
函数

虽然触发多次oncefun, 但依然打印一次;与下边形成对比;,

2.4 emitter.addListener(eventName, listener)类似于emitter.on(eventName, listener)

2.5 emitter.removeListener(eventName, listener), 移除监听

详细见实例代码:

var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();

//定义一个函数
var removeFun = function(){
console.log('输出结果');
}
//以fun名称监听removeFun
emitter.on('fun', removeFun);
//每个30毫秒触发一次回调函数
setInterval(function(){
emitter.emit('fun');
}, 30);
//200毫秒以后触发回调函数
setTimeout(function(){
emitter.removeListener('fun', removeFun);
}, 200);

打印结果

$ node 5removeListener.js 
输出结果
输出结果
输出结果
输出结果
输出结果

#光标停止

######第三部分 更多API详见

语法名称觉得挺好的, 见名知意. 更多的语法就看Node官网吧 -__-

Event: 'newListener'
Event: 'removeListener'
EventEmitter.listenerCount(emitter, eventName)
EventEmitter.defaultMaxListeners
emitter.addListener(eventName, listener)
emitter.emit(eventName[, ...args])
emitter.eventNames()
emitter.getMaxListeners()
emitter.listenerCount(eventName)
emitter.listeners(eventName)
emitter.on(eventName, listener)
emitter.once(eventName, listener)
emitter.prependListener(eventName, listener)
emitter.prependOnceListener(eventName, listener)
emitter.removeAllListeners([eventName])
emitter.removeListener(eventName, listener)
emitter.setMaxListeners(n)