Quantcast
Channel: CodeSection,代码区,Python开发技术文章_教程 - CodeSec
Viewing all articles
Browse latest Browse all 9596

给深度学习入门者的Python快速上手教程之基础篇(02)

$
0
0
给深度学习入门者的python快速上手教程之基础篇(02)

一点号冷眼观AI3小时前

函数、生成器和类

还是从几个例子看起:

def say_hello: print('Hello!') def greetings(x='Good morning!'): print(x) say_hello # Hello! greetings # Good morning! greetings("What's up!") # What's up! a = greetings # 返回值是None def create_a_list(x, y=2, z=3): # 默认参数项必须放后面 return [x, y, z] b = create_a_list(1) # [1, 2, 3] c = create_a_list(3, 3) # [3, 3, 3] d = create_a_list(6, 7, 8) # [6, 7, 8] def traverse_args(*args): for arg in args: print(arg) traverse_args(1, 2, 3) # 依次打印1, 2, 3 traverse_args('A', 'B', 'C', 'D') # 依次打印A, B, C, D def traverse_kargs(**kwargs): for k, v in kwargs.items: print(k, v) traverse_kargs(x=3, y=4, z=5) # 依次打印('x', 3), ('y', 4), ('z', 5) traverse_kargs(fighter1='Fedor', fighter2='Randleman') def foo(x, y, *args, **kwargs): print(x, y) print(args) print(kwargs) # 第一个pring输出(1, 2) # 第二个print输出(3, 4, 5) # 第三个print输出{'a': 3, 'b': 'bar'} foo(1, 2, 3, 4, 5, a=6, b='bar')

其实和很多语言差不多,括号里面定义参数,参数可以有默认值,且默认值不能在无默认值参数之前。Python中的返回值用return定义,如果没有定义返回值,默认返回值是None。参数的定义可以非常灵活,可以有定义好的固定参数,也可以有可变长的参数(args: arguments)和关键字参数(kargs: keyword arguments)。如果要把这些参数都混用,则固定参数在最前,关键字参数在最后。

Python中万物皆对象,所以一些情况下函数也可以当成一个变量似的使用。比如前面小节中提到的用字典代替switch-case的用法,有的时候我们要执行的不是通过条件判断得到对应的变量,而是执行某个动作,比如有个小机器人在坐标(0, 0)处,我们用不同的动作控制小机器人移动:

moves = ['up', 'left', 'down', 'right'] coord = [0, 0] for move in moves: if move == 'up': # 向上,纵坐标+1 coord[1] += 1 elif move == 'down': # 向下,纵坐标-1 coord[1] -= 1 elif move == 'left': # 向左,横坐标-1 coord[0] -= 1 elif move == 'right': # 向右,横坐标+1 coord[0] += 1 else: pass print(coord) # 打印当前位置坐标

不同条件下对应的是对坐标这个列表中的值的操作,单纯的从字典取值就办不到了,所以就把函数作为字典的值,然后用这个得到的值执行相应动作:

moves = ['up', 'left', 'down', 'right'] def move_up(x): # 定义向上的操作 x[1] += 1 def move_down(x): # 定义向下的操作 x[1] -= 1 def move_left(x): # 定义向左的操作 x[0] -= 1 def move_right(x): # 定义向右的操作 x[0] += 1 # 动作和执行的函数关联起来,函数作为键对应的值 actions = { 'up': move_up, 'down': move_down, 'left': move_left, 'right': move_right } coord = [0, 0] for move in moves: actions[move](coord) print(coord)

把函数作为值取到后,直接加一括号就能使了,这样做之后起码在循环部分看上去很简洁。有点C里边函数指针的意思,只不过更简单。其实这种用法在之前讲排序的时候我们已经见过了,就是operator中的itemgetter。itemgetter(1)得到的是一个可调用对象(callable object),和返回下标为1的元素的函数用起来是一样的:

def get_val_at_pos_1(x): return x[1] heros = [ ('Superman', 99), ('Batman', 100), ('Joker', 85) ] sorted_pairs0 = sorted(heros, key=get_val_at_pos_1) sorted_pairs1 = sorted(heros, key=lambda x: x[1]) print(sorted_pairs0) print(sorted_pairs1)

在这个例子中我们用到了一种特殊的函数:lambda表达式。Lambda表达式在Python中是一种匿名函数,lambda关键字后面跟输入参数,然后冒号后面是返回值(的表达式),比如上边例子中就是一个取下标1元素的函数。当然,还是那句话,万物皆对象,给lambda表达式取名字也是一点问题没有的:

some_ops = lambda x, y: x + y + x*y + x**y some_ops(2, 3) # 2 + 3 + 2*3 + 2^3 = 19生成器(Generator)

