python相关内容的学习

1、在将两个字符串连接的时候,我是直接用的的+号来连接两个字符串,其中字符串为路径的一部分,例如‘f:\test\test\test’+‘test.txt’,一直提示错误:SyntaxError: EOL while scanning string literal。这个问题原来是因为路径中用了正斜杠。在使用正斜杠的时候需要两个正斜杠或者使用一个反斜杠。
1、%matplotlib inline是一个魔法函数(官方中的定义:IPython有一组预先定义好的所谓的魔法函数,经常在jupyter notebook中用到),它的意思是将那些用matplotlib绘制的图像显示在页面里而不是弹出一个窗口。

函数

函数参数

必选参数在前,默认参数在后,不然Python的解释器会报错;定义默认参数要牢记一点:默认参数必须指向不变对象

可变参数

在参数前面加一个号,可以传入多个参数,如果传入的是list或tuple类型,Python允许在list或tuple前面加一个号,把list或tuple的元素变成可变参数传进去。

关键字参数

关键字参数允许传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict传给kw参数。如果传入的是字典类型的参数a,kw获得的是a的一份拷贝,对kw的改动不会影响到函数外的a字典数据。

命名关键字参数

如果要限制关键字参数的名字,就可以用命名关键字参数,不至于传入任意的关键字参数。和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。

进阶特性

迭代

迭代:对list或tuple的for循环遍历称为迭代(Iteration)。默认情况下,字典迭代的是key,如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k,v in d.items()。字符串也可以for循环迭代,只要是可迭代对象,for循环就可以正常运行,而不用太关心该对象是list还是其他数据类型。
如何判断一个对象是否可迭代?方法是通过collections的iterable判断。
from collections import Iterable
isinstance(‘abcdefg’,Iterable) #判断str是否可迭代。

列表生成式

列表生成式可以用一行语句代替循环生成list:[xx for x in range(1,11)],输出结果[1,4,9,16,25,36,49,64,81,100], for循环后面还可以加上if判断,[xx for x in range(1,11) if x % 2 == 0],[4,16,36,64,100],这样就可以筛选出值是偶数的平方。
还可以使用两层循环,可以生成全排列:[m + n for m in ‘ABC’ for n in ‘XYZ’],[‘AX’,‘AY’,‘AZ’,‘BX’,‘BY’,‘BZ’,‘CX’,‘CY’,‘CZ’]。
如果是字典,也可以使用d.items()同时迭代key和value。
在一个列表生成式中,for前面的if。。。else是表达式,而for后面的if是过滤条件,不能加else。

生成器

通过列表生成式,可以直接创建一个列表。如果创建一个几百万的列表,会占用很大的存储空间,如果仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。如果列表元素可以按照某种算法推算出来,那是否可以在循环的过程中不断推算出后续的元素?这样就不必创建完成的list。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:g = (x*x for x in range(10))。list可以一次性打印出list的每一个元素,而对于generator需要通过next(g)获得下一个元素值。generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到没有更多的元素时,抛出StopIteration的错误。generator也是可迭代对象,不断调用next(g)实在是太变态了,正确的方法是使用for循环,就不需要关心StopIteration的错误。
定义generator的另一种方法是在一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator function。比较难理解的是generator和函数的执行流程不一样,函数式顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

迭代器

可以被next函数调用并不断返回下一个值得对象称为迭代器:Iterator。
可以使用isinstance()判断一个对象是否是Iterator对象。
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。把list、dict、str等Iterable变成Iterator可以使用iter()函数。
可以使用list()函数,将Iterator返回成一个list。
isinstance(iter(‘abc’),Iterator),输出结果为True。Iterator是惰性的。只有在需要下一个值得时候才会计算。
凡是可作用于for循环的对象都是Iterable类型;
凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()获得一个Iterator对象。

函数式编程

函数式编程的一个特点是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。

高阶函数
map()、reduce()

Python内建了map()和reduce()函数。
map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数作用到Iterable对象的每一个元素,将结果作为新的Iterator返回。
reduce把一个函数作用在一个序列[x1,x2,x3,…]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:reduce(f,[x1,x2,x3,x4]) = f(f(f(x1,x2),x3),x4)

filter()

