Picking up Python🏵

Get Started

Why not Python2 ?

  1. Python核心团队计划在2020年停止支持Python2
  2. Numpy项目发表声明,逐步淘汰对Python2的支持,并于2019.1.1起仅支持Python3
  3. Python3 做了大量的改进和优化,在各方面都有提升

Smallest Knowledge Scope

AI 领域的Python最小的学习范围

  • 类型 | 变量 |流程控制语句
  • 字符和编码
  • 数据容器
  • 函数式编程基础
  • 面向对象的视角理解Python的对象模型
  • 迭代器
  • 上下文管理器
  • Numpy 基础

Some small things

  • Linux 单指OS内核
  • Liunx发行版(Real OS) = Linux内核 + 应用软件
  • 跨硬件平台:最初支持x86架构的自由操作系统,目前已移植到更多的硬件平台
  • 思维方式:一切皆文件

Newbie quick start

输入和输出

  • print

    print(100 + 200) # 优先计算,默认都用空格分隔
  • input

    后面自带一个空格

    >>> name  = input('please enter your name:')
    please enter your name:David Qiao
    >>> print(name)
    'David Qiao'

数据类型

Python Data Type

常量 Constant

在Python中通常用全部大写的变量名表示常量

基本运算

# python3 中和数学上保持一致
>>> 3 / 2
1.5

# 向下取整
>>> 3 // 2
1

# 数据类型转变再运算
>>> 3.0 // 2
1.0

变量入门

  • 变量可以是任意数据类型,因而Python是动态类型语言

  • 使用变量之前必须先给变量赋值 必先赋值

List

List

运算

# List的加法(只同类型可相加)
------------List + List----------------------
L = [1, 2, 3]
L = L + L[-2::-1] # [1, 2, 3, 2, 1]
------------str + str----------------------
s = "David Qiao"
S = " Cindy"
str = s + S # "David Qiao Cindy"
------------str + List----------------------
TypeError: can only concatenate str (not "list") to str

# List的乘法:序列与整数相乘,可快速创建包含重复元素的序列
List & str

初始化

L = []
L = [None] * 5
L = [0] * 10

切片:充分发挥List的有序特性

[起始位置:终止位置:步长]

内置方法

in
>>> L = [1, 2, 3]
>>> 1 in L
True
len & max & min
List 与str的相互转换

join([List]) 将List里的元素用引号内容隔开,连接成一个长的完整的字符串

>>> s = "David Qiao"
# str分解
>>> l = list(s)
>>> l
['D', 'a', 'v', 'i', 'd', ' ', 'Q', 'i', 'a', 'o']
# list组合
>>> s_from_l = ''.join(l)
>>> s_from_l
'David Qiao'

元素 多对多赋值 & 删除

# 多对对赋值
>>> L = list(range(5))
>>> L
[0, 1, 2, 3, 4]
>>> L[0], L[-1] = L[-1], L[0]
>>> L
[4, 1, 2, 3, 0]

# 删除
>>> L
[4, 1, 2, 3, 0]
>>> del L[0]
>>> L
[1, 2, 3, 0]

切片骚操作

# 替换
>>> L = [1, 5]
>>> L[1:] = list(range(2, 5))
>>> L
[1, 2, 3, 4]
-------------------------------------
# 删除 - ①
>>> L
[1, 2, 3, 4]
>>> L[1:3] = []
>>> L
[1, 4]
# 删除 - ②
>>> L
[1, 4]
>>> del L[1:]
>>> L
[1]

range()函数

>>> list(range(1, 10, 2)) #
[1, 3, 5, 7, 9]

NOTES

  • 若超出范围,不报错

  • 不包含终止位置

  • 步长负数:轻松实现List倒序

    L[-1:0:-1] 或者 L[::-1]

多重List的浅拷贝与深拷贝

见下面的拷贝内容

计数函数:count

functools.reduce

functools.reduce
>>> from functools import reduce
>>> L = reduce(lambda x,y: x + y, [[i] * i for i in range(1, 6)])
>>> import random
>>> random.shuffle(L)
>>> L
[4, 5, 4, 5, 3, 5, 4, 5, 3, 2, 5, 4, 1, 2, 3]
>>> L.count(4)
4
>>> L.count(5)
5
>>> L.count(6)
0

排序

  • sorted(): 函数排序后返回,原序列不变
  • L.sort(): 就地排序,直接修改原列表
  • L.sort(): 对象的方法,不是函数,没有返回值

func() 这种单纯属于方法,一般情况是都是返回一个拷贝,并不影响输入

带’.’的这种属于对象的内置方法,就地操作自己

>>> x = L.sort()
>>> print(x)
None

List 的内置方法及其时间空间复杂度

Tuple

初始化后不可修改(? 有限制的)的List就是Tuple

有限制:若元素为可变List类

☞括号歧义

# Python 中括号优先(计算),其次才是Tuple
T = (1) # 优先计算为整数1
T = (1, ) # 真正的Tuple

☞可变

>>> T = ('David Qiao', [1, 2, 3])
>>> T[1].append(4) # NOTE! tuple has no attributes : append(List ONLY)

Dict

Python 中可变的KEY-VALUE形式的数据结构,查找速度极快

  • 用空间换时间策略,消耗内存大
  • 内部存放顺序与放入key的顺序无关
  • key必须是不可变对象

☞初始化

ExcellentStudents = {'David Qiao': 99, 'Cindy': 100}

☞用‘in’提前判断

>>> 'David Qiao' in ExcellentStudents # 🤪
True
>>> ExcellentStudents.keys()
dict_keys(['David Qiao', 'Cindy'])

☞ get

# get([key], return [not fund str / num(-1)])
>>> print(ExcellentStudents.get('David Qiao'))
99
>>> print(ExcellentStudents.get('Lucy'))
None
>>> print(ExcellentStudents.get('Tom', 'failure FIND'))
failure FIND

☞ key必须为不可变类型才可Hash 【list为可变类型】

>>> L = []
>>> ExcellentStudents[L] = 'List'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Set

相当于Dict中Key的集合

key必须hashable(唯一) –> Set中无重复的Key

>>> L = ['David Qiao', 'David Qiao', 'Cindy']
>>> print(L)
['David Qiao', 'David Qiao', 'Cindy']
>>> S = set(L)
>>> print(S)
{'Cindy', 'David Qiao'}

☞ 容错性

>>> print(S)
{'Cindy', 'David Qiao'}
# 增加重复元素
>>> S.add('Cindy')
>>> print(S)
{'Cindy', 'David Qiao'}
# remove没有容错性
>>> S.remove('Tom')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Tom'
# discard有容错性
>>> print(S)
{'Cindy', 'David Qiao'}
>>> S.discard('Tom')
>>> S
{'Cindy', 'David Qiao'}

☞ 集合set 不可存有可变类型

>>> S.add([1, 2, 3])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

raw string

在使用正则表达式的时候使用raw string非常方便

>>> print("David Qiao \nlove \n'Cindy'")
David Qiao
love
'Cindy'

>>> print(r"David Qiao \nlove \n'Cindy'")
David Qiao \nlove \n'Cindy'

三引号

常用于多行输入或多行字符或多行注释

>>> print('''David Qiao
... love
... Cindy
... ''')
David Qiao
love
Cindy

字符编码

  • ASCII编码省空间,但是容易出现乱码(只能使用英文)
  • Unicode统一了各种语言的编码,存在大量空间冗余(几乎支持所有的语言)
  • UTF-8 可变长的Unicode编码,大部分ASCII,其他动态改变
  • ASCII 可认为是UTF-8的一部分

字符编码的常见工作模式

  • 内存中:Unicode
  • 存储时:UTF-8
  • 传输时:UTF-8

存储、传输时:考虑空间和带宽

存储在硬盘的时候是UTF-8(省空间),读入到内存的时候转成Unicode编码(便于处理),处理完之后,再写回硬盘中,用UTF-8

String

内存中默认是str类型(Unicode编码)

存储、传输:以字节为单位的bytes类型(字节流)

>>> print("'David Qiao' is from 中国🇨🇳 ")
'David Qiao' is from 中国🇨🇳
>>> type("'David Qiao' is from 中国🇨🇳 ")
<class 'str'> # Unicode编码
>>> print(b"'David Qiao' is from 中国🇨🇳 ")
  File "<stdin>", line 1
SyntaxError: bytes can only contain ASCII literal characters.

# 有了'b'之后,先用bytes对str进行编码(有中文不可以)
>>>print(b"David Qiao")  
b'David Qiao'
>>> type(b"David Qiao")
<class 'bytes'>
# Difference:
# b->print的时候会全部都打印出来(包括‘b'),
# str类型的时候:只打印字符串内容