生成器是迭代器的一种,形式上看和函数很像,只是把return换成了yield,在每次调用的时候,都会执行到yield并返回值,同时将当前状态保存,等待下次执行到yield再继续:

# 从10倒数到0 def countdown(x): while x >= 0: yield x x -= 1 for i in countdown(10): print(i) # 打印小于100的斐波那契数 def fibonacci(n): a = 0 b = 1 while b < n: yield b a, b = b, a + b for x in fibonacci(100): print(x)

生成器和所有可迭代结构一样,可以通过next函数返回下一个值,如果迭代结束了则抛出StopIteration异常:

a = fibonacci(3) print(next(a)) # 1 print(next(a)) # 1 print(next(a)) # 2 print(next(a)) # 抛出StopIteration异常

Python3.3以上可以允许yield和return同时使用,return的是异常的说明信息:

# Python3.3以上可以return返回异常的说明 def another_fibonacci(n): a = 0 b = 1 while b < n: yield b a, b = b, a + b return "No more ..." a = another_fibonacci(3) print(next(a)) # 1 print(next(a)) # 1 print(next(a)) # 2 print(next(a)) # 抛出StopIteration异常并打印No more消息

Python中的类的概念和其他语言相比没什么不同,比较特殊的是protected和private在Python中是没有明确限制的,一个惯例是用单下划线开头的表示protected,用双下划线开头的表示private:

class A: """Class A""" def __init__(self, x, y, name): self.x = x self.y = y self._name = name def introduce(self): print(self._name) def greeting(self): print("What's up!") def __l2norm(self): return self.x**2 + self.y**2 def cal_l2norm(self): return self.__l2norm a = A(11, 11, 'Leonardo') print(A.__doc__) # "Class A" a.introduce # "Leonardo" a.greeting # "What's up!" print(a._name) # 可以正常访问 print(a.cal_l2norm) # 输出11*11+11*11=242 print(a._A__l2norm) # 仍然可以访问,只是名字不一样 print(a.__l2norm) # 报错: 'A' object has no attribute '__l2norm'

类的初始化使用的是init(self,),所有成员变量都是self的,所以以self.开头。可以看到,单下划线开头的变量是可以直接访问的,而双下划线开头的变量则触发了Python中一种叫做name mangling的机制,其实就是名字变了下,仍然可以通过前边加上“_类名”的方式访问。也就是说Python中变量的访问权限都是靠自觉的。类定义中紧跟着类名字下一行的字符串叫做docstring,可以写一些用于描述类的介绍,如果有定义则通过“类名.doc”访问。这种前后都加双下划线访问的是特殊的变量/方法,除了doc和init还有很多,这里就不展开讲了。

Python中的继承也非常简单,最基本的继承方式就是定义类的时候把父类往括号里一放就行了:

class B(A): """Class B inheritenced from A""" def greeting(self): print("How's going!") b = B(12, 12, 'Flaubert') b.introduce # Flaubert b.greeting # How's going! print(b._name) # Flaubert print(b._A__l2norm) # “私有”方法,必须通过_A__l2norm访问

map可以用于对可遍历结构的每个元素执行同样的操作,批量操作:

map(lambda x: x**2, [1, 2, 3, 4]) # [1, 4, 9, 16] map(lambda x, y: x + y, [1, 2, 3], [5, 6, 7]) # [6, 8, 10]

reduce则是对可遍历结构的元素按顺序进行两个输入参数的操作,并且每次的结果保存作为下次操作的第一个输入参数,还没有遍历的元素作为第二个输入参数。这样的结果就是把一串可遍历的值,减少(reduce)成一个对象:

reduce(lambda x, y: x + y, [1, 2, 3, 4]) # ((1+2)+3)+4=10

filter顾名思义,根据条件对可遍历结构进行筛选:

filter(lambda x: x % 2, [1, 2, 3, 4, 5]) # 筛选奇数,[1, 3, 5]

需要注意的是,对于filter和map,在Python2中返回结果是列表,Python3中是生成器。

列表生成是Python2.0中加入的一种语法,可以非常方便地用来生成列表和迭代器,比如上节中map的两个例子和filter的一个例子可以用列表生成重写为:

[x**2 for x in [1, 2, 3, 4]] # [1, 4, 9 16] [sum(x) for x in zip([1, 2, 3], [5, 6, 7])] # [6, 8, 10] [x for x in [1, 2, 3, 4, 5] if x % 2] # [1, 3, 5]

zip函数可以把多个列表关联起来,这个例子中,通过zip可以按顺序同时输出两个列表对应位置的元素对。有一点需要注意的是,zip不会自动帮助判断两个列表是否长度一样,所以最终的结果会以短的列表为准,想要以长的列表为准的话可以考虑itertools模块中的izip_longest。如果要生成迭代器只需要把方括号换成括号,生成字典也非常容易:

