pyhton
这是一篇根据CA61A python部分所完成的pyhton学习笔记
名称,赋值与用户自定义函数
赋值
除去正常的赋值之外,还可以一次对多个变量赋值,中间用逗号分隔即可,例如
1
area, circumference = pi * radius * radius, 2 * pi * radius
- 对于多重赋值,所有 = 右边的表达式都会先求值,然后再与左边的名称绑定。在这个规则下,我们可以在单个语句内交换两个变量的值。
x,y= y,x
就完成了x,y的交换 - 还可以针对函数赋值,比如对内置函数
f = max
, 此时>>> f
的结果会是 <built-in function max> 同样的,f
也可以被当做函数max
调用
表达式树
对于一个嵌套的表达式,python的计算顺序的按照表达式树的顺序从下到上计算直到根节点.应该是使用后续遍历计算的
非纯函数print
print函数的返回值是none,他的副作用是打印参数. 比如print(print(1),print(2))
的输出就是
1
2
3
1
2
None None
函数定义
1
2
def g(x,y=1):
return x*y `y=1`代表默认是1 这里与c语言不同的是,g本身就可以作为一个变量给别的变量赋值,只有在后面加上()并填上参数的时候才算是调用函数.(或许他自身可以看做一种函数指针?) ### 环境 计算表达式的环境有帧(frame)的序列组成,每个帧都对应一些绑定/映射,将名称与对应的值相连. 全局 帧(global frame)只有一个。赋值和导入语句会将条目添加到当前环境的第一帧。在最开始,我们的环境仅由全局帧组成。如果有函数嵌套调用,就会递归添加帧.这个概念与c++的作用域类似,是一种栈式的方式 
运算符
/ 代表常规除法 3/2 =1.5 保留小数 // 代表向下整除法 -5//4 = -2 核心区别是 // 不会自动将整数转换为浮点数
python命令的可选项
- 直接执行
- 对于win:
python test.py
- 对于linux:
python3 test.py
- 对于win:
- 交互式执行 可以添加参数 -i ,例如
python -i test.py
,这样执行的结果会是正常执行程序到结束,但并不是停止,而是留下一个交互式界面,类似console,可以打印变量,执行函数等.我认为这就相当于shell里的source test.sh
. 执行测试 通过命令
-m doctest
可以执行函数里面手动添加的测试.如果都通过,就没反应.反之会返回相应的报错信息.测试代码可以这样写 ``` def add(a, b): “”” Add two numbers.add(2, 3) 5 add(-1, 1) 0 add(0, 0) 0 “”” return a + b
1
2
3
### 关于函数文档
在函数定义下面的一行最好加上函数的设计文档,即注释.例如:
def add(a, b): “”” calculates a add b “”” return a+b
1
2
3
4
5
之后调用help(add)就会打印 `calculates a add b`
ps: #并不会有这个作用
### 控制
#### 条件语句
if
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
### 测试
1. assert
`assert fib(2) ==3`
2. 文档测试
可以用上文提到的`-m doctest`命令交互式的测试
### 高阶函数
#### 函数作为参数
相比于c++的参数函数,python将函数作为参数传递的方式更为简单
```python
def update(x):
x = 1/x +1
def close(x):
return x - (1/x +1) < 1e-5
def improve(update, close, guess=1):
while not close(guess):
guess = update(guess)
return guess
函数的嵌套定义
- 由于python没有函数重载,因此全局帧的所有函数名字不能相同
- 在使用函数的时候会收到特殊函数签名的限制 对于下面的两个球平方根的函数,如果我想直接嵌套到我之前定义过的improve函数中显然是不行的,因为他的更新函数只有一个参数 ```python
def average(x, y): return (x + y)/2
def sqrt_update(x, a): return average(x, a/x)
1 2 3 4 5 6 7 8 因此我们采用嵌套定义来解决这个问题 ```python def sqrt(a): def sqrt_update(x): return average(x, a/x) def sqrt_close(x): return approx_eq(x * x, a) return improve(sqrt_update, sqrt_close)词法作用域(Lexical scope):局部定义的函数也可以访问整个定义作用域内的名称绑定。在此示例中,sqrt_update 引用名称 a,它是其封闭函数 sqrt 的形式参数。这种在嵌套定义之间共享名称的规则称为词法作用域。最重要的是,内部函数可以访问定义它们的环境中的名称(而不是它们被调用的位置)。
也就是说,函数在被调用的帧继承了他被定义时的所在帧.因此在代用一个函数的时候,最多就有三个帧是可用的,也就是全局帧,定义帧,和调用帧.在查找一个变量时,首先在调用帧查找,之后是定义帧,最后是全局帧
关于python的作用域有两个好处
- 局部函数的名称不会影响定义它的函数的外部名称,因为局部函数的名称将绑定在定义它的当前局部环境中,而不是全局环境中。
- 局部函数可以访问外层函数的环境,这是因为局部函数的函数体的求值环境会继承定义它的求值环境。 这里的 sqrt_update 函数自带了一些数据:a 在定义它的环境中引用的值,因为它以这种方式“封装”信息,所以局部定义的函数通常被称为闭包(closures)。
函数作为返回值
带有词法作用域的编程语言的一个重要特性就是,局部定义函数在它们返回时仍旧持有所关联的环境。
这是将上面提到的闭包整体作为返回值的一种手段,project Hog里有个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def make_test_dice(*outcomes):
"""Return a die that cycles deterministically through OUTCOMES.
>>> dice = make_test_dice(1, 2, 3)
>>> dice()
1
>>> dice()
2
>>> dice()
3
>>> dice()
1
>>> dice()
2
This function uses Python syntax/techniques not yet covered in this course.
The best way to understand it is by reading the documentation and examples.
"""
assert len(outcomes) > 0, 'You must supply outcomes to make_test_dice'
for o in outcomes:
assert type(o) == int and o >= 1, 'Outcome is not a positive integer'
index = len(outcomes) - 1
def dice():
nonlocal index
index = (index + 1) % len(outcomes)
return outcomes[index]
return dice
对于这个函数的返回值dice,每次调用dice都会返回列表里的下一个元素.这是因为列表与index信息都被保存在dice这个闭包里了.与c++在这一点很不同,c++的函数并不会绑定额外的信息,要想实现这种功能,只能通过类来实现.可以想象成python将返回的函数看做是一个类了
不定参数
对于函数传递参数的数量不确定的情况下,可以使用(*args)
作为参数.比如下面的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
def printed(f):
def print_and_return(*args):
result = f(*args)
print('Result:', result)
return result
return print_and_return
printed_pow = printed(pow)
printed_pow(2, 8)
>>> 256
printed_abs = printed(abs)
printed_abs(-10)
>>> 10
函数柯里化
我们可以使用高阶函数将一个接受多个参数的函数转换为一个函数链,每个函数接受一个参数。更具体地说,给定一个函数 f(x, y),我们可以定义另一个函数 g 使得 g(x)(y) 等价于 f(x, y)。在这里,g 是一个高阶函数,它接受单个参数 x 并返回另一个接受单个参数 y 的函数。这种转换称为柯里化(Curring)。
例如
1
2
3
4
5
6
def curried_pow(x):
def h(y):
return pow(x, y)
return h
curried_pow(2)(3)
8
我们也可以写一个自动柯里化函数的函数
1
2
3
4
5
6
7
8
9
10
11
12
def curry2(f):
"""返回给定的双参数函数的柯里化版本"""
def g(x):
def h(y):
return f(x, y)
return h
return g
def uncurry2(g):
"""返回给定的柯里化函数的双参数版本"""
def f(x, y):
return g(x)(y)
return f
Lambda表达式
语法
1
2
lambda x : f(g(x))
"A function that takes x and returns f(g(x))" 这个表达式的返回值是一个匿名函数,具体使用如下 ```python s = lambda x: x*x s(12) ``` lambda表达式并没有什么特殊的作用,只是写起来简单一些,并不推荐将其应用在复杂的函数上,因为会导致我们的代码阅读困难,尤其是复核lambda,`compose1 = lambda f,g: lambda x: f(g(x))`
抽象与一等函数
一般而言,编程语言会对计算元素的操作方式施加限制。拥有最少限制的元素可以获得一等地位(first-class status)。这些一等元素的“权利和特权”包括:
- 可以与名称绑定
- 可以作为参数传递给函数
- 可以作为函数的结果返回
- 可以包含在数据结构中
- Python 授予函数完全的一等地位,由此带来的表达能力的提升是巨大的。
函数装饰器
python提供了一种特殊的解释def的功能,装饰器本身是一个接受当前定义的函数作为参数的函数,这个函数将对当前定义的函数进行一些封装等操作
def trace(fn):
def wrapped(x):
print('-> ', fn, '(', x, ')')
return fn(x)
return wrapped
@trace
def triple(x):
return 3 * x
triple(12)
<function triple at 0x102a39848> ( 12 )
triple 的 def 语句有一个注解(annotation) @trace,它会影响 def 执行的规则。和往常一样,函数 triple 被创建了。但是,名称 triple 不会绑定到这个函数上。相反,这个名称会被绑定到在新定义的 triple 函数调用 trace 后返回的函数值上。代码中,这个装饰器等价于:
1
2
3
def triple(x):
return 3 * x
triple = trace(triple)
数据结构与类和对象
序列
序列(sequence)是一组有顺序的值的集合,是计算机科学中的一个强大且基本的抽象概念。序列并不是特定内置类型或抽象数据表示的实例,而是一个包含不同类型数据间共享行为的集合。也就是说,序列有很多种类,但它们都具有共同的行为。特别是:
- 长度(Length):序列的长度是有限的,空序列的长度为 0。
- 元素选择(Element selection):序列中的每个元素都对应一个小于序列长度的非负整数作为其索引,第一个元素的索引从 0 开始。
list
- 用法
- 创建:
list = [1,2,3]
,res = [[] for _ in range(lengthen)]
- 取值:
a = list[0]
b = list[-1]
c = getitem(list,2)
- 长度:
length = len(list)
- 合并:
[5,6]+list*2
>>> [5,6,1,2,3,1,2,3]
- 嵌套:
pairs = [[1,2],[4,5]]
- 遍历 1. while遍历:采用index遍历 2. for遍历
for it in list
- 序列解包:
for x,y in pairs
范围(ranges):
list(range(5,8))
>>> [5,6,7]
for _ in range(8)
用来循环8次对解释器而言,这个下划线只是环境中的另一个名称,但对程序员具有约定俗成的含义,表示该名称不会出现在任何未来的表达式中。
ps 对于遍历时可能修改该序列的情况,直接遍历这个容器会导致出错,比如说遍历到特定元素并删除他,一个比较好的解决方式是将遍历的对象改为当前容器的赋值,比如下面这样
for bee in bees[:] if bee.hp ==0: remove(bee)
- 修改
- append(elem): Add elem to the end of the list. Return None.
- extend(s): Add all elements of iterable s to the end of the list. Return None.
- insert(i, elem): Insert elem at index i. If i is greater than or equal to the length of the list, then elem is inserted at the end. This does not replace any existing elements, but only adds the new element elem. Return None.
- remove(elem): Remove the first occurrence of elem in list. Return None. Errors if elem is not in the list.
- pop(i): Remove and return the element at index i.
- pop(): Remove and return the last element.
序列处理
- 列表推导:许多序列操作可以通过对序列中的每个元素使用一个固定表达式进行计算,并将结果值保存在结果序列中。在 Python 中,列表推导式是执行此类计算的表达式。 ```python odds = [1, 3, 5, 7, 9] [x+1 for x in odds]
[2, 4, 6, 8, 10]
1
2
3
4
5
6
[x for x in odds if 25 % x == 0]
>>> [1, 5]
# 嵌套推导
x = [[sublist[i+1] - sublist[i] for i in range(len(sublist) - 1)] for sublist in p]
``` - 聚合:序列处理中的第三种常见模式是将序列中的所有值聚合为一个值。内置函数 sum、min 和 max 都是聚合函数的示例。 - 高阶函数:序列处理中常见的模式可以使用高阶函数来表示。首先可以将对序列中每个元素进行表达式求值表示为将某个函数应用于序列中每个元素。 ```python def apply_to_all(map_fn, s):
return [map_fn(x) for x in s] ```
序列抽象
除去list与range,还有两个扩展序列抽象的行为
- 成员资格: 测试某个值在序列中的成员资格,通过in与not in判断,返回True,False.就是判断是否在序列里面
x in list
>>> True
- 切片:一个切片是原始序列的任意一段连续范围,由一对整数指定。和 range 构造函数一样,第一个整数表示起始索引,第二个整数是结束索引加一,第三个代表步长.如果起始索引或结束索引被省略则默认为极值:当起始索引被省略,则起始索引为 0;当结束索引被省略,则结束索引为序列长度,即取到序列最后一位。
1 2 3 4 5 6
>>> digits[0:2] [1, 8] >>> digits[1:] [8, 2, 8] >>> digits[::-1] #逆序遍历 [8,2,8,1]
字符串
Python 中文本值的内置数据类型称为字符串(string),对应构造函数 str
。 字符串字面量(string literals)可以表示任意文本,使用时将内容用单引号或双引号括起来。例如"Hello,world!"
,'你好,世界'
字符串同样满足上面的序列操作,比如len,取元素等
- 成员资格:与其他序列不同的是,字符串匹配的是子字符串,而不是序列元素.
1 2
'here' in "Where's Waldo?" True
- 多行字面量: 也就是说字符串变量可以跨越多行.
- 强制转换:显式调用str
1 2
str(2) + ' is an element of ' + str(digits) '2 is an element of [1, 8, 2, 8]'
树
树的存储可以通过列表实现.
数据抽象
抽象屏障
在实现某一功能的时候,使用抽象层次高的函数代替直接调用低级函数或类的成员会是一个更好的习惯.换言之,选择一个依赖更小的实现方式,这样未来在修改逻辑的时候,需要维护的代码最少.比如实现平方功能的时候,调用mul(x,x)而不是返回num(x)*num(x)
可变数据
对象的引入
对象 (objects) 将数据的值和行为结合到了一起。对象可以直接表示某些信息,也可以用自身的表现行为来表达想表达的东西。一个对象具体应该怎么和其它对象进行交互,都被封装并绑定到了该对象自身的某些值上。当我们试图打印某个对象时,它自己知道应该如何以文字的形式表示自己。如果一个对象由多个部分组成,它知道应该怎么根据实际情况对外展示那些不同的部分。对象既是数据信息又是操作流程,它把二者结合到一起,从而表达复杂事物的属性、交互和行为。
python中所有的值都是对象(验证了我前面的猜测), 即所有的值都有行为与属性
序列对象
像数字这种基本数据类型实例属于不可变的,比如将其作为参数传入函数的时候是不可以修改的.但是列表是可变的. 一个对象可以通过某些操作改变自身属性
- 数据共享与身份: 就是说所有的名称绑定都是类似出++的引用,而不是赋值.比方说
1 2 3 4 5
a = [1,2,3] b = a b.append(5) >>> a >>> [1,2,3,5]
要想避免这种情况,可以使用构造器函数对其复制,
b = list(a)
1 2 3 4 5 6 7 8 9
>>> nest = list(suits) # 复制一个与 suits 相同的列表,并命名为 nest >>> nest[0] = suits # 创建一个嵌套列表,列表第一项是另一个列表 >>> suits.insert(2, 'Joker') # 在下标为 2 的位置插入一条新元素,其余元素相应向后移动 >>> nest [['heart', 'diamond', 'Joker', 'spade', 'club'], 'diamond', 'spade', 'club'] >>> nest[0].pop(2) 'Joker' >>> suits ['heart', 'diamond', 'spade', 'club']
尽管两个列表的元素值相同,但他们仍然可能是完全不同的两个列表对象,所以我们需要一个机制来验证两个对象是否相同。Python 提供了 is 和 is not 两种比较操作符来验证两个变量是否指向同一个对象。
1 2 3 4 5 6
>>> suits is nest[0] True >>> suits is ['heart', 'diamond', 'spade', 'club'] False >>> suits == ['heart', 'diamond', 'spade', 'club'] True
is是用来检验对象的内存地址,==用来判断内容是否相同
列表推导式
列表推导式总会返回一个新列表。举例来说,unicodedata 模块记录了 Unicode 字母表中每个字符的官方名称。我们可以通过字符名称找到对应的 unicode 字符,包括卡牌花色。
1
2
3
>>> from unicodedata import lookup
>>> [lookup('WHITE ' + s.upper() + ' SUIT') for s in suits]
['♡', '♢', '♤', '♧']
元组
tuple
指的是不可变的序列,例如(1,2,3)
,括号不是必须得,但一般都会加上. 有count,len,index
等和list相似的用法,但是涉及到修改列表的都不能用
字典
相当于c++里面的unordered_map,key首先是不可变变量,key之间不能相同.key一般都是字符串,且一个key只能对应一个value ps: py3.7以上版本的字典会确保为插入顺序,对键的更新也不会影响顺序,删除后再次添加会加在末尾
- key不可变: 因为这与python查找value的方式有关,是按照key的值到内存中找的,相当于一种指针,那显然是不能改变值的
get方法,相当于map的at . numerals.get('A',0) >>> 1
,如果找不到返回第二个参数(默认值)
- 推导式语法: 其中,key 和 value 使用冒号分隔。字典推导式会创建一个新的字典对象。
1 2
>>> {x: x*x for x in range(3,6)} {3: 9, 4: 16, 5: 25}
局部状态
列表和字典具有局部状态,代表在程序执行的过程中的某个时间点需改自身的值
>>> def make_withdraw(balance):
"""返回一个每次调用都会减少 balance 的 withdraw 函数"""
def withdraw(amount):
nonlocal balance # 声明 balance 是非局部的
if amount > balance:
return '余额不足'
balance = balance - amount # 重新绑定
return balance
return withdraw
将balance声明为给局部变量,说明绑定的是外面的参数balance,函数在每次执行的时候都会修改balance的值,也就是说,他是非纯函数 非局部语句(nonlocal statement)会改变 withdraw 函数定义中剩余的所有赋值语句。在将 balance 声明为 nonlocal 后,任何尝试为 balance 赋值的语句,都不会直接在当前帧中寻找并更改 balance,而是找到定义 balance 变量的帧,并在该帧中更新该变量。如果在声明 nonlocal 之前 balance 还没有赋值,则 nonlocal 声明将会报错。
ps: nonlocal是只在嵌套函数里才能用,指向上一层的变量.而global是在普通的函数里面,指向全局变量的
- Python 特质: 这种非局部赋值模式是具有高阶函数和词法作用域的编程语言的普遍特征。大多数其他语言根本不需要非局部语句。相反,非局部赋值通常是赋值语句的默认行为。 Python 在变量名称查找方面也有一个不常见的限制:在一个函数体内,多次出现的同一个变量名必须处于同一个运行帧内。因此,Python 无法在非局部帧中查找某个变量名对应的值,然后在局部帧中为同样名称的变量赋值,因为同名变量会在同一函数的两个不同帧中被访问。此限制允许 Python 在执行函数体之前预先计算哪个帧包含哪个名称。
c++ 在函数内可以直接访问全局变量,pyhton就不行,需要声明这个变量是外部的
非局部赋值的好处
没看懂,后面有时间补上吧 教材的对应位置 TODO
数据处理
隐式序列
迭代器
python里的迭代器可以由两个内置函数进行简单访问
- iter(iterable) 获取一个容器的迭代器,处于第一个元素前面.有点像rend
- next(iterator) 获取该迭代器后面的一个元素,并且本身向后移一位
- list(iterator) 从该迭代器后面的所有元素组成的列表.相当于一直next到最后的所有元素
如果在末尾继续next,会触发stopiterator异常 在迭代器上调用 iter 将返回该迭代器,而不是其副本。 Python 中包含此行为,以便程序员可以对某个值调用 iter 来获取迭代器,而不必担心它是迭代器还是容器。
内置迭代器
这些函数将可迭代值作为参数,返回一个迭代器 map
函数是惰性的:调用它时并不会执行计算,直到返回的迭代器被 next
调用 相反,会创建一个迭代器对象,如果使用 next
查询, 该迭代器对象可以返回结果.具体效果有点类似c++标准库里的transforme
>>> def double_and_print(x):
print('***', x, '=>', 2*x, '***')
return 2*x
>>> s = range(3, 7)
>>> doubled = map(double_and_print, s) # double_and_print 未被调用
>>> next(doubled) # double_and_print 调用一次
*** 3 => 6 ***
6
>>> next(doubled) # double_and_print 再次调用
*** 4 => 8 ***
8
>>> list(doubled) # double_and_print 再次调用兩次
*** 5 => 10 *** # list() 会把剩余的值都计算出来并生成一个列表
*** 6 => 12 ***
[10, 12]
生成器与yeild语句
生成器是由一种特殊类型的函数* 生成器函数 *返回的迭代器。 生成器函数与常规函数不同之处在于,它们在其主体内不包含 return 语句,而是使用 yield 语句来返回一系列元素。
在迭代的时候,每次在调用next后,都会开始执行生成器函数,直到yeild语句.注意,下次调用next不是重新执行,而是继续执行上一次的生成器函数.举个例子
>>> def letters_generator():
current = 'a'
while current <= 'd':
yield current
current = chr(ord(current) + 1)
>>> for letter in letters_generator():
print(letter)
a
b
c
d
yield语句就表明了我们正在定义一个生成器(迭代器的一种),因此我们不需要手动定义_next_()和_iter_()函数,python会自动在调用next的时候返回yield的值
TODO
面相对象编程(oop)
创建类
定义语法
创建一个类的语法为
class <name>
<suit>
类中的函数
- 构造函数 和c++一样,python的类也有一个构造函数
class Account: bank = "CNB" def __init__(self, account_holder): self.balance = 0 self.holder = account_holder >>> a = Account('Kirk') 实例化
- bank是类属性,而balance是实例属性,他们的区别是类属性对于所哟类的实例是相同的,而每个实例都有自己的实例属性. 类属性类似于c++的静态成员, 可以独立于实例,直接通过类名访问
- init函数中第一个参数是类的实例,第二个是实例化时传入的参数
- 方法 同样的,首个参数也必须是self.方法的调用是通过实例后面加
.
调用的 - 内置函数
- hasattr(class,attr),判断类里面是否具有这个属性
- getattr(class,attr), 获取类里属性的值
- 函数与方法 在使用方法的时候,是通过实例的点表达式调用的,不需要传递第一个参数,py会自动将点左边的值作为第一个参数.实际上,调用这个方法有两种方式
>>> Account.deposit(spock_account, 1001) # 函数 deposit 接受两个参数 1011 >>> spock_account.deposit(1000) # 方法 deposit 接受一个参数 2011
命名约定 类名采用驼峰命名,函数采用下划线命名
- 属性赋值 对于类的属性赋值对所有没有对这个属性赋值的实例生效,对实例赋值就不会有其他的影响.
继承
语法
class <name>(<basename>)
<suit>
特性
所有属性都会被继承,除了子类重写的那些属性与方法.py在查找类的属性的时候,会从下到上递归的查找,直到找到这个属性为止
举例
- 基类定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
class Account: """一个余额非零的账户。""" interest = 0.02 def __init__(self, account_holder): self.balance = 0 self.holder = account_holder def deposit(self, amount): """存入账户 amount,并返回变化后的余额""" self.balance = self.balance + amount return self.balance def withdraw(self, amount): """从账号中取出 amount,并返回变化后的余额""" if amount > self.balance: return 'Insufficient funds' self.balance = self.balance - amount return self.balance
- 继承类定义
1 2 3 4 5 6
class CheckingAccount(Account): """从账号取钱会扣出手续费的账号""" withdraw_charge = 1 interest = 0.01 def withdraw(self, amount): return Account.withdraw(self, amount + self.withdraw_charge)
- 调用父类 重写的属性可以通过类对象来访问。例如,我们通过调用 CheckingAccount 中包含 withdraw_charge 参数的方法 withdraw 。该方法的实现是通过调用 Account 中的 withdraw 方法来实现的。请注意,我们调用了 self.withdraw_charge 而不是等效的 CheckingAccount.withdraw_charge 。前者相对于后者的好处是,从 CheckingAccount 继承的类可能会覆盖 withdraw_charge 。如果是这种情况,我们希望我们的实现的 withdraw 找到新值而不是旧值。 ```python3 class child(parent): def act(self): super().act() # 这里直接调用父类方法 self.do_something()
1
2
3
4
5
6
7
8
#### 多继承
Python 支持子类从多个基类继承属性的概念,这种语言功能称为多重继承(multiple inheritance).举个栗子
```py3
>>> class AsSeenOnTVAccount(CheckingAccount, SavingsAccount):
def __init__(self, account_holder):
self.holder = account_holder
self.balance = 1 # 赠送的 1 $!
那么有个和c++中多继承相似的问题,如何解决两个基类间的冲突问题?python的解决办法是通过一个c3算法来决定采纳哪个基类的内容 对于这样的继承关系,Python 会从左到右解析名称,然后向上解析名称。在此示例中,Python 按顺序检查以下类中的属性名称,直到找到具有该名称的属性:
AsSeenOnTVAccount, CheckingAccount, SavingsAccount, Account, object
对象抽象
对象抽象的核心概念是泛型函数,会涉及到一些类型转换,共享接口等.联想c++的模版与函数重载
字符串转换(针对多种对象类型的函数设计方法)
python中规定,任何对象(py中所有值都是对象)都应该具有两个字符串值,一个是给人看的(就是直接在交互界面输出返回的值),另一个是py可解释表达式 str()
返回第一种值,而repr()
函数返回第二种.举个栗子
>>> from datetime import date
>>> tues = date(2011, 9, 12)
>>> repr(tues)
'datetime.date(2011, 9, 12)'
>>> str(tues)
'2011-09-12'
那么问题来了,repr函数的参数可能是任何对象,这想要定义repr
这个函数是不可能的.那么这种情况下,py是如何实现这个函数的呢. 在这情况下,对象系统提供了一种优雅的解决方案:repr
函数总是在其参数值上调用一个名为 __repr__
的方法。
1
2
>>> tues.__str__()
'2011-09-12'
通过在用户定义类中实现这个相同的方法,我们可以将 repr
函数的适用范围扩展到将来我们创建的任何类。这个例子突出了点表达式的另一个优势,那就是它们提供了一种机制,可以把现有的函数的作用域扩展到新的对象类型.这种函数称之为多态函数,即会根据输入类型的不同有多种表现.
总的来说,python解决泛型编程的方法就是在每种需要的类里面加入这个函数,在调用函数的时候,会直接调用对应类里面相应的函数.与c++相比就是把集中的函数重载和运算符重载分散到每个对象里面了
专用方法
在 Python 中,某些特殊名称会在特殊情况下被 Python 解释器调用。例如,类的
__init__
方法会在对象被创建时自动调用。__str__
方法会在打印时自动调用,__repr__
方法会在交互式环境显示其值的时候自动调用。 在 Python 中有一些为其他行为而准备的特殊名称。下面介绍其中某些常用的。 比方说平时使用的len函数实际是调用__len__()
的内置函数,以及在if xxx :
做判断的时候,是调用了内置的__bool__
方法,注意没有小括号.在对一个序列取值的时候如list[2]
,实际是调用了__getitem__
方法 对于算数运算也是通过特定函数实现的,相比于c++的运算符重载,这里是通过调用左侧值的__add__
方法与右侧值的__radd__
方法完成运算
多重表示
这个设计问题来自于大型系统中常常希望对于同一个对象有多种表示,比如对于复数就有普通的实数虚数表示,还有弧度表示.联想到我最近的毕设,一个图里的节点可以有两种表示,一种是他的名称,一种是拓扑排序的index.前者在画图的时候用,后者在执行图算法的时候用 就拿这个复数来说,在实现的时候会有下面这样的问题.
>>> class Complex(Number):
def add(self, other):
return ComplexRI(self.real + other.real, self.imag + other.imag)
def mul(self, other):
magnitude = self.magnitude * other.magnitude
return ComplexMA(magnitude, self.angle + other.angle)
在计算加法的时候,第一种表示方式好用,在计算乘法的时候明显是第二种好用,但是我们不能同时存两种表示方式吧,一个好的解决方法是按照一种方式存储,同时保留计算到另一种表示形式的方式. 我的毕设部分就通过一个映射来解决的这个问题.复数转换可没法映射,下面是解决代码
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> from math import atan2
>>> class ComplexRI(Complex):
def __init__(self, real, imag):
self.real = real
self.imag = imag
@property
def magnitude(self):
return (self.real ** 2 + self.imag ** 2) ** 0.5
@property
def angle(self):
return atan2(self.imag, self.real)
def __repr__(self): # 记得上一节提到的专用方法吗
return 'ComplexRI({0:g}, {1:g})'.format(self.real, self.imag)
这是按照正常方式存储的,在需要的时候,可以通过magnitude和angle方法计算弧度制. @property 修饰符允许函数在没有调用表达式语法(表达式后跟随圆括号)的情况下被调用。 即在调用示例的magnitude属性的时候,会返回对应函数计算后的值.相应的,会有弧度制存储的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> from math import sin, cos, pi
>>> class ComplexMA(Complex):
def __init__(self, magnitude, angle):
self.magnitude = magnitude
self.angle = angle
@property
def real(self):
return self.magnitude * cos(self.angle)
@property
def imag(self):
return self.magnitude * sin(self.angle)
def __repr__(self):
return 'ComplexMA({0:g}, {1:g} * pi)'.format(self.magnitude, self.angle/pi)
实现完表示如下
1
2
3
4
5
>>> from math import pi
>>> ComplexRI(1, 2) + ComplexMA(2, pi/2)
ComplexRI(1, 4)
>>> ComplexRI(0, 1) * ComplexRI(0, 1)
ComplexMA(1, 1 * pi)
这种通过接口实现的多重表示可以针对不同的类型开发对应的函数,它们只需要就它们共享的属性名称和和对于这些属性的行为条件达成一致。接口还具有可添加性。如果程序员想要添加一个复数的第三方表现形式到同一个程序中,他们只需要创建一个拥有相同属性名称的类即可。
泛型编程
因为python是动态类型,所以实际上一个函数的输入可以是多种类型组合. 那么为了多种输入类型都能有正确的输出, 可以在函数内部检测输入的类型,进而分发到专门的函数中 下面是通过定义专用函数动态分派类型来实现复数与有理数之间的计算
class Number:
def __add__(self, other):
if self.type_tag == other.type_tag:
return self.add(other)
elif (self.type_tag, other.type_tag) in self.adders:
return self.cross_apply(other, self.adders)
def __mul__(self, other):
if self.type_tag == other.type_tag:
return self.mul(other)
elif (self.type_tag, other.type_tag) in self.multipliers:
return self.cross_apply(other, self.multipliers)
def cross_apply(self, other, cross_fns):
cross_fn = cross_fns[(self.type_tag, other.type_tag)]
return cross_fn(self, other)
adders = {("com", "rat"): add_complex_and_rational,
("rat", "com"): add_rational_and_complex}
multipliers = {("com", "rat"): mul_complex_and_rational,
("rat", "com"): mul_rational_and_complex}
关于作业
测试命令最后加一个 –local可以不用登录伯克利的账号
总结
看完现代c++之后又学完这门课,发现收获还是很大的.这门课最重要的不是py的语法,更像是教会你一门语言,尤其是环境帧, 高阶函数中对一等对象的解释, 以及泛型编程部分最为精彩.学完之后更能理解这门语言的设计思路而不是简单的去使用它. 不过或许这两门课对我来说最大的收获可能是习惯直接阅读英文材料了: )