教程地址:Python教程–廖雪峰
还没学之前就有所耳闻,Python是动态语言,不需要定义类型名的,语法上已经将这一块封装好了。
除了整形、浮点型和字符串外,Python也有布尔值(有特殊的运算符and\or),“常量”(Python的常量其实是一个变量)。但最大的不同点是,Python有独特的“列表”和“字典”类型。
列表:分为list(列表)和tuple(元组),list是可变的,而tuple是不可变的。list有点像一个数组但是内部保存的元素可以是多种且不同的数据类型。tuple也很类似,但是它内部的元素必须确定,即不可改变。
有一个比较容易搞混的例子:t = (‘a’, ‘b’, [‘A’, ‘B’]),它是正确的。在tuple的指向里,三个元素的位置是确定不变的;在list中,它储存元素的空间是可以更换不同的元素的。
dict:当看到dict的特性时,我立马想到数据结构里的哈希表,查了一下确实是如此。那就很好理解了,key值是索引哈希值所在位置的钥匙。另一个方面,dict的特性就像查字典那样,时间永远是常量级的,典型的空间换时间思想。一个很重要的地方时,key必须是一个不可变的数据类型,也就是说list是不行的,某种情况下的tuple也是不行的(上面的例子)。
set:从词义上理解就很清晰了,就是一个集合。事实上它所储存的元素是不可重复的。当然,dict的特性也保证了key所对应的value也是不可重复的,要不然查找效率就不是常量级了。
函数没有太大的变化,记录一下之前没见过的。
isinstance():检查参数类型是否合格。
返回多个值:C/C++是不允许返回多个值的,如果实在需要,只能用数组指针。Python之所以可以返回多个值,其实是tuple的作用。
import XX:类似于头文件
Python的参数有点复杂,它不需要定义参数是哪种数据类型(一开始学真的很不习惯,被C虐惯了),但可以根据参数的作用来区分参数类型,一共有5类:必选、默认、可变、关键词、命名关键词。并且参数的先后顺序要严格按照这个来,否则会产生歧义。如调用fun(a, b = 1, c)传入(1, 2)时,我们不知道应该是报错还是按照调用(1, 1, 2)来。
必选:也叫位置参数,调用时必须有一个值占位
默认:提前设置好参数的值,如果调用时没有值占位,那么就使用默认值
可变:传入的参数个数是可变的,在参数前加一个*,看起来有点像指针,函数内部接收到的其实是一个tuple,因此代码不会变化
关键字:**kw,允许传入任意个含参数名的参数,这些关键字参数在函数内部自动组装成一个dict。
命名关键字:限制关键字参数名字,需要用到特殊分隔符 * , * 后面的参数被视为命名关键字;如果函数中有可变参数,就不需要再使用分隔符了
对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。
大神的小结:
Python的函数具有非常灵活的参数形态,既可以实现简单的调用,又可以传入非常复杂的参数。
默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!
要注意定义可变参数和关键字参数的语法:
*args是可变参数,args接收的是一个tuple;
**kw是关键字参数,kw接收的是一个dict。
以及调用函数时如何传入可变参数和关键字参数的语法:
可变参数既可以直接传入:func(1, 2, 3),又可以先组装list或tuple,再通过args传入:func((1, 2, 3));
关键字参数既可以直接传入:func(a=1, b=2),又可以先组装dict,再通过 * * kw传入:func(* * {‘a’: 1, ‘b’: 2})。
使用* args和**kw是Python的习惯写法,当然也可以用其他参数名,但最好使用习惯用法。
命名的关键字参数是为了限制调用者可以传入的参数名,同时可以提供默认值。
定义命名的关键字参数在没有可变参数的情况下不要忘了写分隔符*,否则定义的将是位置参数。
讲到一个尾递归的东西,可总结为:返回值只能是函数自身而不能是一个表达式
尾递归很容易改成循环的形式,但是很多编辑器没有做到。印象中C好像是做到了?
在Python中,代码不是越多越好,而是越少越好。代码不是越复杂越好,而是越简单越好。
用法是[x:x],提取list或tuple里这一部分的数据,十分方便
语法是 for ... in ...
,不仅可以迭代list或tuple,还可以操作dict
dict内部元素储存是无序的,所以迭代的结果可能会和储存的顺序不一样。另外,dict迭代默认情况下是key,可用for value in d.value()
迭代value,还可以for k, v in d.item()
来同时迭代key和value。
字符串迭代,要实现同时索引对和元素对的迭代,可以使用内置的enumerate()函数。
判断是否为可迭代对象,通过collections的Iterable类型来判断。
格式:[元素或表达式 , 循环式]
循环式还可以用两层循环。
运用列表生成式,可以写出非常简洁的代码。例如,列出当前目录下的所有文件和目录名,可以通过一行代码实现:
>>> import os # 导入os模块,模块的概念后面讲到
>>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录
确实不愧为“人生苦短,我用python”,脑海中想着如果要用C的话,先不论要设多少个边界条件,关键是也不知道怎么写啊……
生成器可以看成是一个集成了算法的列表。
要比较列表生成式和生成器的差别,可以看一段代码:
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
at 0x1022ef630>
因而生成器是要通过一边循环一遍计算得到所有的结果的。
另外,如果在一个函数里添加了yield X
,函数就变成了生成器。而yield的作用是输出当前生成器计算所得的X的结果。
可迭代对象 Iterable:可以直接作用于for循环的对象,包括集合数据类型和generator
迭代器 Iterator:可以被next()函数调用并不断下一个值的对象。
可以isinstance()函数来区分可迭代对象和迭代器。
说实话没看明白,不过上网搜索了一下,函数式和命令式同属于面向过程的思维。函数式编程式接近数学计算。只能明白这么多了。
总结起来就是大神说的那么几句话:变量可以指向函数、函数名也是变量、函数可以作为变量传参。
虽然很多人都说学新语言就要放下固有语言的成见,但是我还是不由自主地对比起C/C++与python在这一点的不同。变量指向函数可以通过函数指针解决,但应该不是一个层次的东西。python的核心是为了实现函数作为参数传递的机制,这是高阶函数的特征。而C/C++并无意做到这一点,它们无法忍受未知的事物作为参数,而一个函数在未被调用之前,很明显会被看作未知事物。
map: 接受两个参数,第一个函数对象, 一个Interable。map将的函数作用到序列的每个元素,并把结果作为新的Interable。
reduce: 把一个函数作用在一个序列上,这个函数必须接受两个参数,reduce把结果继续和序列的下一个元素累积计算
filter: 用于过滤序列。返回值决定保留还是丢弃该元素
sorted: 排序函数,可以接受一个key函数来实现自定义排序
即将函数作为结果值返回。这一部分着实有点难以理解,换个名字我就吓了一跳,它就是赫赫有名的“闭包”。
举教程的例子:
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum
f = lazy_sum(1, 3, 5, 7, 9)
当我们调用lazy_sum()
时,返回的是求和函数。只有调用函数f
时,才能计算真正求和的结果。
需要注意的两个地方是:
1. 当我们调用lazy_sum()
时,每次调用都会返回一个新的函数,即使传入相同的参数,两次调用也是不等价的。
2. 当一个函数返回了一个函数后,其内部的局部变量还被新函数引用。这时,如果返回函数的返回值使用了循环变量,那么多次调用的函数会共享最后一次变量的值,达不到我们的目的。
关键词lambda,就是一个表达式,表达式的值就是返回值。可以把匿名函数赋给一个变量,也可以把它作为返回值返回
假如要增强某一个函数的功能,比如说在函数调用前后打印日志,但又不希望改变原函数的定义,可以使用装饰器在代码运行阶段动态增强功能。
本质上装饰器就是一个返回函数的高阶函数,如:
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper
@log
def now():
print('2015-3-25')
输出结果:
>>> now()
call now():
2015-3-25
实质上是实现了now = log(now)
的功能。
利用functools.partial
帮助创建一个偏函数,这个可以固定某些参数,返回一个新函数,调用这个新函数会更简单。
创建偏函数时,实际上可以接受函数对象、* args、**kw这三个参数的传入。
在python文件中,一个py文件就是一个模块。使用模块可以提高代码的可维护性,其次方便其它地方的引用。python有内置的模块和来自第三方的模块。使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中。但是我们自己命名的函数尽量不要与内置函数名冲突。
为了避免同样的模块名冲突,python引进了按目录来组织模块的方法,称为包。引入了包以后,只要顶层的包名不与别人的冲突,那所有模块都不会与别人冲突。每一个包目录下面都会有一个_init_.py
的文件,否则python会把这个目录当成普通目录。
python的函数和变量可以通过前缀_
来改变作用域。正常的变量和函数是公开的,即public,可以直接访问,;而类似_XXX_
的变量是特殊变量也可以直接引用,但是有特殊用途,比如_name_
;类似_XXX
和__XXX
这样的函数就是非公开的,即private。
虽然如此,但是在python中其实并没有一种方法可以完全限制住private变量或函数的访问,只是从编程习惯上我们不应该引用它。
和C++很像,定义类也是通过class
关键字。class后紧跟着类名,紧接着是(object),表示这个类是从哪个类继承下来的。
创建实例是用类+()实现的。可以自由地给一个实例变量绑定一些属性。
由于类可以起到模板的作用,因此可以通过定义一个特殊的__init__
方法,在创建实例的时候,把我们认为必须强制绑定的属性绑上去。__init__
方法的第一个参数是self
,表示创建的实例本身。
可以将函数定义在类里,实现数据封装。
和静态语言不同(如C++),Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同。
如果要让内部属性不被外部访问,可以把属性的名称前面加上两个下划线__
。在python中,实例的变量名如果加上了两个下划线,就变成了一个私有变量。
其实python的私有变量是没有得到像C++那样的机制保护的。事实上,之所以不能直接访问私有变量是因为解释器对外会把__XXX
改成_Object__XXX
,所以仍然可以通过后者来访问这个私有变量。
这一小节对面对对象编程来说是重点。刚学C++时,继承还好理解,多态就有些费解了。正好现在再看看python的继承和多态,两相对比来理解。
理解继承的时候,一定要想象一棵树(最好是数据结构的那种树),根结点是所有子孙结点的父辈结点。同样的,一棵继承树的根类是下面所有结点的父类,它们共同继承了这个父类的全部功能。
理解多态时,我是对比重载和多态的相同和不同之处。和C++不同的是,python是不支持函数名重载的,内部对比无从说起。但是在类的继承中,相同的函数名子类可以覆盖父类。在引用子类的实例时,实例的函数是子类所覆盖的那个函数。
看一个例子来理解会好一点:
def run_twice(animal):
animal.run()
animal.run()
因为继承,所以所有的子类都可以作为animal参数;又因为多态,所以在引用参数的内部函数时其实是引用了覆盖了的函数。
廖老师的总结:
这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
1.对扩展开放:允许新增Animal子类;
2.对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
最后是静态语言和动态语言在继承上的不同。静态语言,以C++为例,像上面那个例子,传入的参数必须只能是animal或它的子类;而动态语言,如python,只要那个实例有run()
方法,那就可以传入,亦被称为“鸭子类型”。
当我们拿到一个对象的参数时,可以使用type()
isinstance()
dir()
来知道这个对象是什么类型,有哪些方法。
基本类型、指向函数或类的变量都可以用type()判断,返回的是对应的Class()
类型。
如果要判断一个对象是否为函数,可以使用types
模块中定义的常量。
可用于判断class的类型,告诉我们一个对象是否是某一个类型。
能用type()判断的都可以用isinstance()判断,而后者还可以判断一个变量是否是某些类型中的一种。如:
>>>isinstance([1,2,3], (list,tuple))
True
用于获得一个对象的所有属性和方法,返回值是一个包含字符串的list。
类似xxx的属性和方法在Python中都是有特殊用途的,比如len方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的len()方法,所以,下面的代码是等价的:
>>> len('ABC')
3
>>> 'ABC'.__len__()
3
我们自己写的类,如果也想用len(myObj)的话,就自己写一个len()方法:
>>> class MyDog(object):
... def __len__(self):
... return 100
...
>>> dog = MyDog()
>>> len(dog)
100
仅仅把属性和方法列出来是不够的,配合getattr()
setattr()
hasattr
,我们可以直接操作一个对象的状态。
用这些函数,可以测试对象的属性或方法。如果试图获取不存在的属性,会抛出AttributeError的错误;可以传入一个默认参数,如果属性不存在,就返回默认值。
看下面这段测试程序:
>>> class Student(object):
... name = 'Student'
...
>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
类属性可以用作一个类所有对象的全局变量,在统计一些数据的时候特别有用。
在C++中,之前那些已经算是基础语法所学的全部了。在python中,面向对象还有很多高级特性。
__slots__
python中,动态绑定允许我们在程序运行的过程中动态给实例或class加上属性和方法。但是,如果我们想要限制实例的属性,可以定义一个特殊的变量__slots__
,来限制该class实例能添加的属性。
需要注意的是,__slots__
定义的属性只能对当前类起作用,对继承的子类是不起作用的。但是,如果子类也定义了__slots__
,子类允许定义的属性就是自身的加上父类的。
@propert
在绑定属性时,为了避免参数值出现不符合常理的情况,我们需要对它进行限制。一种方法是将类的变量类型设置为私有,利用类的方法来访问;但是对于一些情况来说这种方式显得有些麻烦。
要想达到既能检查参数,又可以用类似属性这样简单的方式来访问类的变量的目的,可以使用python内置的@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:
raise ValueError('score must between 0 ~ 100!')
self._score = value
把一个getter方法变成属性,只需要加上@property就可以了;此外,@property又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值。于是我们就有了一个可控的属性操作。
>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
通过这个装饰器,我们还可以只定义getter方法,实现只读属性。
应用到多重继承的设计通常被称为MixIn。MixIn的目的是给一个类增加多个功能,这样在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能。这样一来,我们不需要复杂而庞大的继承链,只要选择组合不同的类的功能,就可以快速构造出所需的子类。
需要注意的是,只允许单一继承的语言不能使用MixIn设计。
python的class还有许多有特殊作用的函数,可以帮助我们定制类。
打印一个类的实例时,若是想要按照我们的想法打印,只需要定义__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)
但是若想直接敲变量不用print,这时候直接显示变量调用的不是__str__()
,而是__repr__()
。两者的区别是前者返回用户看到的字符串,而后者返回程序开发者看到的字符串,即后者是为调试服务的。
解决的方法就是再定义一个__repr__()
,可以直接用赋值语句将__str__()
赋给它。
如果一个类想被用于for···in···循环,就必须实现__iter__()
方法,该方法返回一个迭代对象,然后python的for循环就会不断调用该迭代对象的__next__()
方法拿到下一个循环值,知道遇到StopInteration
错误退出循环。如:
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a, b
def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己
def __next__(self):
self.a, self.b = self.b, self.a + self.b # 计算下一个值
if self.a > 100000: # 退出循环的条件
return StopInteration()
return self.a # 返回下一个值
若将fib实例用于for循环:
>>> for n in Fib():
··· print(n)
···
1
1
2
3
5
···
46368
75025
Fib实例虽然能作用于for循环,看起来和list有点像,但是把它当作list来使用还是不行的。要表现得像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
如果要实现list的切片方法,需要作判断传入的参数是一个int还是一个slice对象。如果把对象看成dict,__getitem__
的参数也可能是一个可以作key的object,例如str。
与之对应的是__setitem__()
方法,把对象视作list或dict来对集合赋值。最后还有一个__delitem__()
方法,用于删除某个元素。
总之,通过上面的方法,我们自己定义的类表现得和python自带的list、tuple和dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
正常情况下,当我们调用类的方法或属性时,如果不存在就会报错。写一个__getattar__()
方法可以动态返回一个属性。
只有在没有找到属性的情况下,才调用__getattar__
。此外如果调用如s.abc都会返回None,这是因为我们定义的__getattar__
默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误。
这实际上可以把一个类的所有属性和方法全部动态化处理,不需要任何特殊手段。这种完全动态调用的特性可以针对完全动态的情况作调用。
现在很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:
http://api.server/user/friends
http://api.server/user/timeline/list
如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。
利用完全动态的__getattar__
,可以写一个链式调用。
class Chain(object):
def __init__(self, path=''):
self._path = path
def __getattr__(self, path):
return Chain('%s/%s' % (self._path, path))
def __str__(self):
return self._path
__repr__ = __str__
>>> Chain().status.user.timeline.list
'/status/user/timeline/list'
还有些REST API会把参数放到URL中,比如GitHub的API:
GET /users/:user/repos
调用时,需要把:user替换为实际用户名:
Chain().users('michael').repos
一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。如果要想在实例本身上调用,只需要定义一个__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() # self参数不要传入
My name is Michael.
__call__()
还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成一个函数,把函数看成对象。
但是如何判断一个变量时对象还是函数呢?其实,更多的时候我们需要判断的是一个对象是否能被调用,能被调用的对象就是一个Callable
对象。通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。
当我们需要定义大量常量时,可以为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。
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
装饰器可以帮助我们检查保证没有重复。访问这些枚举类型可以有多种方式,既可以用成员名称引用枚举变量,又可以直接根据value的值获得枚举类型。
总结:Enum可以把一组相关常量定义在一个class中,且class不可变,而且成员可以直接比较。
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。
比方说我们要定义一个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))
'type'>
>>> print(type(h))
'_main_.Hello'>
要创建一个class对象,type()
函数依次传入3个参数:
1. class的名称;
2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的元素写法;
3. class的方法名称与函数绑定,这里我们把函数fn
绑定到方法名hello
上。
通过type()
函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()
函数创建出class。
正常情况下,我们都用class Xxx...
来定义类,但是,type()
函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。
除了使用type()
动态创建类外,要控制类的创建行为,还可以使用metaclass。
当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。
连接起来就是:先定义metaclass,就可以创建类,最后创建实例。
所以metaclass允许你创建类或修改类。
后面的……没看懂,先不记了。