iter_odd = (x for x in [1, 2, 3, 4, 5] if x % 2) print(type(iter_odd)) # <type 'generator'> square_dict = {x: x**2 for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

至于列表生成和map/filter应该优先用哪种,这个问题很难回答,不过Python创始人Guido似乎不喜欢map/filter/reduce,他曾在表示过一些从函数式编程里拿来的特性是个错误。

字符串

Python中字符串相关的处理都非常方便,来看例子:

a = 'Life is short, you need Python' a.lower # 'life is short, you need Python' a.upper # 'LIFE IS SHORT, YOU NEED PYTHON' a.count('i') # 2 a.find('e') # 从左向右查找'e',3 a.rfind('need') # 从右向左查找'need',19 a.replace('you', 'I') # 'Life is short, I need Python' tokens = a.split # ['Life', 'is', 'short,', 'you', 'need', 'Python'] b = ' '.join(tokens) # 用指定分隔符按顺序把字符串列表组合成新字符串 c = a + '\n' # 加了换行符,注意+用法是字符串作为序列的用法 c.rstrip # 右侧去除换行符 [x for x in a] # 遍历每个字符并生成由所有字符按顺序构成的列表 'Python' in a # True

Python2.6中引入了format进行字符串格式化,相比在字符串中用%的类似C的方式,更加强大方便:

a = 'I’m like a {} chasing {}.' # 按顺序格式化字符串,'I’m like a dog chasing cars.' a.format('dog', 'cars') # 在大括号中指定参数所在位置 b = 'I prefer {1} {0} to {2} {0}' b.format('food', 'Chinese', 'American') # >代表右对齐,>前是要填充的字符,依次输出: # 000001 # 000019 # 000256 for i in [1, 19, 256]: print('The index is {:0>6d}'.format(i)) # <代表左对齐,依次输出: # *--------- # ****------ # *******--- for x in ['*', '****', '*******']: progress_bar = '{:-<10}'.format(x) print(progress_bar) for x in [0.0001, 1e17, 3e-18]: print('{:.6f}'.format(x)) # 按照小数点后6位的浮点数格式 print('{:.1e}'.format(x)) # 按照小数点后1位的科学记数法格式 print ('{:g}'.format(x)) # 系统自动选择最合适的格式 template = '{name} is {age} years old.' c = template.format(name='Tom', age=8)) # Tom is 8 years old. d = template.format(age=7, name='Jerry')# Jerry is 7 years old.

format在生成字符串和文档的时候非常有用,更多更详细的用法可以参考Python官网:

7.1. string - Common string operations - Python 2.7.13 documentation

在Python中,推荐用上下文管理器(with-as)来打开文件,IO资源的管理更加安全,而且不用老惦记着给文件执行close函数。还是举例子来说明,考虑有个文件name_age.txt,里面存储着名字和年龄的关系,格式如下:

Tom,8 Jerry,7 Tyke,3 ...

读取文件内容并全部显示:

with open('name_age.txt', 'r') as f: # 打开文件,读取模式 lines = f.readlines # 一次读取所有行 for line in lines: # 按行格式化并显示信息 name, age = line.rstrip.split(',') print('{} is {} years old.'.format(name, age))

open的第一个参数是文件名,第二个参数是模式。文件的模式一般有四种,读取(r),写入(w),追加(a)和读写(r+)。如果希望按照二进制数据读取,则将文件模式和b一起使用(wb, r+b…)。

再考虑一个场景,要读取文件内容,并把年龄和名字的顺序交换存成新文件age_name.txt,这时可以同时打开两个文件:

with open('name_age.txt', 'r') as fread, open('age_name.txt', 'w') as fwrite: line = fread.readline while line: name, age = line.rstrip.split(',') fwrite.write('{},{}\n'.format(age, name)) line = fread.readline

有的时候我们进行文件操作是希望把对象进行序列化,那么可以考虑用pickle模块:

import pickle lines = [ "I'm like a dog chasing cars.", "I wouldn't know what to do if I caught one...", "I'd just do things." ] with open('lines.pkl', 'wb') as f: # 序列化并保存成文件 pickle.dump(lines, f) with open('lines.pkl', 'rb') as f: # 从文件读取并反序列化 lines_back = pickle.load(f) print(lines_back) # 和lines一样

注意到,序列化的时候就得使用b模式了。Python2中有个效率更高的pickle叫cPickle,用法和pickle一样,在Python3中就只有一个pickle。

相比起其他一些语言,在Python中我们可以更大胆地使用异常,因为异常在Python中是非常常见的存在,比如下面这种简单的遍历:

a = ['Why', 'so', 'serious', '?'] for x in a: print(x)