Python内建的filter()函数用于过滤序列。
和map()类似,filter()也接收一个函数和一个序列。与map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
把一个序列中的空字符串删掉,可以这么写:
def not_empty(s):
return s and s.strip()
list(filter(not_empty,[‘A’,‘B’,None,‘C’,’ ']))
注意到filter()函数返回的是一个Iterator,也就是一个惰性序列,所以要强迫filter()完成计算结果,需要用list()函数获得所有结果并返回list。

sorted()

Python内置的sorted()函数就可以对list进行排序:
sorted([36,5,-12,9,-12]),输出结果:[-21,-12,5,9,36]
sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:
sorted([36,5,-12,9,-21],key=abs),输出结果:[5,9,-12,-21,36]。
默认情况下,对字符串排序,是按照ASCII的大小比较的,由于‘Z’<‘a’,大写字母会排在小写字母的前边。
现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。
要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True。
用sorted()排序的关键在于实现一个映射函数。

返回函数

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。
如果不需要立刻求和,可以不返回求和的结果,而是返回求和的函数:
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数:
f = lazy_sum(1,3,5,7,9),
调用函数f的时候,才会真正去计算f的结果,f(),输出结果为25。
上面这个例子中,在函数lazy_sum中定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种称为“闭包”。
**注意:**当调用lazy_sum()时,每次调用都会返回一个新的函数,即使传入相同的参数,生成的sum函数也不是同一个。
返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。

匿名函数

在Python中,对匿名函数提供了有限支持。关键字lambda表示匿名函数,以map()函数为例,计算x的平方,可以通过直接传入匿名函数来实现:
list(map(lambda x : x * x,[1,2,3,4,5,6,7,8,9])),匿名函数只能有一个表达式,不用写return,返回值就是该表达式的结果。用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突,此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数。

装饰器

因为函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。
def now():
print(‘2020-7-1’)
f = now
f()
结果:2020-7-1
函数对象有一个__name__属性,可以拿到函数的名字:now.name,‘now’
如果我们要增强now()函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。本质上,decorator就是一个返回函数的高阶函数。
def log(func):
def wrapper(*args,**kw):
print(‘call %s():’ % func.name)
return func(*args,**kw)
return wrapper
观察上面的log,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
@log
def now():
print(‘2020-7-1’)
调用now函数,会在运行now()函数之前打印一行日志:
now()
call now():
2020-7-1
把@log放到now()函数的定义处,相当于执行了语句:now = log(now)
由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。
wrapper()函数的参数定义是(*args,**kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

偏函数

int()函数有个base参数。默认值为base=10,可以赋值其他禁止,可以转换二进制字符串,int(‘12345’,base=8),但是如果要转换大量的二进制字符串,每次都传入int(x,base=2)非常麻烦,于是,可以定义一个int2函数,
def int2(x,base=2):
return int(x,base)
functools.partial就是帮助我们创建一个偏函数的,不需要自己定义int2(),可以直接使用下面的代码创建一个新的函数int2。
import functools
int2 = functools.partial(int,base=2)
int2(‘100000’) 64
int2(‘1010101’) 85
简单总结functools.partial的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

面向对象编程

类和实例

Python中,定义类是通过class关键字:
class Student(object):
pass
class后面跟类名,类名通常是大写开头的单词,接着是(object),表示继承的类是哪个,如果没有合适的继承类,就使用object类,object类是所有类都会继承的类。
定了好了类,就可以根据Student类创建Student实例,通过类名+()实现。stu = Student()。
可以自由地给一个实例变量绑定属性,给实例stu绑定一个name属性:
stu.name = xiaoming
由于类起到模板的作用,所以,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把name、score等属性绑上去。
class Student(object):
def init(self,name,score):
self.name = name
self.score = score
注意: init方法前后分别有两个下划线,而且第一个参数永远是self,表示创建的实例本身,so,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向的创建的实例本身。
有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器会自己把实例变量传进去:
stu = Student(‘xiaoming’,60)
和普通函数相比,在类中定义的函数的第一个永远是实例变量self,并且,在调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

数据封装

在类中定义函数,将对属于类的数据的操作定义在函数中,这样,就把“数据”给封装起来了。

访问限制

在类的内部,可以有属性和方法,而外部代码可以直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
但是这样做,外部代码还是可以自由轻易地修改一个实例的name、score属性:
stu = Student(‘xiaoming’,59)
stu.score 输出结果:59
stu.score = 99
stu.score 输出结果:99
如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__, 在python中,实例的变量名如果以__开头就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以改一改Student类:
class Student(object):
def int(self,name,score):
self.__name = name
self.__score = score
def print_score(self):
print(’%s:%s’ %(self.__name,self.__score))
改完后,对于外部代码来说,功能没什么变动,但是已经不能从外部访问加了__两个下划线的属性。
在访问私有变量会报错:AttributeError:‘Student’ object has no attribute ‘__name’。这样通过访问限制的保护,代码更加健壮。
如果外部代码要获取name和score怎么办?可以给Student类增加get_name和get_score这样的方法:
如果允许外部代码修改私有变量怎么办?可以给Student类增加set_name和set_score方法。
需要注意的是,在Python中,变量名类似__xxx__的,也就是以双下划线开头,是特殊变量,特殊变量是可以直接访问的。还有一种变量是以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是按照约定俗称的规定,最好不要访问。把它视为私有变量,不要随意访问。
双下划线开头的实例变量也是可以从外部直接访问的,因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量。
因为不同版本的Python解释器会把__name变量改成不同的变量名。Python本身没有任何机制阻止你干坏事,一切全靠自觉。

继承和多态

当定义一个class的时候,可以从某个现有的类继承,定义的新类class称为子类,而被继承的class称为基类、父类或超类。
子类可以继承父类的方法,子类什么都不用做就会拥有父类的方法,假设当子类和父类都存在相同的一个run()方法时,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run(),这就是“多态”。
在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做事父类。反之,则不行。

获取对象信息

利用type()函数来判断对象的类型,基本类型都可以用type()判断:type(123),结果<class ‘int’>;type(‘str’),结果<class ‘str’>;type(None),结果<type(None) ‘NoneType’>。如果一个变量指向函数或者类,也可以用type()判断:type(abs),结果<class ‘builtin_function_or_method’>。
type()函数返回的是对应的class类型。
判断基本数据类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用types模块中定义的常量:
import types
def fn():
… pass

type(fn)==types.FunctionType
True
type(abs)==types.BuiltinFunctionType
True
type(lambda x: x)==types.LambdaType
True
type((x for x in range(10)))==types.GeneratorType
True
另一种方法是isinstance(),对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
假设继承链是:object->Animal->Dog->Tom,那么isinstance()就可以告诉我们,一个对象是否是某种类型。a = Animal() d = Dog() h = Husky(),然后判断:isinstance(h,Husky),结果:True,再判断:isinstance(h,Dog),结果:True。h虽然自身是Husky类型,但由于Husky是从Dog继承下来的,所以,h也还是Dog类型。换句话说,instance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。
能用type()判断的也可以用instance()判断,总是优先使用isinstance()判断类型,可以将指定类型及其子类 一网打尽。
使用dir()函数,如果想要获得一个对象的所有方法和属性,可以使用dir()函数,它返回一个包含字符串的list,list包含的是一个对象的所有属性和方法的字符串。
类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法。所以下面的方法是等价的:
len(‘ABC’)和’ABC’.len()。
仅仅把属性和方法列出来是不够的,配合getsttr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态:
class MyObject(object):
def init(self):
self.x = 9
def power(self):
return self.x * self.x
obj = MyObject()
然后,测试该对象的属性:
hasattr(obj,‘x’) # 有属性‘x’吗?
True
hasattr(obj,‘y’)
False
setattr(obj,‘y’,19)
hasattr(obj,‘y’)
True
getattr(obj,‘y’)
19
obj.y
19
还可以传入一个default参数,如果属性不存在,就返回默认值:
getattr(obj,‘z’,404)

实例属性和类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。给实例绑定属性的方法是通过实例变量,或者通过self变量。
class Student(object):
def init(self,name):
self.name = name
s = Student(‘Bob’)
s.score = 90
但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student所有:
class Student(object):
name = ‘Student’
这个类属性,类的所有实例都可以访问到。
在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

面向对象高级编程

使用__slots__

想要限制实例的属性怎么办?比如,只允许对Student实例添加name和age属性。
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:
class Student(object):
slots_ = (‘name’,‘age’) #用tuple 定义允许绑定的属性名称。
s = Student(object)
当执行s.score = 99时。将会报AttributeError的错误。使用__slots__要注意,slots__定义的属性仅对当前实例起作用,对继承的子类的是不起作用的。
除非在子类中也定义__slots
,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__。

使用@property

有没有既能检查参数,又可以用类似属性这样简单的方式来访问类的变量呢?装饰器可以给函数动态加上功能,对于类的方法,装饰器一样起作用:
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self,value):
if not isinstance(value,int):
raise ValueError(‘score must be an integer!’)
if value < 0 or value > 100:
reaise ValueError(‘score must betwees 0~100’)
self._score = value
@property 的实现比较复杂,先考察如何使用,把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作:
s = Student()
s.core = 60 ##实际通过@property转化成s.set_score(60)
s,core ##实际转化为s.get_score()
60
还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性。

多重继承

通过多重继承,一个子类就可以同时获得多个父类的所有功能。

定制类

除了__slots__和__len__()方法之外,Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类。
str
定义一个Student类,打印一个实例:
class Strudent(object):
def init(self,name):
self.name = name
print(Student(‘Michael’))
<main.Student object at 0x109afb190>
打印出一堆<main.Student object at 0x109afb190>,不好看。
怎么才能打印得好看呢?只需要定义好__str__()方法,返回一个好看的字符串就可以了:
class Student(object):
def init(self,name):
self.name = name
def str(self):
return ‘Student object (name: %s)’ %self.name

print(Student(‘Michael’))
Student object(name: Michael)

s = Student(‘Michael’)
s
<main.Student object at 0x109afb310>
不使用print,直接打印实例s,打印出来的实例还是不好看,这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,repr()是为调试服务的。
解决办法是再定义一个__repr__(),但是通常__str__()和__repr__()代码都是一样的,所以有个偷懒的写法:
class Student(object):
def init(self,name):
self.name = name
def str(self):
return ‘Student object (name=%s)’ % self.name
repr = str

iter
如果一个类想被用于for…in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。
以斐波那契数列为例,写一个Fib类,可以作用于for循环:
class Fib(object):
def init(self):
self.a,self.b = 0,1
def iter(self):
return self
def next(self):
self.a,self.b = self.b,self.a + self.b
if self.a > 100000:
raise StopIteration()
return self.a
getitem
Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第某个元素:
Fib()[5]
TypeError:‘Fib’ object does not support indexing
要表现的像list那样按照下标取出元素,需要实现__getitem__()方法:
class Fib(object):
def getitem(self,n):
a,b = 1,1
for x in range(n):
a,b = b, a+b
return a
现在,就可以按下标访问数列的任意一项了。
getattr
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定义Student类:
class Student(object):
def init(self):
self.name = ‘Michael’
调用name属性没有问题,但是,调用不存在的score属性,就有问题了:
s = Student()
s.core
AttributeError:‘Student’ object has no attribute ‘score’
为避免这个错误,除了加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。
class Student(object):
def init(self):
self.name = ‘Michael’
def getattr(self,attr):
if attr == ‘score’:
return 99
当调用不存在的score属性时,Python解释器会试图调用__getattr__(self,‘score’)来尝试获得属性,这样,我们就有机会返回score的值:
s = Student()
s.core
99
注意,只有在没有找到属性的情况下,才调用__getattr__,已有的属性,比如name,不会在__getattr__中查找。
注意到任意调用如s.abc都会返回None,这是因为我们定义的__getattr__ 默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:
class Student(object):
def getattr(self,attr):
if attr==‘age’:
return lambda:25
raise AttributeError(’‘Student’ object has no attribute ‘%s’’ % attr)
这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。

call
当调用实例方法时,可以用实例名.方法名来调用,能不能直接在实例本身上调用呢?在Python中,答案是肯定的。任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用。
class Student(object):
def init(self,name):
self.name = name
def call(self):
print(‘My name is %s.’ % self.name)
调用方式如下:
s = Student(‘Michael’)
s() ##__call__参数是self,s()不需要传入参数。
My name is Michael.
call()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。

如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。

那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()的类实例:
callable(Student())
True
callable(max)
True
callable([1,2,3])
False
通过callable()函数,我们就可以判断一个对象是否是"可调用"对象。

使用枚举类

from enum import Enum
Month = Enum(‘Month’, (‘Jan’, ‘Feb’, ‘Mar’, ‘Apr’, ‘May’, ‘Jun’, ‘Jul’, ‘Aug’, ‘Sep’, ‘Oct’, ‘Nov’, ‘Dec’))
这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:
for name, member in Month.members.items():
print(name, ‘=>’, member, ‘,’, member.value)
value属性则是自动赋给成员的int常量,默认从1开始计数。

如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:
from enum import Enum, unique

@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
@unique装饰器可以帮助我们检查保证没有重复值。

使用元类

type()
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
比方说我们要定义一个Hello的class,就写一个hello.py模块:
class Hello(object):
def hello(self,name=‘world’):
print(‘Hello,%s.’ % name)
当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的class对象,测试如下:
from hello import Hello
h = Hello()
h.hello()
Hello, world.
print(type(Hello))
<class ‘type’>
print(type(h))
<class ‘hello.Hello’>
type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,它的类型就是class Hello。
class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。
type()函数既可以返回一个对象的类型,也可以创建出一个新的类型,比如,可以通过type()函数创建出Hello类,而无需通过class Hello(object)…的定义:
def fn(self, name=‘world’): # 先定义函数
… print(‘Hello, %s.’ % name)

Hello = type(‘Hello’, (object,), dict(hello=fn)) # 创建Hello class
h = Hello()
h.hello()
Hello, world.
print(type(Hello))
<class ‘type’>
print(type(h))
<class ‘main.Hello’>
要创建一个class对象,type()函数依次传入3个参数:
class的名称;
继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。
通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。

正常情况下,我们都用class Xxx…来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

metaclass
除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass。
metaclass,直译为元类,简单的解释就是:

当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。

但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。

连接起来就是:先定义metaclass,就可以创建类,最后创建实例。

所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。

metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。

我们先看一个简单的例子,这个metaclass可以给我们自定义的MyList增加一个add方法:

定义ListMetaclass,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass:
metaclass是类的模板,所以必须从‘type’类型派生:
元类先不看了。。。。

错误、调试和测试

Python内置了一套try…except…finally的错误处理机制,
try:
print(‘try…’)
r = 10 / 0
print(‘result:’, r)
except ZeroDivisionError as e:
print(‘except:’, e)
finally:
print(‘finally…’)
print(‘END’)
当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。
如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句:
try:
print(‘try…’)
r = 10 / int(‘2’)
print(‘result:’, r)
except ValueError as e:
print(‘ValueError:’, e)
except ZeroDivisionError as e:
print(‘ZeroDivisionError:’, e)
else:
print(‘no error!’)
finally:
print(‘finally…’)
print(‘END’)

Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。比如:
try:
foo()
except ValueError as e:
print(‘ValueError’)
except UnicodeError as e:
print(‘UnicodeError’)
第二个except永远也捕获不到UnicodeError,因为UnicodeError是ValueError的子类,如果有,也被第一个except给捕获了。

Python所有的错误都是从BaseException类派生的,常见的错误类型和继承关系看这里:

https://docs.python.org/3/library/exceptions.html#exception-hierarchy

使用try…except捕获错误还有一个巨大的好处,就是可以跨越多层调用,比如函数main()调用bar(),bar()调用foo(),结果foo()出错了,这时,只要main()捕获到了,就可以处理:
def foo(s):
return 10 / int(s)

def bar(s):
return foo(s) * 2

def main():
try:
bar(‘0’)
except Exception as e:
print(‘Error:’, e)
finally:
print(‘finally…’)
也就是说,不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写try…except…finally的麻烦。

调用栈
如果错误没有被捕获,它就会一直往上抛,最后被Python解释器捕获,打印一个错误信息,然后程序退出。来看看err.py:
err.py:
def foo(s):
return 10 / int(s)

def bar(s):
return foo(s) * 2

def main():
bar(‘0’)

main()
执行,结果如下:

$ python3 err.py
Traceback (most recent call last):
File “err.py”, line 11, in
main()
File “err.py”, line 9, in main
bar(‘0’)
File “err.py”, line 6, in bar
return foo(s) * 2
File “err.py”, line 3, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
出错并不可怕,可怕的是不知道哪里出错了。解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:

错误信息第1行:

Traceback (most recent call last):
告诉我们这是错误的跟踪信息。

第2~3行:

File “err.py”, line 11, in
main()
调用main()出错了,在代码文件err.py的第11行代码,但原因是第9行:

File “err.py”, line 9, in main
bar(‘0’)
调用bar(‘0’)出错了,在代码文件err.py的第9行代码,但原因是第6行:

File “err.py”, line 6, in bar
return foo(s) * 2
原因是return foo(s) * 2这个语句出错了,但这还不是最终原因,继续往下看:

File “err.py”, line 3, in foo
return 10 / int(s)
原因是return 10 / int(s)这个语句出错了,这是错误产生的源头,因为下面打印了:

ZeroDivisionError: integer division or modulo by zero
根据错误类型ZeroDivisionError,我们判断,int(s)本身并没有出错,但是int(s)返回0,在计算10 / 0时出错,至此,找到错误源头。

**注意:**出错的时候,一定要分析错误的调用栈信息,才能定位错误的位置。

记录错误

如果不捕获错误,自然可以让Python解释器来打印出错误堆栈,但程序也被结束了。既然我们能捕获错误,就可以把错误堆栈打印出来,然后分析错误原因,同时,让程序继续执行下去。

Python内置的logging模块可以非常容易地记录错误信息:
err_logging.py

import logging

def foo(s):
return 10 / int(s)

def bar(s):
return foo(s) * 2

def main():
try:
bar(‘0’)
except Exception as e:
logging.exception(e)

main()
print(‘END’)
同样是出错,但程序打印完错误信息后会继续执行,并正常退出:

$ python3 err_logging.py
ERROR:root:division by zero
Traceback (most recent call last):
File “err_logging.py”, line 13, in main
bar(‘0’)
File “err_logging.py”, line 9, in bar
return foo(s) * 2
File “err_logging.py”, line 6, in foo
return 10 / int(s)
ZeroDivisionError: division by zero
END
通过配置,logging还可以把错误记录到日志文件里,方便事后排查。

调试

第一种方法简单直接粗暴有效,就是用print()把可能有问题的变量打印出来看看:
def foo(s):
n = int(s)
print(’>>> n = %d’ %n)
return 10/n
def main()
foo(‘0’)
main()
$ python err.py
n = 0
Traceback (most recent call last):

ZeroDivisionError: integer division or modulo by zero
用print()最大的坏处是将来还得删掉它,想想程序里到处都是print(),运行结果也会包含很多垃圾信息。
所以第二种方法:断言
凡是用print()辅助查看的地方,都可以用断言(assert)来替代:
def foo(s):
n = int(s)
assert n != 0,‘n is zero!’
return 10 / n
def main():
foo(‘0’)
assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。
如果断言失败,assert语句本身就会抛出AssertionError:
$ python err.py
Traceback (most recent call last):

AssertionError: n is zero!
当然了,如果程序中到处都是assert,和print()相比也好不到哪去。不过,启动Python解释器时可以用-O参数来关闭assert:
$ python -O err.py
Traceback(most recent call last):

ZeroDivisionError: division by zero
关闭后,可以把所有的assert语句当成pass来看。

logging
把print()替换为logging是第三种方式,和assert比,logging不仅会抛出错误,而且可以输出到文件:
import logging
s = ‘0’
n = int(s)
logging.info(‘n = %d’ % n)
print(10 / n)
logging.info()就可以输出一段文本。运行,发现除了ZeroDivisionError,没有任何信息,怎么回事,别急,在import logging之后添加一行配置再试试:
import logging
logging.baseConfig(level=logging.INFO)
看到输出了:
$ python err.py
INFO:root:n = 0
Traceback (most recent call last):
File “err.py”, line 8, in
print(10 / n)
ZeroDivisionError: division by zero
这就是logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。
logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。

pdb
第四种方式是启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
#err.py
s = ‘0’
n = int(s)
print(10 / n)
然后启动:
$ python -m pdb err.py
/Users/michael/Github/learn-python3/samples/debug/err.py(2)()
-> s = ‘0’

以参数-m pdb启动后,pdb定位到下一步要执行的代码-> s = ‘0’。输入命令l来查看代码:
(Pdb) l
1 # err.py
2 -> s = ‘0’
3 n = int(s)
4 print(10 / n)
输入命令n可以单步执行代码:
(Pdb) n
/Users/michael/Github/learn-python3/samples/debug/err.py(3)()
-> n = int(s)
(Pdb) n
/Users/michael/Github/learn-python3/samples/debug/err.py(4)()
-> print(10 / n)
任何时候都可以输入命令p 变量名来查看变量:
(Pdb) p s
‘0’
(Pdb) p n
0
输入命令q结束调试,退出程序:
(Pdb) q
这种通过pdb在命令行调试的方法理论上是万能的,但实在是太麻烦了,如果有一千行代码,要运行到第999行得敲多少命令啊。还好,我们还有另一种调试方法。
pdb.set_trace()
这个方法也是用pdb,但是不需要单步执行,只需要import pdb,然后,在可能出错的地方放一个pdb.set_trace(),就可以设置一个断点:
import pdb
s = ‘0’
n = int(s)
pdb.set_trace() #运行到这里会自动暂停
print(10 / n)
运行代码,程序会自动在pdb.set_trace()暂停并进入调试环境,可以用命令p查看变量,或者用命令c继续运行:
$ python err.py
/Users/michael/Github/learn-python3/samples/debug/err.py(7)()
-> print(10 / n)
(Pdb) p n
0
(Pdb) c
Traceback (most recent call last):
File “err.py”, line 7, in
print(10 / n)
ZeroDivisionError: division by zero
这个方式比直接启动pdb单步调试效率要高很多,但也高不到哪去。
最后一种方式是使用IDE。

单元测试

如果你听说过“测试驱动开发”(TDD:Test-Driven Development),单元测试就不陌生。

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。

比如对函数abs(),我们可以编写出以下几个测试用例:

输入正数,比如1、1.2、0.99,期待返回值与输入相同;

输入负数,比如-1、-1.2、-0.99,期待返回值与输入相反;

输入0,期待返回0;

输入非数值类型,比如None、[]、{},期待抛出TypeError。

把上面的测试用例放到一个测试模块里,就是一个完整的单元测试。

如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,要么测试条件输入不正确,总之,需要修复使单元测试能够通过。

单元测试通过后有什么意义呢?如果我们对abs()函数代码做了修改,只需要再跑一遍单元测试,如果通过,说明我们的修改不会对abs()函数原有的行为造成影响,如果测试不通过,说明我们的修改与原有行为不一致,要么修改代码,要么修改测试。

这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。

我们来编写一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问,用起来就像下面这样:
d = Dict(a=1, b=2)
d[‘a’]
1
d.a
1
mydict.py代码如下:
class Dict(dict):

def __init__(self, **kw):
    super().__init__(**kw)

def __getattr__(self, key):
    try:
        return self[key]
    except KeyError:
        raise AttributeError(r"'Dict' object has no attribute '%s'" % key)

def __setattr__(self, key, value):
    self[key] = value

为了编写单元测试,我们需要引入Python自带的unittest模块,编写mydict_test.py如下:
import unittest

from mydict import Dict

class TestDict(unittest.TestCase):

def test_init(self):
    d = Dict(a=1, b='test')
    self.assertEqual(d.a, 1)
    self.assertEqual(d.b, 'test')
    self.assertTrue(isinstance(d, dict))

def test_key(self):
    d = Dict()
    d['key'] = 'value'
    self.assertEqual(d.key, 'value')

def test_attr(self):
    d = Dict()
    d.key = 'value'
    self.assertTrue('key' in d)
    self.assertEqual(d['key'], 'value')

def test_keyerror(self):
    d = Dict()
    with self.assertRaises(KeyError):
        value = d['empty']

def test_attrerror(self):
    d = Dict()
    with self.assertRaises(AttributeError):
        value = d.empty        

编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。

以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。

对每一类测试都需要编写一个test_xxx()方法。由于unittest.TestCase提供了很多内置的条件判断,我们只需要调用这些方法就可以断言输出是否是我们所期望的。最常用的断言就是assertEqual():
self.assertEqual(abs(-1), 1) # 断言函数返回的结果与1相等
另一种重要的断言就是期待抛出指定类型的Error,比如通过d[‘empty’]访问不存在的key时,断言会抛出KeyError:
with self.assertRaises(KeyError):
value = d[‘empty’]
而通过d.empty访问不存在的key时,我们期待抛出AttributeError:
with self.assertRaises(AttributeError):
value = d.empty
运行单元测试
一旦编写好单元测试,我们就可以运行单元测试。最简单的运行方式是在mydict_test.py的最后加上两行代码:

if name == ‘main’:
unittest.main()
这样就可以把mydict_test.py当做正常的python脚本运行:

$ python mydict_test.py
另一种方法是在命令行通过参数-m unittest直接运行单元测试:
$ python -m unittest mydict_test

Ran 5 tests in 0.000s

OK
这是推荐的做法,因为这样可以一次批量运行很多单元测试,并且,有很多工具可以自动来运行这些单元测试。
setUp与tearDown
可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。

setUp()和tearDown()方法有什么用呢?设想你的测试需要启动一个数据库,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样,不必在每个测试方法中重复相同的代码:
class TestDict(unittest.TestCase):
def setUp(self):
print(‘setUp…’)
def tearDown(self):
print(‘tearDown…’)
可以再次运行测试看看每个测试方法调用前后是否会打印出setUp…和tearDown…。

IO编程

文件读写

open()、read()、close().
最后一步是调用close()方法关闭文件,文件使用完后必须关闭,因为文件对象会占用操作系统的资源,并且操作系统同一时间能打开的文件数量也是有限的。由于文件读写时都有可能产生IOError,一旦出错,后面的f.close()就不会调用。所以,为了保证无论是否出错都能正确地关闭文件,可以使用try…finally来实现:
try:
f = open(‘path’,‘r’)
print(f.read())
finally:
if f:
f.close()
但是每次都这么写实在太繁琐,所以,Python引入了with语句来自动帮我们调用close()方法:
with open(‘path’,‘r’) as f:
print(f.read())
这和前面的try…finally是一样的,但是代码更加简洁,并且不必调用f.close()方法。调用read()会一次性读取文件的全部内容,如果文件有10G,内存就爆了,所以,要保险起见,可以反复调用read(size)方法,每次最多读取size个字节的内容。另外,调用readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回list。因此,要根据需要决定怎么调用。
如果文件很小,read()一次性读取最方便;如果不能确定文件大小,反复read(size)比较保险;如果是配置文件,调用readlines()最方便:
for line in f.readlines():
print(line.strip()) #把末尾的’\n’ 删掉
file-like Object
像open()函数返回的这种有个read()方法的对象,在Python中统称为file-like Object。除了file外,还可以是内存的字节流,网络流,自定义流等等。file-like Object不要求从特定类继承,只要写个read()方法就行。

StringIO就是在内存中创建的file-like Object,常用作临时缓冲。
二进制文件
前面讲的默认都是读取文本文件,并且是UTF-8编码的文本文件。要读取二进制文件,比如图片、视频等等,用‘rb’模式打开文件即可:
f = open(’/Users/michael/test.jpg’, ‘rb’)
f.read()
b’\xff\xd8\xff\xe1\x00\x18Exif\x00\x00…’ # 十六进制表示的字节
字符编码
要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如,读取GBK编码的文件:
f = open(’/Users/michael/gbk.txt’, ‘r’, encoding=‘gbk’)
f.read()
‘测试’

对于有些编码不规范的文件,有时候可能会遇到UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个errors参数,表示如果遇到编码错误后如何处理。最简单的方式是直接忽略:
f = open(’/Users/michael/gbk.txt’, ‘r’, encoding=‘gbk’, errors=‘ignore’)
写文件
写文件和读文件是一样的,唯一区别是调用open()函数时,传入标识符’w’或者’wb’表示写文本文件或写二进制文件:
f = open(’/Users/michael/test.txt’, ‘w’)
f.write(‘Hello, world!’)
f.close()
你可以反复调用write()来写入文件,但是务必要调用f.close()来关闭文件。当我们写文件时,操作系统往往不会立刻把数据写入磁盘,而是放到内存缓存起来,空闲的时候再慢慢写入。只有调用close()方法时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调用close()的后果是数据可能只写了一部分到磁盘,剩下的丢失了。所以,还是用with语句来得保险:

with open(’/Users/michael/test.txt’, ‘w’) as f:
f.write(‘Hello, world!’)
要写入特定编码的文本文件,请给open()函数传入encoding参数,将字符串自动转换成指定编码。

细心的童鞋会发现,以’w’模式写入文件时,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)。如果我们希望追加到文件末尾怎么办?可以传入’a’以追加(append)模式写入。

参数设置

1、pandas中dataframe中float/double类型数据默认以科学计数法显示。
可以通过pd.set_option中的display.float_format参数来让其显示为正常的小数,比如保留3位小数:pd.set_option(‘display.float_format’,lambda x:’%.3f’ % x)。

NumPy学习

NumPy,是Numerical Python的简称,它是目前Python数值计算中最为重要的基础包。大多数计算包都提供了基于NumPy的科学函数功能,将NumPy的数组对象作为数据交换的通用语。

Pandas学习

1、统计某一列中各个值得出现次数
df.loc[:,‘index_name’].value_counts()
2、根据其他列的条件为一列进行赋值
df.loc[df.字段满足的条件,‘要赋值的字段’] = 要赋的值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值