字符串的编解码(decode & encode)

  • 纯英文可用ASCII将str编码为bytes
  • 含有中文则可用UTF-8将str编码为bytes
  • 从网络或者磁盘上读取的字节流为bytes
  • 进入内存总之为Unicode编码了
# 关注下下面的代码吧👇
>>> s_u = "David Qiao"
>>> s_b = b"David Qiao"
>>> type(s_u)
<class 'str'>
>>> type(s_b)
<class 'bytes'>

>>> type(s_u.encode('ascii'))
<class 'bytes'>
>>> type(s_b.decode('ascii'))
<class 'str'>
>>> print(s_b.decode('ascii'))
David Qiao
>>> s_cn_u = "David Qiao is from 中国"
>>> print(s_cn_u.encode('ascii'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 19-20: ordinal not in range(128)
>>> print(s_cn_u.encode('utf-8'))
b'David Qiao is from \xe4\xb8\xad\xe5\x9b\xbd'
# 注意一定要在decode的str前加'b'
>>> print('David Qiao is from \xe4\xb8\xad\xe5\x9b\xbd'.decode('utf-8'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'decode'
>>> print(b'David Qiao is from \xe4\xb8\xad\xe5\x9b\xbd'.decode('utf-8'))
David Qiao is from 中国

拷贝

初见

可变类型对象的赋值,传递的是引用,类似C语言当中的指针

>>> L = ['b', 'a', 'c']
>>> print(sorted(L)) # sorted(L) 并未改变L
['a', 'b', 'c']
>>> print(L)
['b', 'a', 'c']
# L.sort() 改变了L
>>> L.sort()
>>> print(L)
['a', 'b', 'c']

# str.replace()
>>> s = "David"
>>> print(s.replace('D', 'd'))
david

若不想传递引用,需要使用拷贝的方式

>>> a = ['David Qiao', 'Cindy']
>>> b = a[:]
>>> id(a) == id(b)
False

多重List的浅拷贝和深拷贝

只需要参考下面的这个例子👇

import copy

a = [[1, 2, 3], [4, 5, 6]] # 本质:a存储两个地址
b = a # 只是将地址传给了b --> 相当于引用
c = copy.copy(a)
d = copy.deepcopy(a)

a.append(7)
a[1][2] = 10

print('原数组:', a) # [[1, 2, 3], [4, 5, 10], 7]
print('引用赋值:', b) # [[1, 2, 3], [4, 5, 10], 7]
print('浅拷贝:', c) # [[1, 2, 3], [4, 5, 10], 7]
# 元素拿出来,可变对象转变为不可变对象
print('深拷贝:', d) # [[1, 2, 3], [4, 5, 6]]

---------------------------------------------------------
import copy
a = [[1, 2, 3], [4, 5, 6]]
b = copy.deepcopy(a[1])
print(b) # [4, 5, 6]

循环

☞range

>>> list(range(5))
[0, 1, 2, 3, 4]

函数

内置函数

>>> my_func = max
>>> my_func([1, 2, 3])
3

自定义函数

def my_max(a, b):
  pass # 可以先定义一个函数,而不去执行

if __name__ == '__main__': # 有这个语句可以将后面的语句当成一个脚本进行运行
  # 否则作为模块被import到另一个python脚本当中
  assert my_max(1, 2) == 2 # assert 先判断这个是True然后继续往后执行,否则报错
  assert my_max(4, 3) == 4 # assert 多用于测试上面的函数

返回多个值

def mySort(a, b):
  if a > b:
    return a, b # 数据容器,拆包
  else:
    return b, a
if __name__ == '__main__':
  assert mySort(1, 2) == (2, 1)
  assert mySort(4, 3) == (4, 3)
  print("OK")

默认参数

def myMax(a, b = 0):
  if a > b:
    return a
  else:
    return b

if __name__ == "__main__":
  assert myMax(1, 2) == 2
  assert myMax(3) == 3

默认参数的记忆性

默认参数在函数定义的时候就已被计算并冻结,因此默认参数一般指向不变对象

可变对象一般都是引用,冻结的就是地址,地址指向的位置可以随便改的

函数没有办法保证原子性,修改默认参数的话,默认参数是会变的

>>> def add_David(L = []): # 在函数定义的时候,默认参数L的地址已被分配确定,使用默认参数的时候,自动在这个地址上进行后续的操作
...     L.append('David')
...     return L
...
>>> add_David()
['David']
>>> add_David()
['David', 'David']
>>> add_David()
['David', 'David', 'David']

传入任意个数的参数

def calc_sum(*numbers): # 当成List
    sum = 0;
    for n in numbers:
        sum += n
    return sum

if __name__ == "__main__":
    test_func = calc_sum
    assert test_func() == 0
    assert test_func(3) == 3
    assert test_func(1, 3, 5) == 9
    print("OK")

Python中的面向对象

class Student(object):

    def __init__(self, name, score = -1):
        self.__name = name
        self.__score = score
        self.say_hi()

    def name(self):
        return self.__name

    def say_hi(self):
        if self.__score < 0:
            print("{}: Hi, my name is {}. I'm a new student.".format(self.__name, self.__name))
        else:
            print("{}: Hi, my name is {}. My score is {}.".format(self.__name, self.__name, self.__score))

    def get_score(self, teacher, score):
        self.score = score
        print("{}: teacher {} just gave me a {}".format(self.__name, teacher, score))

class Teacher(object):

    def __init__(self, name):
        self.__name = name
        self.say_hi()

    def say_hi(self):
            print("{}: Hi, my name is {}. I'm a teacher .".format(self.__name, self.__name))

    def score(self, student, score):
        student.get_score(self.__name, score)
        print("{}: I just gave {} a {}".format(self.__name, student.name(), score))

if __name__ == '__main__':
    studentA = Student("A")
    teacherB = Teacher("B")
    teacherB.score(studentA, 80)

Python 代码的组织-模块

充分复用

包->模块->类 / 功能模块

每个包里面都含有一个__init__.py文件,而且必须存在,用以区分普通目录还是包

创建包或者模块的时候,不可与系统自带的包或者模块重名

使用现成的模块封装一个自己模块🤙

'first module' # 封装模块的惯例:1️⃣用第一行表示封装模块的功能

__author__ = 'David Qiao' # 2️⃣第二行声明下版权,就是谁写的这个模块

# 下面就是开始模块的正文

import sys

def say_hi():
     args = sys.argv # 实时动态的获取参数
     if len(args) == 1:
        print("Welcome to David Qiao's website!")
     elif len(args) ==2:
        print("Hi, %s, Welcome to David Qiao's website!!" % args[1])
     else:
        print("Too many arguments")


if __name__ == "__main__":
    say_hi()
DIY module test

包管理工具 pip

三个步骤:搜索->下载->安装

pip version pip List

解释器默认搜索🔍路径

sys.path

>>> import sys
>>> print(sys.path)
['', '/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python37.zip', '/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7', '/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/site-packages', '/usr/local/lib/python3.7/site-packages/pyecharts-0.5.11-py3.7.egg', '/usr/local/lib/python3.7/site-packages']
>>> print('\n'.join(sys.path))

/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages
/usr/local/lib/python3.7/site-packages/pyecharts-0.5.11-py3.7.egg
/usr/local/lib/python3.7/site-packages

有效期只限于当前环境的解释器下,一旦退出即失效

>>> sys.path.append('/Users/apple')
>>> print('\n'.join(sys.path))

/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python37.zip
/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7
/usr/local/Cellar/python/3.7.0/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload
/usr/local/lib/python3.7/site-packages
/usr/local/lib/python3.7/site-packages/pyecharts-0.5.11-py3.7.egg
/usr/local/lib/python3.7/site-packages
/Users/apple

Function & Functional Programming

代码块

  • 代码块靠缩进建立,最好是4个空格,而非制表符
  • 代码块由: 来引导,有缩进的提示终止
  • 代码块里可以放条件、循环语句,函数、类定义上下文管理器

函数的定义

  • 函数是一个代码块
  • def 定义
  • 返回一个值(任何情况都是)
  • Python中调用符号:()
  • Python中可调用判断函数:callable
  • 函数中的return 语句:可省略;可单独出现(不跟任何东西);可返回多个变量(终止与返回值)
  • 放在函数定义开头的字符串称为docstring,作为函数的一部分(__doc__)
  • 交互式解释器中的help函数

文档字符串

>>> def print_david():
...     print("David Qiao")
...
>>> help(print_david)
👇 会刷屏,包括哪个module,已经调用方法,按下Q会返回交互式解释器
help
>>> def print_david():
...     '''JUST A FUNCTION'''
...     print("David Qiao")
...
>>> help(print_david)
Help on function print_david in module __main__:

print_david()
    JUST A FUNCTION
(END)
>>> print_david.__doc__
'JUST A FUNCTION'
>>> print_david.__doc__ = "just a function" # 同时说明,python的函数也是一个对象,Function类的实例

同时说明,python的函数也是一个对象,Function类的实例

Help on function print_david in module __main__:

print_david()
    just a function
(END)

函数中的return 语句

# 省略return
def david_1():
    pass
# 单独出现,后面不跟任何东西
def david_2():
    return
# return None
def david_3():
    return None
# 返回一个数字
def david_4():
    return 4
# 返回多个变量(而且是不同类型的)
def david_5():
    return 1, 2, 3, 4, "5"
# eval 把一个str当作一个命令去执行
if __name__ == "__main__":
    print([eval("david_" + str(x) + "()") for x in range(1, 6)])
前几个都为缺省状态
[None, None, None, 4, (1, 2, 3, 4, '5')] # python只能返回一个值,隐式转换为Tuple

List comprehension 列表生成式

>>> sum([x for x in range(101) if x % 2 == 0]) # 高斯求和
2550

>>> sum([1 if x % 3 == 2 else -1 if x % 3 == 1 else 0 for x in range(101)])
-1
[1 if x % 3 == 2 else -1 if x % 3 == 1 else 0 for x in range(101)]

函数参数

形参和实参

  • 定义函数的时候是形参,使用函数的时候传递的是实参
  • 形参尽量传递不可变类型(str,Tuple, 数字…)

参数的传递

>>> def david(a, b):
    a.append(1)
    b.append(2)
    print(a, b)
    return a + b

>>> a, b = [1], [1]
>>> david(a, b)
[1, 1], [1, 1]
[1, 1, 1, 1]
>>>print(a, b)
[1, 1], [1, 1] # 已经发生了改变
  • 实参传递本质上是赋值(可变和不可变对象)

  • 可变对象赋值实质为地址

  • 提出问题:如何传递一个可变对象的同时不希望函数被修改?

      david(a[:], b[:]) # 用切片的方式创建一个副本

    嵌套的List使用copy.deepcopy

位置参数和关键字参数

  • 参数位置解耦(参数和它的位置解开,参数代表什么意义就代表什么意义,无论它们在哪个位置)
  • 默认参数设置

位置参数:不管你参数名字叫什么,只看你的位置

# 变量名字和它的意义没有什么关系,只看它传进来的位置而已
>>> def say_hi(say, name):
...     print(say + ' ' + name + '!')
...
>>> def say_hi_2(name, say):
...     print(name + ' ' + say + '!')
...
>>> say_hi('hello', 'David Qiao')
hello David Qiao!
>>> say_hi_2('hello', 'David Qiao')
hello David Qiao!

有什么不好?如果有很多位置参数?

关键词参数: 参数列表出现了谁等于谁的情况

>>> say_hi(name = 'David Qiao', say = "hello") # say_hi(say, name)
hello David Qiao!

位置(参数顺序)就不重要了,重要的是参数名和它的意义

只要变量名字取的好,就不担心它的位置了

更重要的用途是用来设置默认值,尤其是在大型的项目当中

功能完整的函数:主逻辑用位置参数,配置选项用关键字参数

def__init__(self,
    featurewise_center=False,
            samplewise_center=False,
            featurewise_std_normalization=False,
            samplewise_std_normalization=False,
            zca_whitening=False,
            zca_epsilon=1e-6,
            rotation_range=0,
            width_shift_range=0.,
            height_shift_range=0.,
            brightness_range=None,
            shear_range=0.,zoom_range=0.,
            channel_shift_range=0.,
            fill_mode='nearest',
            cval=0.,
            horizontal_flip=False,
            vertical_flip=False,
            rescale=None,
            preprocessing_function=None,
            data_format='channels_last',
            validation_split=0.0,dtype='float32'):

选自Keras 的预处理库

建议:

  • 最好不同时使用多个位置参数与多个关键字参数
  • 如果使用了关键字参数,未知参数越少越好,并且集中放在最前面(最好是一个/最多两个位置参数)
  • 尽量减少位置参数,最好没有

任意数量量的参数与Python中的星号

先来看一个函数,怎么实现这个函数

>>> sum(1, 2)
3
>>> sum(1, 2, 3)
6
>>> sum(1, 2, 3, 4)
10
···

如何接受任意个参数星号

def sum(*l): # '*'含义就是:将传入的多个参数全部打个包,放入一个'L'的 List当中
    result = 0
    for x in l: result += x
    return result

与赋值不同的是,函数的参数用星号拆包或者缝合的时候,得到的是元组

# 赋值的时候(用list在收集的): 缝合的时候类型是list
>>> list(range(7))
[0, 1, 2, 3, 4, 5, 6]
>>> a, *b, c = list(range(7))
>>> type(b)
<class 'list'>
>>> a
0
>>> b
[1, 2, 3, 4, 5]
>>> c
6

# 函数传入参数的时候(用Tuple来收集的): 缝合的时候类型是tuple
>>> def sum(*l):
...     print("l's type: ", type(l))
...     print("l is:", l)
...
>>> sum(1, 2)
l's type:  <class 'tuple'>
l is: (1, 2) # kw(keyword)是Tuple类型

还可以接受任意个关键字参数两个星号

>>> def register(**kw):
...     print("kw's type:", type(kw))
...     print("kw is:", kw)
...
>>> register(name = "David Qiao", age = 22)
kw's type: <class 'dict'>
kw is: {'name': 'David Qiao', 'age': 22} # kw是字典类型

可以一起收集位置参数与关键字参数

>>> def register(*tuple, **dict):
...     print(tuple)
...     print(dict)
...
>>> register(1, 2, 3, name = "David Qiao", age = 22)
(1, 2, 3)
{'name': 'David Qiao', 'age': 22}

星号拆包

>>> def sum(a, b, c):
···    return a + b + c
···
>>> x = (1, 2, 3)
>>> sum(*x)
6
>>> sum(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sum() missing 2 required positional arguments: 'b' and 'c'

对于函数参数中的星号拆包和封包,用一个即可,用两个其实等于没用

>>> def sum_1(*l): # 封包:接收多个参数输入
...     result = 0
...     for x in l: result += x
...     return result
...
>>> def sum_2(l):
...     result = 0
...     for x in l: result += x
...     return result
...
>>> sum_1(*range(5)) # 拆包,拆成很多输入值
10
>>> sum_2(range(5))
10

NameSpace and Scope Resolution 名称空间与作用域解析

What is Namespace?

  • A namespace is a mapping from names to objects. 名称到对象的一组映射
  • Most namespaces are currently implemented as Python dictionaries. 被字典映射

链式作用域

What is scope?

  • Namespace can exist independently from each other

  • They are structured in a certain hiearchy 结构化的包含等级关系的一组结构(Built-in 解释器)

    LEGB
  • The scope in Python defines the hierarchy level 链式作用域定义了层级结构

  • in which we search namespaces for certain “name-to-object” mappings

  • Scope resolution for variable names via the LEGB rule 作用域解析

作用域的产生

  • Local: function and class method 局部作用于函数&类方法定义(审查)的时候
  • Enclosed: its enclosing functioin, if a function is wrapped inside another function 嵌套作用域
  • Global: the uppermost level of executing script itself 全局作用域-> 模块层面的 import的时候就产生了,(执行脚本的时候)
  • Built-in: special names that Python reserves for itself Python解释器启动的时候,built-in的作用域就已经有了

作用域的查看(globals()locals())

>>> print(globals()) # 本来就是mapping(映射),用dict去表达
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
>>> print(globals().keys())
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__'])
>>> print('\n'.join(globals().keys()))
__name__
__doc__
__package__
__loader__
__spec__
__annotations__
__builtins__
>>> david_qiao = "🐯"
>>> print('\n'.join(globals().keys()))
__name__
__doc__
__package__
__loader__
__spec__
__annotations__
__builtins__
david_qiao # 人为在模块级别重新定义了一个变量,多了一个在作用域里的变量
>>> david_cindy = "💑" # david_cindy
>>> import math # 模块名:cindy
>>> def say_hi(): pass # 函数名:say_hi
...
>>> class Love: pass # 类名:Love
...
>>> print('\n'.join(globals().keys()))
__name__
__doc__
__package__
__loader__
__spec__
__annotations__
__builtins__
david_qiao
david_cindy
math
say_hi
Love
from copy import copy

globals_init = copy(list(globals().keys()))
print("globals_init: ", *globals_init, sep = "\n") # same: '\n'.join(globals_init)
print()

a, b = 1, 2

def f1(c = 3):
    d = 4

    print("globals_in_f1", *(globals().keys() - globals_init))
    print("locals_in_f1", *(locals().keys()))

    def f2(e = 5):
        f = None

        print("globals_in_f2", *(globals().keys() - globals_init))
        print("locals_in_f2", *(locals().keys()))

    f2()

if __name__ == "__main__":
    f1()

作用域间变量的遮盖

···此处有省略

Global

def f1():
    global name # s
    name = "David Qiao"

if __name__ == "__main__":

    name = "Cindy"
    f1()
    print(name)
David Qiao

Local

Local

nonlocal

作用域的生命周期

  • built-in: 解释器在则在,否则反之
  • global: 导入模块时创建,直到解释器退出
  • local: 函数调用时才创建
  • nonlocal: 详见👇(闭包)

函数式编程

综述

Python之所以可以使用函数式编程的范式,它的基本是:函数式一个一等对象,

函数式编程的特征:大量使用高阶函数(函数作为参数传给了另外一个函数)

匿名函数也是服务于高阶函数的(传入的函数可能只使用一次,没有必要起名字,是最简化的)

Python用列表生成式 + 迭代器 + 函数式编程 模式混合起来,可以用一行代码解决很多问题

  • 前提:函数在Python中是一等对象(何为一等对象?将在👇讲解)
  • 工具: built-in高阶函数;lambda函数;operator模块;functools模块
  • 模式:闭包 & 装饰器
  • 替代: 用List Comprehension可轻松替代map & filterreduce规约函数[向量经过函数变成一个值(降维)]替代起来比较困难)
  • 原则: No Side Effect

何为No Side Effect?

函数的所有功能就仅仅是返回一个新的值而已,没有其他的行为,尤其是不得修改外部变量

因而,各个独立的部分的执行顺序可以随意打乱,带来执行顺序上的自由

执行顺序的自由使得一系列新的特性得以实现:无锁的并发惰性求值编译器级别的性能优化(多线程上计算上的优化)等

程序状态与命令式编程

  • 程序的状态首先包含了当前定义的全部变量
  • 有了程序的状态,我们的程序才能不断向前推进
  • 命令式编程,就是通过不断修改变量的值,来保存当前运行的状态,来步步推进

函数式编程

  • 通过函数来保存程序的状态(通过函数创建新的参数和返回值来保存状态)
  • 函数一层层的叠加起来,每个函数的参数或返回值代表了一个中间状态( n * f(n) )
  • 命令式编程里一次变量值的修改,在函数式编程里编程了一个函数的转换
  • 最自然的方式:递归

一等函数

一等对象
  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

Python 中所有的函数都是一等对象,简称为一等函数

💨什么是运行时

在运行时创建

>>> def say_hi(): print("David Qiao")
...
>>> say_hi()
David Qiao

能赋值给变量或数据结构中的元素

>>> test = say_hi
>>> test()
David Qiao

能作为参数传递给函数

>>> def repeat(f, time):
...     [f() for i in range(time)]
...
>>> repeat(say_hi, 3)
David Qiao
David Qiao
David Qiao

能作为函数的返回结果

>>> def repeat_func(f, num):
...     def new_func():
...         [f() for i in range(num)]
...     return new_func
...
>>> repeated_func = repeat_func(say_hi, 3)
>>> repeated_func()
David Qiao
David Qiao
David Qiao

高阶函数

  • 定义: 接受函数为参数,或把函数作为返回结果的函数
map高阶函数

可用 [x * x for i in range(10)]替代

# 把一个函数映射到一个可迭代对象
# 每一个迭代对象都用这个行为去操作下,返回一个可迭代对象
# 可用 [x * x for i in range(10)]替代
>>> def square(x): return x * x
...
>>> xx = map(square, range(10))
>>> xx
<map object at 0x108fb57b8>
>>> xx = list(xx)
>>> xx
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
filter高阶函数

可用[i for i in x if bool(i)]替代

>>> x = [(), [], {}, None, '', False, 0, True, 1, 2, -3]
>>> x_result = filter(bool, x)
>>> x_result
<filter object at 0x108fb57b8>
>>> x_result = list(x_result)
>>> x_result
[True, 1, 2, -3]
reduce高阶函数

不好用List去替代:规约函数

# 不要看这里的功能很鸡肋,在复杂的程序的时候可以实现很强大的功能
>>> def multiply(a, b): return a * b # 操纵两个变量,返回一个值
...
>>> from functools import reduce
>>> reduce(multiply, range(1, 5)) # 1 * 2 * 3 * 4
24
sorted高阶函数
>>> sorted([x * (-1) ** x for x in range(10)])
[-9, -7, -5, -3, -1, 0, 2, 4, 6, 8]
>>> sorted([x * (-1) ** x for x in range(10)], reverse = True)
[8, 6, 4, 2, 0, -1, -3, -5, -7, -9]
# key参数是关键:先进行abs处理之后,然后再sorted
>>> sorted([x * (-1) ** x for x in range(10)], key = abs)
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]
>>> sorted([x * (-1) ** x for x in range(10)], reverse = True, key = abs)
[-9, 8, -7, 6, -5, 4, -3, 2, -1, 0]
partial 高阶函数

参数非常多的时候,锁死某些参数

>>> from functools import partial
>>> abs_sorted = partial(sorted, key = abs)
>>> abs_sorted([x * (-1) ** x for x in range(10)])
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9]
>>> abs_sorte = partial(sorted, reverse = True, key = abs)

>>> abs_sorted = partial(sorted, reverse = True, key = abs)
>>> abs_sorted([x * (-1) ** x for x in range(10)])
[-9, 8, -7, 6, -5, 4, -3, 2, -1, 0]

匿名函数

  • 定义:使用lambda表达式创建的函数,函数本身没有名字
  • 特点:只能使用纯表达式,不能赋值,不能使用while & try 等块语句
  • 语法:lambda [arg1 [, arg2 [, arg3...]]]: expression

什么是表达式?

Expressions get a value; Statements do something.

lambda 与用def的区别在哪?

  • 写法上:
    • def可以用代码块,一个代码块包含多个语句
    • lambda只能用单行表达式,而表达式仅仅是单个语句中的一种
  • 结果上:
    • def语句一定会增加一个函数名称
    • lambda不会,这就降低了变量名污染的风险

Compound Statements > Simple Statements > Expressions

代码块(复合语句) > 单行语句 > 表达式

能用一个表达式直接放到return里返回的函数都可以用lambda速写

>>> def multiply(a, b): return a * b
...
>>> multiply(3, 4)
12
>>> multiply_by_lambda = lambda x, y: x * y
>>> multiply_by_lambda(3, 4)
12

lambda函数一般作为高阶函数的参数

List + lambda 可以得到 🐼行为列表

>>> f_list = [lambda x: x + 1, lambda x: x ** 2, lambda x: x ** 3] # 3个操作(行为)函数列表
>>> [f_list[j](10) for j in range(3)]
[11, 100, 1000]

惰性计算

生成器 + 可迭代器

>>> f_list = [lambda x: x ** i for i in range(5)]
>>> [f_list[j](10) for j in range(5)]
[10000, 10000, 10000, 10000, 10000]

Closure

概述

  • 装饰器的本质是一个闭包,而@ 仅仅是一个语法糖🍬
  • 闭包的基础是Python中的函数是一等对象
  • 理解闭包需要知道Python如何识别变量所处的作用域
  • 自定义变量所处的作用域有三种:global nonlocal local

再谈变量作用域- 4个维度

  • 从内层函数的角度看,变量使用的两个维度

    • 能否访问:LEGB规则(从里向外按照规则只要找到就可读)
    • 能否修改:需要声明才能修改 global nonlocal local
  • 变量作用域识别三要素

    • 出现位置:在哪里访问
    • 赋值位置:在哪里赋值
    • 声明类型:在哪里声明了
  • 三种变量作用域

    • 局部:local
    • 全局:global
    • 非全局:nonlocal
a = 1
print(a)

def func_enclosed():
    global a
    a = 2
    print(a)

    def func_local():
        nonlocal a
        a = 3
        print(a)

    func_local()
    print(a)

func_enclosed()
print(a)
  File "Untitled.py", line 200
    nonlocal a
    ^
SyntaxError: no binding for nonlocal 'a' found

为什么会有nonlocal

  • nonlocal填补了global & local之间的空白
  • nonlocal的出现其实也是一种权衡利弊的结果:私有之安全封装,全局之灵活共享
  • 这也是闭包出现的原因

定义

  • 延伸了作用域的函数(能访问定义体之外定义的非全局变量
    • local作用域下还可以访问nonlocal层 🐒那么,local层就是一个闭包

从一个avg函数说起:

>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

Version -1: 用类实现 🐥格外臃肿

class Averager():

    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total / len(self.series)
>>> from test import Averger
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

Version -2: 用闭包实现(实现了简洁,又表达了语义) 🐥Mini-Class

每调用一次averager,就将new_value添加到series里面-> 相当于local层的变量可以调用nonlocal层的变量,将里面层的参数添加到外面层里去了,并且它会隐式的存起来,一直保存着,直到local层的函数被销毁

series 生命周期和averager的生命周期是一样的

def make_averager():
    series = [] # nonlocal

    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total / len(series)

    return averager
>>> avg = make_averager() # already runned
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

总结:闭包是一种函数,它会保留定义函数时存在的外层非全局变量的绑定

Version -3: 优化过的闭包

Python2中无nonlocal关键字,只能用可变对象来临时性的解决中间层变量修改的问题

nonlocal是Python3中引入的一个官方的解决方案,以弥补内层函数无法修改中间层不可变对象

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        nonlocal count, total # very important!!
        total += new_value
        count += 1
        return total / count

    return averager

if __name__ == "__main__":
    avg = make_averager()
    print(avg(200))
    print(avg(300))

闭包有什么用呢?

  1. 共享变量的时候避免使用了不安全的全局变量
  2. 允许将函数与某些数据关联起来,类似于简化版面向对象编程
  3. 相同代码每次生成的闭包,其延伸的作用域都彼此独立(计数器,注册表)每一次都相当于创建一个新的Mini-Class
  4. 函数的一部分行为在编写时无法预知,需要动态实现,同时又想保持接口的一致性 外层函数参数比内层现有参数多,需要用户自己去定义参数的时候
  5. 较低的内存开销:类(解释器起止 贯穿解释器生命周期)的生命周期远大于闭包(生成开始到最后一次调用)
  6. 实现装饰器(也可使用非闭包)

Decorator

Python四大神器之一(位列榜首):装饰器、迭代器、生成器、上下文管理器

Why?为什么要使用装饰器(概述)

  • 名称管理(为了不引入多余的变量名)
  • 显示调用
  • 就近原则
  • 充分复用

不引入多余的变量

  • 有时候,写一个闭包,仅仅是为了增强一个函数的功能
  • 功能增强完了之后,只对增强了功能的最终函数感兴趣
  • 装饰前的函数引用就变得多余
  • 因而出现了func = decorated_by(func)这种即席覆盖的写法
def decorate(func):
    func.__doc__ += '\nDecorated by decoreate---David Qiao'
    return func

def add(x, y)
    '''Return the sum of x and y'''
    return x + y

decorated_add = decorate(add) # 即席覆盖:我们只想要装饰后的函数,装饰前的函数没有用了

分析:

  • 引入了新的变量名decorated_add
  • 原来的add函数一般情况下后面的程序中不会用到了,我们更多的只关注decorated_add

改进版一:

def decorate(func):
    func.__doc__ += '\nDecorated by decoreate---David Qiao'
    return func

def add(x, y)
    '''Return the sum of x and y'''
    return x + y
add = decorate(add) # 仅仅只是变化了一个变量名的问题

分析:

  • 没有新的变量名的引入,变量名使用效率提升
  • 装饰函数的执行代码需要单独调用,可能不符合就近原则

显示调用,就近原则

  • 装饰的次数多了,这种方式依旧显得多余(每次都得去add = decorate(add)
  • 而且会带来新的问题:在哪写这句比较好(decorate 和 被装饰函数怎么分布)
  • 为了编码更加优雅,保持显示调用,遵守就近原则,出现了@语法糖🍬

改进版二:@出现

def decorate(func):
    func.__doc__ += '\nDecorated by decoreate---David Qiao'
    return func

@decorate # add = decorate(add)
def add(x, y)
    '''Return the sum of x and y'''
    return x + y

分析:

  • 通过语法糖,保证了装饰过程与原函数彼此之间的独立性
  • 同时,还保证了两者代码之间的就近原则,形成一个有机的整体
  • 问题:装饰定义函数与被装饰函数在同一个模块中实现,影响了复用效率

分层封装,充分复用

改进版三:装饰器被单独封装在一个模块中:

引用 模块:DecorateToolBox.py

class DecorateToolBox:

    @classmethod # 不需要实例化就可以直接通过这个类调用这个方法
    def decorate(self, func):
        func.__doc__ += '\nDecorated by decoreate---David Qiao'
        return func

主模块:test.py

from DecorationToolBox import DecorateToolBox

@DecorateToolBox.decorate
def add(x, y):
    '''Return the sum of x and y.'''
    return x + y

启动Python解释器:

>>> from test import add
>>> help(add)
Help on function add in module test:

add(a, b)
    Return the sum of x and y.
    Decorated by decorate.
(END)

分析:

  • 将不同级别的功能模块封装在不同的文件中,是编写大型程序的基础
  • 实现一个装饰器,并不一定需要写出一个闭包
  • 类可以不依赖一个实体而直接调用其方法,得益于classmethod装饰器

总结

  • 避免重复,充分复用:装饰器的模块化使程序避免重复的前置和收尾代码
  • 显示调用,就近原则:装饰器是显式的,并且在需要装饰器的函数中即席使用

What?什么是装饰器?

  • 一个可调用的对象(输入一个函数,返回一个函数,在Python中函数也是一个对象),以某种方式增强函数式的功能(并不是修改函数对象)
  • 装饰器是一个语法糖🍬,在源码中标记函数(此源码指编译后的源码)—> 回过头来再看👀
  • 解释器解析源码的时候将被装饰的函数作为第一个位置参数传给装饰器(将@下方的函数的地址直接传入到装饰器第一个参数,单输入单返回值函数)
  • 装饰器可能会直接处理被装饰函数,然后返回它(一般仅修改属性,不修改代码)
  • 装饰器也可能用一个新的函数或可调用对象替换被装饰函数(但核心功能一般不变)
  • 装饰器仅仅看着像闭包,其实功能的定位与闭包有重合也有很大区别
  • 装饰器模式的本质是元编程:在运行时改变程序行为(只有在运行时才动态修改函数的功能)
  • 装饰器的一个不可忽视的特性:在模块加载时立即执行(解释器一开始先把所有的Python源代码过一遍,先把它编译成字节码,之后才开始一行行执行,但是对于装饰器,在第一次过的时候已经动态地把装饰器已经执行过了,然后把执行完的函数,直接替换过去,直接在编译成字节码的时候,就已经是使用过装饰器,已经增强过功能的函数了)
  • 装饰器是可以堆叠的,自底向上逐个装饰
  • 装饰器可以带参数的,但此时至少要写两个装饰器(使用闭包)
  • 装饰器的更加Pythonic的实现方式其实是在类中实现__call__()方法

装饰器的堆叠

def deco_1(func):
    print("running deco_1...")
    return func

def deco_2(func):
    print("running deco_2...")
    return func
# 在字节码编译阶段,调用deco_2替换,然后成一个新的函数,调用deco_1进行替换,最后在运行时运行替换后的函数
@deco_1
@deco_2
def f():
    print("running f...")

if __name__ == "__main__":

    f()
running deco_2...
running deco_1...
running f...

装饰器在导入时立即执行

def deco_1(func):
    print("running deco_1...")
    return func

def deco_2(func):
    print("running deco_2...")
    return func

@deco_1
@deco_2
def f():
    print("running f...")

if __name__ == "__main__":

    pass
running deco_2...
running deco_1...

带参数的装饰器

既然装饰器只能接受一个位置参数,并且是被动的接受解释器传来的函数引用,那么如何实现带参数的装饰器?

🤩问题分析:

  1. 限制条件一:装饰器本身只能接受一个位置参数
  2. 限制条件二:这个位置参数已经被装饰函数的引用占据了
  3. 问题目标:希望装饰器能够使用外部传入的其他的参数
  4. 推论:装饰器需要访问或者修改外部参数

三种备选方案:

  1. 在装饰器内访问全局不可变对象,若需要修改,则使用global声明(不安全)
  2. 在装饰器内访问外部可变对象(不安全)
  3. 让装饰器成为闭包的返回(较安全)

方案:编写一个闭包,接受外部参数,返回一个装饰器

参数化之前的装饰器:

registry = set() # 定义一个注册表
def register(func): # 装饰器:register(注册)
    registry.add(func)
    return func

# 想象这里的()是一种调用 ,’f1'是一种引用
@register
def f1():
    print("running f1.")

@register
def f2():
    print("running f2.")


def f3():
    print("running f3.")

def main():
    f1()
    f2()
    f3()

if __name__ == "__main__":

    print(registry)
    main()
{<function f2 at 0x106868ae8>, <function f1 at 0x106868a60>}
running f1.
running f2.
running f3.

🦁参数化之后的装饰器

registry = set()
# 整体就是一个闭包,里面是个装饰器,输入一个函数,返回一个函数
# 里面根据外面的参数,执行相应的操作
def register(flag = True):
    def decorate(func): # 装饰器特征:输入一个函数
        if flag:
            registry.add(func)
        else:
            registry.discard(func)
        return func # 装饰器特征:返回一个函数

    return decorate
# 用‘()’调用了register这个闭包,返回来一个装饰器,整体是一个装饰器
# 通过一个小括号的调用,不同调用方式(传入不同的参数),返回不同的装饰器,相当于实现了带参数的装饰器
# 其实不是一个带参数的装饰器,其实是一个带参数的闭包,闭包返回了一个装饰器,传给了‘@’
# ‘@’基于返回的装饰器对于下面的函数进行装饰
@register() # 'register()' -> decorate[return] (装饰器)根据flag参数,返回不同的装饰器
def f1():
    print("running f1.")
# 不要认为‘()’是专属于函数的一部分,应当认为‘()’是函数的调用,相当于实现'__call__'
@register(False)
def f2():
    print("running f2.")

@register(True)
def f3():
    print("running f3.")

def main():
    f1()
    f2()
    f3()

if __name__ == "__main__":

    print(registry)
    main()
{<function f3 at 0x109e70c80>, <function f1 at 0x109e70b70>}
running f1.
running f2.
running f3.

分析:

  • 此时,register变量被使用了两次
  • 第一次是后面的调用:()(调用之后才变成一个装饰器)
  • 第二次是前面的装饰:@(装饰器复合仅能用于装饰器)

注意: register不是装饰器; regsiter() 或者 register(False)才是

看到‘@’后面函数名有’()’了,已经不是装饰器了,是一个返回装饰器的一个闭包

How?装饰器怎么用?

  • 从模仿开始

    从开源社区里找哪里用到了装饰器,研究下为什么要使用装饰器,发现用的不错的话,可以记到笔记📒上面,在下次遇到类似场景的时候模仿下

常见使用场景

  • 运行前处理:如确认用户授权

    比如有一个函数完成的是功能模块,传一些用户的信息,可以写一个装饰器去统一处理用户信息是否符合相应的授权(一个功能是读取某个数据,但是同时把用户信息也传了过来,我们可以写一个装饰器,先去把用户信息先去我的注册表里查一下这个用户是否具有相关的读取权限,如果有的话继续往后执行,否则在此处还没有读取的时候就踢回去了,抛出一个异常,该用户并不具有读取权限)

  • 运行时注册:如注册信号系统

    Flask框架-URL的注册(轻量级的编写API、编写一个web后端的一个框架,它里面的主要的URL,去编写URL的API的时候,把URL作为一个字符串,传给了一个参数,这个参数相当于一个闭包的参数,这个闭包会返回一个装饰器,这个装饰器把我写API里面执行的功能注册到注册表里面去了,后续的话,我在启动API服务的时候,Flask框架的核心层就会通过注册表里注册过的所有的URL统一的实现调度)

  • 运行后清理:如序列化返回值(AI 开发,数据分析中常用场景

    正常情况下我的一个功能:比如我输入一个数据,输出一个数据,输入数据的时候,一般情况下我们会写成Python语言原生的一个数据结构List,比如输入一个List,一般情况下我会返回List甚至返回一个Dict都是可以的,但是有的时候我需要把这样的一个功能函数,把输出变成一个json,把它序列化成一个Json的字符串,这样的情况下,你其实没有必要把这个函数写成好几个版本,其实仅仅需要写一个装饰器,就是负责这个函数执行完后把这个结果返回出来,把结果直接用json.dumps(string)变成一个字符串,再返回出来,用装饰器临时把这个函数的返回值改成了序列化,用json序列化后的字符串

注册或者授权机制

(往往和web应用开发相关 - api / 网页的后端)

  • 函数的注册,参考上面的例子(为了实现统一的管理)
  • 将某个功能注册到某个地方(可以统一访问的一个路由上去),比如Flask框架中的URL的注册,相当把URL通过装饰器的方式作为路由关联到某个功能上来,通过这个路由,找到某个功能
  • 比如验证身份信息或加密信息(执行某个数据读取或者写入操作),以确定是否继续后续的运算后者操作(执行某个数据的读取操作或者数据写入操作,我需要先确认它的身份是否是合法的是否具有相关的权限 | 先把函数功能写好,再把相关的写成装饰器,用这个装饰器装饰下这个函数,那么这个函数就具有验证身份信息的功能了,这个时候需要把带有验证信息的功能注册到某个URL上面的话,可在这个装饰器上面再写个装饰器,这个装饰器可以传一个参数,就是一个装饰器上再加一个带参数的装饰器的闭包 )
  • 比如查询一个身份信息是否已经被注册过,如果没有则注册,如果有则直接返回账户信息

参数的数据验证或者清洗

(往往和数据清洗或者异常处理相关)

我们可以强行对输入参数进行特殊限制:

def require_ints(func):
    def temp_func(*args):
        if not all([isinstance(arg, int) for arg in args]):
            raise TypeError("{} only accepts integers as arguments.".format(func.__name__))
        return func(*args)
    return temp_func

def add(x, y):
    return x + y

@require_ints
def require_ints_add(x, y):
    return x + y

if __name__ == "__main__":
    print(add(1.0, 2.0))
    print(require_ints_add(1.0, 2.0))
3.0
Traceback (most recent call last):
  File "Untitled.py", line 369, in <module>
    print(require_ints_add(1.0, 2.0))
  File "Untitled.py", line 356, in temp_func
    raise TypeError("{} only accepts integers as arguments.".format(func.__name__))
TypeError: require_ints_add only accepts integers as arguments.

复用核心计算模块,仅改变输出方式

让原本返回Python原生数据结构的函数变成输出JSON结构(跨平台跨语言):

import json

def json_output(func):
    def temp_func(*args, **kw):
        result = func(*args, **kw)
        return json.dumps(result)
    return temp_func

def generate_a_dict(x):
    return  {str(i): i**2 for i in range(x)}

@json_output
def generate_a_dict_json_output(x):
    return {str(i): i**2 for i in range(x)}

if __name__ == "__main__":
    a, b = generate_a_dict(5), generate_a_dict_json_output(5)
    print(a, type(a))
    print(b, type(b))
{'0': 0, '1': 1, '2': 4, '3': 9, '4': 16} <class 'dict'>
{"0": 0, "1": 1, "2": 4, "3": 9, "4": 16} <class 'str'> # json处理后的单引号变双引号

总结

  • 装饰器作为Python四神器之一,功能非常强大(还有迭代器,生成器,上下文管理器)
    • 读入数据文件的时候,需要一个句柄,这个时候需要一个上下文管理器去封装读文件这个操作,当跳出这个上下文管理器的时候,就释放引用了
    • 当正负样本比例完全失衡的时候需要一个方式去补充下正样本或者负样本,这种情况下,用迭代器去封装成一个生成器,或者直接写成一个生成器,很轻松的变成一个无限的循环(封装成一个无限循环的生成器),每执行一次就返回一组数据,非常适合用online training的方式,动态的生成正负样本数据去训练,用生成器的方式很巧妙地动态修改你到底想传给他什么样的数据,或者以什么样的顺序传给它
  • 装饰器(总之就是动态修改函数功能)用好了可以让程序复用性大大提高,代码也会变的非常优雅
  • 要理解装饰器,需要先理解一等函数,变量作用域,函数式编程,闭包(扩展了装饰器,用闭包去返回一个装饰器,用装饰器去修改函数的功能,用两层的方式去实现可动态的装饰器)
    • Python虽然支持函数式编程,虽然函数式编程也很强大,但是Python是一种面向对象的语言,并不支持函数式编程的所有功能,仅仅支持部分函数式编程的功能,而且是用面向对象的方式实现的函数式编程,本身并不是一个函数式编程语言
  • 要用好装饰器,要先从模仿开始,多读别人的代码,多模仿,慢慢就有感觉了
  • Python内置了很多的装饰器,功能都很强大,编写框架或大型程序的时候你一定可以用到

Object-oriented Programming(OOP)

OKR

  • 抛开Python语言,理解面向对象编程

  • 用Python进行面向对象编程

  • 用Special Method实现Pythonic面向对象编程

Key Result

当你看完本章的内容,如果可以清晰地叙述出下面几个问题,你就可以继续往下看啦🤗

  1. 能用面向过程的编程思维去解决问题或完成任务
  2. 能阐述清楚面向对象与对象与面向过程的区别
  3. 能清楚的对问题背景进行分析,在面向过程与面向对象之间作出合适的选择
  4. 当决定使用面向对象设计时,能遵守面向对象设计的通用原则

什么是面向过程,什么是面向对象?

面向过程式编程(符合人类思考方式的一种编程模式)

  1. 来了一个任务
  2. 对任务进行流程分解,得到任务的不同阶段
  3. 对不同阶段内的子任务分解,知道可以用分支语句和循环语句分解到几个表达式为止
  4. 对不同子任务连接在一起,共同完成总任务
  5. 识别重复性工作,抽象出基于任务或功能的函数,对整个程序进行重构
  6. 实现功能函数(func)与执行过程(main)分离
  7. 基础功能不变的情况下,下一个任务中可以复用的是函数

Closure

面向对象式编程

  1. 来一个任务
  2. 找到任务中所有的利益相关方,并对利益相关方进行归类
  3. 找到每个利益相关方需要履行的职责和希望被满足的需求,并进行分类
  4. 将每个利益相关方类别的属性量化, 并定义明确的行为,抽象出一个类别
  5. 将所有同一类的利益相关方用共同的类模板进行实例化
  6. 实现类别(class)与执行任务(main)的分离
  7. 识别不同类别之间的关系,梳理衍生关系
  8. 基于各种不同类别之间的衍生关系,抽象出基础类别(基类)
  9. 将所有类别重构至每个类别都基于各自的基类层层继承而来
  10. 利益相关方不变的情况下,下一个任务中可以复用的是抽象基类和各种派生类(不需要看代码)

举例说明

任务:训练一个基于深度卷积神经网络的人脸识别模型,并将其封装成可调用API部署上线

🧐基于两种不同的思维方式:

基于过程分解出子任务:

  1. 找到合适的用于训练模型的数据库
  2. 定义模型的结构,loss函数,优化方法
  3. 根据模型的输入输出,将数据库制作为train,validation,test三个数据集
  4. 多次训练模型,寻找合适的超参数
  5. 找到训练出的最优方案,封装成RESTful API部署上线

基于对象分解出利益相关方

  1. DataSet 数据:收集,验证,转换,加载,切分,增强
  2. Model (看Keras源码很好)模型:结构,loss,优化器,超参数,训练流程,评估,使用
  3. 基础设施:模型定义环境(调试small set),模型训练环境(硬件设备),模型部署环境(偏运维Docker/ TensorFlow Service RPC / Restful 端到端支持)

面向对象与面向过程的区别

  • 面向过程思考的出发点是事情本身
  • 面向对象思考的出发点是假设所有事物都有生命,他们之间会怎么分工协作
  • 两种编程思路无明显优劣之分,一切只看适不适合
  • 评估方法:预测未来,找到复用概率最高的几个点
  • 将这几个点用于下面的原则进行初步评估:高内聚,低耦合
  • 按照以上方法评估之后,心中往往已经有了答案

一般而言:

  • 注重复用和可维护性,OOP多数要胜出的
  • 注重短期开发速度,而且是一次性的,面向过程肯定是首选

举个例子,一下两种场景,选择肯定是有区别的:

  1. 未来一两年内都要做人脸识别研究;
  2. 在一家创业公司,做人脸识别模型仅仅是老板或者产品经理拍脑袋想出来的…

比设计模式更重要的是设计原则

面向对象设计的目标

  1. 可扩展:新特性很容易添加到现有系统中,基本不影响系统原有功能
  2. 可修改:当修改某一部分代码时,不会影响到其他不相关的部分
  3. 可替代:用具有相同接口的代码去替换系统中一部分代码的时候,系统不受影响

以上三点就是用来检测软件设计是否合理的要素

面向对象设计的SOLID原则

  1. 单一职责原则:设计出来的每一个类,只有一个引起这个类变化的原因
  2. 开闭原则:对扩展开放,对修改封闭
  3. 替换原则:父类能用的功能,换成子类一样可以用
  4. 接口隔离原则:接口设计要按需供给(类似微服务设计)
  5. 依赖倒置原则:抽象不依赖于细节,细节应该依赖于抽象(针对接口编程)

遵循以上原则去设计面向对象程序,效果一般不会很差

AI(场景) + Python(语言) + OOP(编程模式):

  • AI: 业务导向不明显,需求变动频率较低,实现和复现频率较高
  • Python:虽然是一门面向对象语言,但与传统的面向对象并不相同(Python强调多态)
  • OOP:使用Python时,并不需要深入学习OOP或者OOD那些理论

用Python这门面向对象语言去做AI开发,只需要两句话:

高内聚,低耦合

抽象不变的接口,封装变化的细节

OOP in Python

  • 掌握OOP在Python语言中的基本语法,能用Python编写面向对象风格的程序
  • 清楚知道OOP的几个重要特性及目的(Python重多态)

面向对象的Python的实现

类的创建

class Model:
    pass

def main():
    model = Model()
    print(model)

if __name__ == "__main__":
    main()
<__main__.Model object at 0x100c13470> # '__main__'这个模块,'Model'这个类的实例化对象,内存中的位置

要点:

  1. 类名一般大写:实例化出来的对象,名称一般小写
  2. 类在被定义时,也创建了一个局部作用域
  3. 类名加上()生成对象,说明类被定义后便可以调用
  4. 本质上讲,类本身就是一个对象

类的数据绑定:

 class Model:
    name = "CNN"

def main():
    print(Model.name)

    model = Model() # 使用默认的init函数
    print(model.name)

    model.name = "RNN"
    print(Model.name)
    print(model.name)

if __name__ == "__main__":
    main()
CNN
CNN
CNN
RNN

要点:

  1. 类定义体中,可以自己定义自己的属性,并通过.来引用
  2. 类实例化出对象以后,创建了新的局部作用域,也就是对象自己的作用域
  3. 对实例化出来的对象引用属性时,先从自己的作用域找,未找到则向上找,这里找到了类作用域

读的时候层层上找,写的时候只能在自己的作用域里面

  1. 实例化出来的对象是可以运行时绑定数据的

类的自定义实例化:__init__()

class Model:
    name = "DNN"

    def __init__(self, name):
        self.name = name

def main():
    cnnmodel = Model('CNN')
    rnnmodel = Model('RNN')
    print(Model.name, cnnmodel.name, rnnmodel.name)
    cnnmodel.name, rnnmodel.name = "RNN", "CNN" # 说明初始化完之后,变量仍然可以被修改的
    print(Model.name, cnnmodel.name, rnnmodel.name)

if __name__ == "__main__":
    main()
DNN CNN RNN
DNN RNN CNN

要点:

  1. 类定义体中, self指代实例化出来的对象(用了self的都是绑定到具体对象上的)
  2. 没有跟在self后面的属性属于类属性
  3. 可以使用__init__()函数自定义初始化方式
  4. 隶属于累的方法是共享的Model.name,隶属于对象的方式是每个对象私有的具体实例化的对象.name

对象方法

class Model:
    name = 'DNN'

    def __init__(self, name):
        self.name = name

    def print_name(self): # 若参数列表有‘self’,则表示只能由初始化对象来调用该方法
        print(self.name) 

def main():
    cnnmodel = Model("CNN")
    cnnmodel.print_name()
    cnnmodel.name = "RNN"
    cnnmodel.print_name()

if __name__ == "__main__":
    main()
CNN
RNN

要点:

  1. 类里的functionmethod
  2. 类定义体中的方法在默认情况下隶属于对象,而非类本身
  3. 直接在类上调用方法时会报错

NOTES:

cnnmodel.print_name() 等价于 Model.print_name(cnnmodel)

那么有没有隶属于类自己的方法呢?👇

类方法

class Model:
    name = "DNN"

    def __init__(self, name):
        self.name = name

    def print_name(self):
        print(self.name)

    @classmethod
    def print_cls_name(cls):
        print(cls.name)


def main():
    Model.print_cls_name()

    cnnmodel = Model("CNN")
    cnnmodel.print_name()
    cnnmodel.name = "RNN"
    cnnmodel.print_name()

    Model.print_cls_name()

if __name__ == "__main__":
    main()
DNN
CNN
RNN
DNN

要点:

  1. 使用@classmethodcls可以直接将方法绑定到类本身,不用初始化就可以调用类方法
  2. 同时:若想将几个函数功能相同,或者有相同的作用对象(或者不需要实例化出来实例对象)的函数绑定在一起,同时还可以传入参数

属性封装

class Model:
    __name = "DNN"

    def __init__(self, name):
        self.__name = name 

    def print_name(self):
        print(self.__name)

    @classmethod 
    def print_cls_name(cls):
        print(cls.__name)

def main():
    Model.print_cls_name()
    cnnmodel = Model("CNN")
    cnnmodel.print_name()
    # Python访问限制:其实就是一个名称替换,双下划线前加个类名,类名前面再加个单下划线
    # Python封装不彻底
    print(Model.__name)
    # print(cnnmodel.__name)

if __name__ == "__main__":
    main()
AttributeError: type object 'Model' has no attribute '__name' # type object
AttributeError: 'Model' object has no attribute '__name' # 'Model' object

要点:

  1. 通过双下划线开头,可以将数据属性私有化,对于method一样适用
  2. 从报错信息也能看出来,Model是一个type objectcnnmodel是一个Model object
  3. Python中的私有化是假的,本质是做了一次名称替换,因此十几种也有为了方便调试而适用单下划线的情况,而私有化全凭自觉了

继承(隐式实例化)

Binding

要点:

  1. 如果子类没有定义自己的__init__()函数,则隐式调用父类的
  2. 子类可以使用父类中定义的所有属性和方法,但类方法的行为需要注意
  3. 使用了@classmethod后的方法虽然可以继承,但是方法里面的cls参数绑定了父类,即使在子类中调用了类方法,但通过cls引用的属性依旧是父类的类属性

继承(显式实例化)

class Model:
    __name = "DNN"
    def __init__(self, name):
        self.__name = name 

    def print_name(self):
        print(self.__name)

    @classmethod 
    def print_class_name(cls):
        print(cls.__name)

class CNNModel(Model):
    __name = "CNN"

    def __init__(self, name, layer_num):
        Model.__init__(self, name)
        self.__layer_num = layer_num

    def print_layer_num(self):
        print(self.__layer_num)

if __name__ == "__main__":
    cnnmodel = CNNModel("CNN", 5)
    cnnmodel.print_name()
    cnnmodel.print_class_name()
    CNNModel.print_class_name()
    cnnmodel.print_layer_num()
CNN
DNN
DNN
5

如果子类中定义了__init__()函数,必须显示执行父类的初始化

多态

Pythonic 多态是语言上的多态,写很多多态和语言无缝衔接,而不是编程模式上的

class Model:
    __name = "DNN"

    def __init__(self, name):
        __name = name 

    def print_name(self):
        print(self.__name)

    @classmethod 
    def print_class_method(cls):
        print(cls.__name)

class CNNModel(Model):
    __name = "CNN"

    def __init__(self, name, layer_num):
        Model.__init__(self, name)
        self.__layer_num = layer_num

    def print_name(self):
        print(self.__name) # 保留了父函数
        self.print_layer_num() # 新加了自己实现的方法的调用

    def print_layer_num(self):
        print("Layer Num:", self.__layer_num)

class RNNModel(Model):
    __name = "RNN"

    def __init__(self, name, nn_type):
        Model.__init__(self, name)
        self.__nn_type = nn_type

    def print_name(self):
        print(self.__name)
        self.print_nn_type()

    def print_nn_type(self):
        print("NN Type:", self.__nn_type)
# 多态的实现(偏传统:聚焦了同一条继承树上的三个不同的类)
# Python的多态不需要来自同一个继承树上,甚至是只要是个类,只要实现了print_name方法,那么Python就可以实现
def print_model(model):
    model.print_name()

if __name__ == "__main__":
    model = Model("DNN")
    cnnmodel = CNNModel("CNN", 5)
    rnnmodel = RNNModel("RNN", "LSTM")
    [print_model(m) for m in [model, cnnmodel, rnnmodel]] # 不同类型对象 相同的方法print_name 得到各自期望的输出
DNN
CNN
Layer Num: 5
RNN
NN Type: LSTM

要点:

  1. 多态的设计就是要完成对于不同类型对象使用相同的方法调用能得到各自期望的输出
  2. 在数据封装、继承和多态中,多态是Python设计的核心,也叫鸭子🦆类型

面向对象的重要特性总结:

  • 封装:只需要我能做什么,不需要知道我怎么做的
  • 继承:纵向复用,增加更多的属性和功能
  • 多态:横向复用,在父类既有的属性和功能进一步加强丰富(更多在重载层面的,重新实现一个函数、运算符)

Python的设计初衷是强调多态,也就是所谓的鸭子模型(聚焦于功能,而不是继承和关系),明白了Python的设计初衷之后,我们编写的Python代码要在一定程度上避免以下三种情况(虽然写成这样也是可以的):

  1. C语言般的过程式编程
  2. C++或者Java般的传统的面向对象编程
  3. Lisp般的函数式编程

Pythonic OOP

OKR

  • 建立更加Pythonic的对OOP的认识
  • 在更深层次认识Python这门语言
  • 为后续的迭代器、生成器、上下文管理器打下坚实的基础

KEY RESULT

  • 建立认知:Python中一切都是Object
  • 建立认知:什么是Special Method,从Special Method的设计看Python的Duck Typing
  • 学会使用:用@property管理对象属性的访问
  • 建立认知:Cross-Cutting:从Multi-Inheritance以及Decorator看Python中Mixln的设计

关于双下划线

怎么读

  • _name: single underscore name
  • __name: double underscore name
  • __name__: dunder name
  • __init__(): dunder init method (function)

双下划线开头和结尾的变量或方法:

  • 类别:special; magic; dunder
  • 实体:attribute; method

如何认识Python的special method(摘自官方文档)

  • special method: method with special name (dunder)
  • Why use it? A class can implement certain operations that are invoked by special syntax
  • original intention of design: operator overloading

Allowing classes to define their own behavor with respect to language operators

  • 个人补充:尽可能的保证Python行为的一致性(Special Method更像是一种协议)

从语言设计层面理解Python的数据模型

一切都是对象

  • Python的数据模型是Objects
  • Python的程序中所有数据都是用objects 或者 objects之间的关系表示的
  • 甚至Python的代码都是objects

Objects的组成一:identity

  • Python中变量存的都是它的引用,从identity找到所在的值

  • objects创建后,identity再也不会改变直到被销毁

  • id()is关注的就是一个object的identity

要点:

  1. 变量存的是创建的object的identity
  2. 创建出来的不同的object有不同的identity
  3. 变量的id变了不是因为object的identity变了,而是对应的object变了
  4. 对于immutable object, 计算结果如果已经存在可直接返回相同的identity

Object的组成二:type

  • objects创建后,type也不会改变
  • type()函数返回一个Object的type
  • type决定了一个object支持哪些运算,可能的值在什么范围内

Object的组成三:value

  • 有些Object的value是可以改变的: mutable object
  • 有些Object的value是不可以改变的: immutable object
  • 需要注意当一个Object是个container的情况
  • 一个Object的type决定了它是否mutable

存放其他Object的reference的Object:Container

  • 当我们聚焦于Container的values时,我们关注的是value
  • 当我们聚焦于Container的mutability时,关注的是identity

Pythonic OOP with Special Method and Attribute

The implicit superclass - object & type object
  • 每一个class在定义的时候如果没有继承,都会隐式继承object这个superclass
  • 每一个自定义的class在Python中都是一个type object
class X: # class X(object):
    pass

class Y(X):
    pass

if __name__ == "__main__":
    x = X()
    y = Y()
    print(x.__class__.__name__) # 从type实例化出来的东西是具体的某个类:X 
    print(X.__class__.__name__) # 从具体实例化出来的类(X)实例化出来的对象:x
    # 👇 print(x.__class__.__base__.__name__) |  '__base__' 继承自...
    print(X.__base__.__name__) # X作为类,继承自object
    print(X.__class__.__name__)# X作为对象,是一个type object,type也是继承自object
    print(X.__class__.__base__.__name__) # 🌠
X
type
object
type
object
print(X.__class__.__base__.__name__)

   Reprint policy


《Picking up Python🏵》 by David Qiao is licensed under a Creative Commons Attribution 4.0 International License
  TOC