当用for进行遍历时,会对要遍历的对象调用iter。这需要给对象创建一个迭代器用来依次返回对象中的内容。为了能成功调用iter,该对象要么得支持迭代协议(定义iter),要么得支持序列协议(定义getitem)。当遍历结束时,iter或者getitem都需要抛出一个异常。iter会抛出StopIteration,而getitem会抛出IndexError,于是遍历就会停止。

在深度学习中,尤其是数据准备阶段,常常遇到IO操作。这时候遇到异常的可能性很高,采用异常处理可以保证数据处理的过程不被中断,并对有异常的情况进行记录或其他动作:

for filepath in filelist: # filelist中是文件路径的列表 try: with open(filepath, 'r') as f: # 执行数据处理的相关工作 ... print('{} is processed!'.format(filepath)) except IOError: print('{} with IOError!'.format(filepath)) # 异常的相应处理 ...

深度学习中对数据高效处理常常会需要并行,这时多进程就派上了用场。考虑这样一个场景,在数据准备阶段,有很多文件需要运行一定的预处理,正好有台多核服务器,我们希望把这些文件分成32份,并行处理:

from multiprocessing import Process#, freeze_support def process_data(filelist): for filepath in filelist: print('Processing {} ...'.format(filepath)) # 处理数据 ... if __name__ == '__main__': # 如果是在windows下,还需要加上freeze_support #freeze_support # full_list包含了要处理的全部文件列表 ... n_total = len(full_list) # 一个远大于32的数 n_processes = 32 # 每段子列表的平均长度 length = float(n_total) / float(n_processes) # 计算下标,尽可能均匀地划分输入文件列表 indices = [int(round(i*length)) for i in range(n_processes+1)] # 生成每个进程要处理的子文件列表 sublists = [full_list[indices[i]:indices[i+1]] for i in range(n_processes)] # 生成进程 processes = [Process(target=process_data, args=(x,)) for x in sublists] # 并行处理 for p in processes: p.start for p in processes: p.join

其中ifname== 'main'用来标明在import时不包含,但是作为文件执行时运行的语句块。为什么不用多线程呢?简单说就是Python中线程的并发无法有效利用多核,如果有兴趣的读者可以从下面这个链接看起:

GlobalInterpreterLock - Python Wiki

深度学习中的数据多是文件,所以数据处理阶段和文件相关的操作就非常重要。除了文件IO,Python中一些操作系统的相关功能也能够非常方便地帮助数据处理。想象一下我们有一个文件夹叫做data,下边有3个子文件夹叫做cat,dog和bat,里面分别是猫,狗和蝙蝠的照片。为了训练一个三分类模型,我们先要生成一个文件,里面每一行是文件的路径和对应的标签。定义cat是0,dog是1,bat是2,则可以通过如下脚本:

import os # 定义文件夹名称和标签的对应关系 label_map = { 'cat': 0, 'dog': 1, 'bat': 2 } with open('data.txt', 'w') as f: # 遍历所有文件,root为当前文件夹,dirs是所有子文件夹名,files是所有文件名 for root, dirs, files in os.walk('data'): for filename in files: filepath = os.sep.join([root, filename]) # 获得文件完整路径 dirname = root.split(os.sep)[-1] # 获取当前文件夹名称 label = label_map[dirname] # 得到标签 line = '{},{}\n'.format(filepath, label) f.write(line)

其中,os.sep是当前操作系统的路径分隔符,在Unix/linux中是'/',Windows中是'\'。有的时候我们已经有了所有的文件在一个文件夹data下,希望获取所有文件的名称,则可以用os.listdir:

filenames = os.listdir('data')

os也提供了诸如拷贝,移动和修改文件名等操作。同时因为大部分深度学习框架最常见的都是在Unix/Linux下使用,并且Unix/Linux的shell已经非常强大(比Windows好用太多),所以只需要用字符串格式化等方式生成shell命令的字符串,然后通过os.system就能方便实现很多功能,有时比os,还有Python中另一个操作系统相关模块shutil还要方便:

import os, shutil filepath0 = 'data/bat/IMG_000001.jpg' filepath1 = 'data/bat/IMG_000000.jpg' # 修改文件名 os.system('mv {} {}'.format(filepath0, filepath1)) #os.rename(filepath0, filepath1) # 创建文件夹 dirname = 'data_samples' os.system('mkdir -p {}'.format(dirname)) #if not os.path.exists(dirname): # os.mkdir(dirname) # 拷贝文件 os.system('cp {} {}'.format(filepath1, dirname)) #shutil.copy(filepath1, dirname)扫码入群

微信扫一扫,加入“冷眼观AI交流群”。入群你将获得:

向AI大牛直接技术咨询

获得AI专家的技术分享

与各AI同学进行切磋、交流


Viewing all articles
Browse latest Browse all 9596

Trending Articles