更换ssh证书导致paramiko报No session existing错误

问题出现

现在公司的发布系统使用了paramiko来执行远程操作,ssh连接用的证书被记录在配置文件里,是一个列表的形式。没错,我们的证书有很多,用来连接到不同的环境。

接到运维通知,由于安全原因,访问某台机器使用的证书做了更换。

随后没多久,就收到测试同学的反馈,发布代码时系统提示“No existing session”。

简单问题简单解决

这个问题乍看上去只要更新一下配置就可以了,简单的一批。

修改配置文件,重启系统,又重试了一次。结果,还是报这个错误。疯狂了疯狂了,配置也生效了,竟然还是不能连接吗?

问题初探

连接的代码很简单,就是单纯的连接远程机器。

1
2
3
4
5
6
7
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(host, SSH_PORT, SSH_USER, SSH_PASSWORD, key_filename=SSH_KEYS)

# ...

client.close()

通过测试,发现用命令行连接远程机器是可以的,而且也没有使用ProxyCommand之类,但是通过paramiko就出问题。毫无疑问,问题出在paramiko或者配置上。

session关键字让这个问题看起来像是paramiko的锅,它没处理好自己的会话吗?

一番google,发现了这个链接:https://stackoverflow.com/questions/6832248/paramiko-no-existing-session-exception。这个哥们也是遇到了一样的错误提示,但是他用了密码而系统查找私钥导致出错,关掉密钥查找就解决了这个问题。

看来问题出在密钥的配置上。

开启paramiko日志

发布系统使用的是paramiko 2.1.2,在paramiko的源码里找到了打开日志的方法。

paramiko/util.py:238

1
def log_to_file(filename, level=DEBUG):

通过该函数,可以将paramiko的日志打印到某个文件,比如paramiko.util.log('/tmp/paramiko-debug.log')。打印的日志如下所示:

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
28
29
30
31
32
.transport: starting thread (client mode): 0x8ab46350L
.transport: Local version/idstring: SSH-2.0-paramiko_2.1.2
.transport: Remote version/idstring: SSH-2.0-OpenSSH_5.3
.transport: Connected (version 2.0, client OpenSSH_5.3)
.transport: kex algos:[u'diffie-hellman-group-exchange-sha256', u'diffie-hellman-group-exchange-sha1', u'diffie-hellman-group14-sha1', u'diffie-hellman-group1-sha1'] server key:[
.transport: Kex agreed: diffie-hellman-group1-sha1
.transport: Cipher agreed: aes128-ctr
.transport: MAC agreed: hmac-sha2-256
.transport: Compression agreed: none
.transport: kex engine KexGroup1 specified hash_algo <built-in function openssl_sha1>
.transport: Switch to new keys ...
.transport: Adding ssh-rsa host key for [10.0.2.243]:36000: 2e3faf53075afccf09a3ac2q391dd5e8
.transport: Trying key 3a96e7e9e3f59963fbee693f44x8cf58 from /home/user/.ssh/id_rsa1
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 8628c2021979e0e0302aac6bc8xcbcef from /home/user/.ssh/id_rsa2
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 6e399bc162a737150e1bd643abx7737d from /home/user/.ssh/id_rsa3
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 77d22314df154bfd5ca591bd16x19363 from /home/user/.ssh/id_rsa4
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key 77d22314df154bfd5ca591bd16x19363 from /home/user/.ssh/id_rsa5
.transport: userauth is OK
.transport: Authentication (publickey) failed.
.transport: Trying key c1909f00bf62c74eed5a3a9e5dxa73d1 from /home/user/.ssh/id_rsa6
.transport: userauth is OK
.transport: Disconnect (code 2): Too many authentication failures for user
.transport: Trying key c897a8e51a0d41facf7fa712a2xeace8 from /home/user/.ssh/id_rsa7
.transport: Trying discovered key 3a96e7e9e3f5996xfbee693f44b8cf58 in /home/user/.ssh/id_rsa1

我们几乎可以一眼看到最后的错误提示:“Disconnect (code 2): Too many authentication failures for user”。看起来像是失败次数过多被拒了。google后得到了这个链接:https://superuser.com/questions/187779/too-many-authentication-failures-for-username。“This is usually caused by inadvertently offering multiple ssh keys to the server.”,答主建议明确指定使用哪个证书来访问服务器。

我down了一份OpenSSH_7.8的源码下来,经过一番grep,找到了下面这几个代码片段。

ssh/auth.c:281

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void
auth_maxtries_exceeded(Authctxt *authctxt)
{
struct ssh *ssh = active_state; /* XXX */

error("maximum authentication attempts exceeded for "
"%s%.100s from %.200s port %d ssh2",
authctxt->valid ? "" : "invalid user ",
authctxt->user,
ssh_remote_ipaddr(ssh),
ssh_remote_port(ssh));
packet_disconnect("Too many authentication failures");
/* NOTREACHED */
}

ssh/auth2.c:322

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
userauth_finish(struct ssh *ssh, int authenticated, const char *method,
const char *submethod)
{
// ...
if (authenticated == 1) {
/* turn off userauth */
// ...
} else {
/* Allow initial try of "none" auth without failure penalty */
if (!partial && !authctxt->server_caused_failure &&
(authctxt->attempt > 1 || strcmp(method, "none") != 0))
authctxt->failures++;
if (authctxt->failures >= options.max_authtries)
auth_maxtries_exceeded(authctxt);
// ...
}
}

ssh/srvconf.h:39

1
#define DEFAULT_AUTH_FAIL_MAX   6       /* Default for MaxAuthTries */

原来ssh默认允许尝试6个密钥,如果还没有成功,就会直接退出了。这么做的原因想来也显而易见了。

解决办法

我的临时解决办法是,将密钥文件的配置控制在6个以内。当然,更好点的办法是使用fabric来替代直接使用paramiko,并复用系统的.ssh/config配置,这就是后面需要考虑的了。

使用subprocess模块异步并发执行远程命令

远程执行命令

运维自动化平台不可避免地会涉及到远程命令执行操作,主要分为两类主要做法:目标机器安装agent,或者使用ssh。

saltstack是一个典型的agent模式的远程控制工具,麻烦的地方是首先要在目标机器上安装saltstack的agent。

使用ssh的模块居多,fabric和ansible是此类工具中的典型,这类工具的优点是方便,不用在目标机安装agent。值得一提的是,这两个工具都是基于paramiko。

使用ssh执行的另一种做法是,直接调用本地的ssh来完成。缺点是针对每一台远程主机都需要开一个进程,比起在程序内建立连接要耗费更多的资源。优点也很明显,比如,公司最近在做ssh的改造,在改造的过程中,可能会出现明明ssh命令可以连接,使用第三方模块就是不灵的情况。ssh的命令和第三方模块在配置上毕竟有差异,需要维护两份配置。

这篇文章就是要使用进程来完成在批量远程机器上执行某个命令。

subprocess模块大概

subprocess模块是python2中的一个官方模块,顾名思义,它主要用于操作子进程。

subprocess.Popen()用于创建一个子进程,它的第一个参数是列表,即创建进程所需要的参数,类似c语言中的argv。

需要注意的一点是,Popen是异步执行的,也就是说创建Popen后会立刻返回,子进程继续执行。关于异步,还有很多可以聊的地方,后面另开一篇文章写一下。

既然是异步的,我们就需要某种机制来跟它进行通信。Popen提供了多个方式来与子进程通信。

Popen.wait()将主程序挂起等待子进程的结束,结束后会返回子进程的返回码。

Popen.poll()可以检测子进程是否还在执行,并返回子进程的返回码。

下面我们将用几个小例子来不断扩展,最终实现一个可在多台远程机器并行执行命令的功能。

一个简单的远程执行示例

我们来写一个简单的示例,说明如何使用subprocess模块远程执行一条命令。

1
2
3
4
5
6
7
8
9
10
11
import subprocess


cmd = 'echo hello'
cmd_arg = ['ssh', 'localhost', cmd]

process = subprocess.Popen(
cmd_arg, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
retcode = process.wait() # 0
out = process.stdout.read() # 'hello'
err = process.stderr.read() # ''

我们首先创建了一个Popen对象,然后等待这个子进程的执行结束,获取返回值和输出。

这断代码很简单,注意stdout=subprocess.PIPE,我们想捕捉到子进程的输出,而不是直接打印到标准标出,所以要求Popen把输出打印到管道供我们读取。

同时在多个机器上执行命令

这里我们利用了Popen的异步特性,来加快多服务器任务的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import subprocess


cmd = 'sleep 3 && echo hello'
cmd_arg1 = ['ssh', 'localhost', cmd]
cmd_arg2 = ['ssh', '127.0.0.1', cmd]

process1 = subprocess.Popen(
cmd_arg1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process2 = subprocess.Popen(
cmd_arg2, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

retcode1 = process1.wait()
retcode2 = process1.wait()

这次,我们连接了两个机器并执行耗时3s的操作,由于Popen是异步的,这个脚本的实际执行时间仍然是3s。这个地方的操作是不是与多线程的操作有点类似,哈哈。

多个机器执行获取执行时间

由于网络环境和机器配置的不同,我们在不同机器的执行时间是有差别的。有时候,我们需要一个时间数据来了解哪个机器执行耗时比较久,以此做优化。

这时,我们需要Popen.poll()来异步检测命令是否执行完成,而不是将主进程挂起等待第一个子进程。如果第一个子进程执行时间比第二个子进程要长,我们就获取不到第二个子进程的执行时间。

为了更好的可读性,这里我们没有加入for循环之类的结束,放弃了部分逻辑上的灵活性。

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
28
29
30
31
32
import time
import subprocess


cmd = 'sleep 3 && echo hello'
cmd_arg1 = ['ssh', 'localhost', cmd]
cmd_arg2 = ['ssh', '127.0.0.1', cmd]

process1 = subprocess.Popen(
cmd_arg1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process2 = subprocess.Popen(
cmd_arg2, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

process1_ok = False
process2_ok = False

start_time = time.time()
while True:
if process1_ok and process2_ok:
break

if not process1_ok:
if process1.poll() is not None:
process1_ok = True
print 'process 1 finished, delta time:', time.time() - start_time

if not process2_ok:
if process2.poll() is not None:
process2_ok = True
print 'process 2 finished, delta time:', time.time() - start_time

time.sleep(1) # 防止空跑导致cpu占用过高

执行输出为:

1
2
process 1 finished, delta time: 4.01682209969
process 2 finished, delta time: 4.01691508293

最后的执行时间多了1s,这是因为我们做了一个time.sleep(1)操作。由于我们考查的是哪台机器影响了整体的耗时,而且远程任务执行时间远不止1s,所以这里的1s不影响我们的判断。当然,适当缩小time.sleep()的参数也是可以的。

封闭一个可并行在多台服务器执行的函数

我们将上面的脚本封闭一下,形成一个可以复用的函数。有点长,但是只是在之前基础上做了一些简单的操作,并没有增加什么高深的东西。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def run(hostname, command, user=None):
"""
使用ssh执行远程命令

hostname 字符串或数组,多个hostname使用数组
"""
if not isinstance(hostname, list):
hostname = [hostname]
else:
hostname = list(set(hostname))

# 创建执行命令子进程
processes = {}
for h in hostname:
if user is None:
user_at_hostname = h
else:
user_at_hostname = '{0}@{1}'.format(user, h)

cmd_args = ['ssh', user_at_hostname, command]
processes[h] = subprocess.Popen(
cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

# 等待子进程结束,获取结果
start_time = time.time()
result = {}
while True:
running_hostnames = set(processes.keys()) - set(result.keys())
if len(running_hostnames) == 0:
break

for h in running_hostnames:
process = processes[h]
retcode = process.poll()

if retcode is None:
continue

status = STATUS_SUCC if retcode == 0 else STATUS_FAIL
out, err = process.stdout.read(), process.stderr.read()
delta_time = time.time() - start_time

result[h] = {
'out': out,
'err': err,
'status': status,
'delta_time': delta_time
}

time.sleep(1)

r = {
'ts': time.time(),
'cmd': command,
'result': result,
}

return r

celery使用fabric出现大量ssh -W进程

公司的运维平台有很大一部分是python写的。

发布系统不可避免的要与远程机器做交互,我们选择了基于paramiko的fabric模块来完成这部分工作。

由于发布过程中存在大量耗时很久的任务,所以选用了celery来执行异步任务。有一部分远程操作是通过celery的任务调用fabric来完成的。

出现大量ssh -W

在某一天,突然接到运维同学的反馈,说生产环境存在大量ssh -W进程,希望可以查一下出现的原因。

通过定位,很快确认了是ssh连接远程时使用了ProxyCommand,导致出现了ssh -W进程,这些进程实际上是ssh连接到远端的代理。

但是奇怪的是,为什么任务执行完后仍然存在大量ssh -W不能正常退出。

后来通过阅读fabric的源代码,在代码里找到了答案。

fabric/state.py

1
2
3
4
5
#
# Host connection dict/cache
#

connections = HostConnectionCache()

fabric/network.py

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class HostConnectionCache(dict):
"""
...
"""
def connect(self, key):
"""
Force a new connection to ``key`` host string.
"""
from fabric.state import env

user, host, port = normalize(key)
key = normalize_to_string(key)
seek_gateway = True
# break the loop when the host is gateway itself
if env.gateway:
seek_gateway = normalize_to_string(env.gateway) != key
self[key] = connect(
user, host, port, cache=self, seek_gateway=seek_gateway)

def __getitem__(self, key):
"""
Autoconnect + return connection object
"""
key = normalize_to_string(key)
if key not in self:
self.connect(key)
return dict.__getitem__(self, key)


# ...


def disconnect_all():
"""
Disconnect from all currently connected servers.

Used at the end of ``fab``'s main loop, and also intended for use by
library users.
"""
from fabric.state import connections, output
# Explicitly disconnect from all servers
for key in connections.keys():
if output.status:
# Here we can't use the py3k print(x, end=" ")
# because 2.5 backwards compatibility
sys.stdout.write("Disconnecting from %s... " % denormalize(key))
connections[key].close()
del connections[key]
if output.status:
sys.stdout.write("done.\n")

原来,fabric默认会维护一个连接缓存,并且不会主动断开这些连接,直到脚本结束垃圾回收时自己断开。但是由于使用了celery,celery是常驻进程不会退出,导致fabric不能自己主动释放连接,所以连接一直在,ssh -W也一直在。

知道原因,就很好解决了。可以主动调用network.disconnect_all()来释放连接,或者干脆就保持着连接,以待下次使用。

需要注意的是,HostConnectionCache是一个全局变量,在celery这类常驻进程中使用还是存在一些坑的。

cpu飙升到100%

知道原因后,我做了一个kill操作,手动将ssh -W杀死了。这时候,再次向之前的目标机发送命令时,ssh应该会报错,因为代理被干掉了。

意料之中,系统再次执行命令时果然提示“broken pipe”。

但是,奇怪的是,运行celery的机器,cpu占用率达到了100%。这就有点奇怪了。

查找原因无果,在github上提交了一个issue https://github.com/paramiko/paramiko/issues/1277。ploxiln提供了另一个关联issue,表明这是paramiko库的一个bug,通过链接可以查找到patch文件。

到此,问题都已解决。

吹牛逼的艺术

写下这个题目,我是非常心虚的。因为我不会吹牛逼。

不过我想起了之前做过的一些软件系统,它们最开始看上去确实其貌不扬,但是经过不断地优化改进,最终的效果非常令人满意。

所以我决定写这么一篇其貌不扬的文章,或者说给自己看的一份指南,就像手机使用指南一样。不同的是,这份指南不提供产品,因为我们指导的其实就是我们自己。然后,我来慢慢地把它优化改进,让它变成一份更有参考价值的指南。

不得不说,吹牛逼是一件很有技术含量的事。

为什么我们需要吹牛逼

我觉得我们是需要吹牛逼的,就像我们需要手机。

对于个人,吹牛逼能将我们丰富的内心世界跃然嘴前。为了不让这么一个美丽的心灵埋没,我们必须要学会吹牛逼的能力。

对于他人,吹牛逼能把我们的神奇经历传授于众,让人们的生活更丰富。

如何吹牛逼

要想吹一个满分的牛逼,需要做到下面几点:

  1. 必须要有吹牛逼的资本。吹自己擅长的,不吹自己不擅长的(这时要吸)。
  2. 吹牛逼一定要吹实,不吹虚。如果吹虚不务实,再牛的逼也会被吹破,让自己陷入尴尬的境地。
  3. 吹牛逼一定要打草稿。不打草稿的牛逼,往往是吹到一半就卡壳泄了气。
  4. 平日一定要注意自己的言行。要时刻牢记,注意当下的言行,以为我们日后吹牛逼积攒资本。
  5. 勤学苦练。我觉得这个在最开始是最难的,因为最开始可吹的牛逼是很少的。这是个量变到质变的过程,不可强硬,顺其自然。
  6. 从小事练起。吹牛逼非一日之功,而且要注意从一些微不足道的地方练起,从走路吃饭说话写字练起,吹可吹与不可吹之牛逼。

唠唠python(9) -- 基础概念的梳理

温顾而知新,可以为师矣。

温顾知新,可以为师。当然,我们的目标不是为师。

时常复习一下自己学习到的东西,是一种不错的习惯。就像牛的反刍,复习是对已有知识的梳理,这个过程既轻松,又能更好的巩固我们的所学,而且时常还可以关注到自己之前因匆忙而遗漏的东西。

当然,这篇文章草草看一下也是可以的,有很多东西是不需要一次掌握的。

当我们都意识不到有这么一个概念时,实际上已经掌握个七八分了,只需要再稍微整理一下思路,就可以轻松掌握了。这就是所谓的质变吧?

术语

我们首先得说说“术语”这个词。

“术语”听起来给人的第一印象是吓人,又是什么神奇的正常人看不懂的东西?

为什么会有术语这种神奇的东西呢?

人类的语言是很重要的,从诞生以来,它大大提高了人们沟通的效率。但是随着语言的发展,同一个词都衍生出了多种意思,而在使用不当的情况下,语言恰恰又阻碍了人们的沟通。

怎么来避免这种一词多义的情况呢,于是人们发明出了术语这么个东西。术语的特点在于,在某种特定的领域或上下文中,同一个术语是精确的,它会被多方理解成同一种意思,这样人们就达成了共识。

说白了,术语就是为了使大家沟通的过程尽可能少的产生歧义。

为了达到这种目的,术语就不得不避开我们平日经常会用到的词。这就导致术语看起来稀奇古怪,对我们不那么友好。实际上,它是为了精确的沟通而作了妥协,我们要体谅。

字符串

1
print "hello, I'm a string"

我们之前说,print可以显示“一句话”。实际上,“一句话”是非常不准确的,因为一句话有可能是我们听到的一段录音,也有可能是文章中的文字。我们可以找个更明确的词。

在计算机中,一个字母a叫做一个字符。顾名思义,就是一个符号,它代表一个字母。就像我们的羊肉串是把很多羊肉串起来,我们之前所说的“一句话”是由一个或多个字符组成的,故叫做字符串。

“a”是字符串,”abc”也是字符串。

变量

1
2
s = "hello, I'm a string"
print s

根据我们之前的解释,s类似于一个箱子,它可以把我们的字符串”hello, I’m a string”放进去,在使用的时候(如print s)再从里面取出来,这样就达到了类似箱子一样的暂存功能。

这里的箱子只是一种类比,在语言中有一个术语,叫做“变量”,这个术语听起来怪怪的,不那么直观。它实际上是从数学里借来的概念(说借并不合适,计算机本就是数学的一个分支),意思是没有固定的值,可以改变的数。到计算机里,它也可能是字符串或其它类型的东西。

这里的“变”很好理解,一个箱子可以放衣服,也可以放鞋袜。

当然,有变量就会有常量。比如我们的文具盒只能放铅笔像皮,放不下衣物。python是一门简洁的语言,在语法上是没有定义常量的,所以我们可以不需要纠结“常量”的概念,在这里提到是为了帮助我们理解“变量”这个概念。

功能

我们说洗衣机把洗衣服的很多步骤打包封装起来,只留给我们一个按钮开关。“洗衣机有洗衣服的功能”,虽然听起来有点白痴,但它确实是个事实。

功能的概念是很好好理解的。它比较接近我们的生活。

在计算机中也有一个概念,叫做“函数”,它是比功能更准确的一种描述。这也是一个从数学中借用过来的概念,它并非我们日常生活中提到的功能,而就是单指语言中所提供的对步骤的封装。

类是使用了生物学中的概念。

生物学中对生物有“界门纲目科属种”的不同精细程度的分类,如豹是猫科,豹属,再细分下来是豹种。计算机语言相对于生物学分类,它只有类的概念,但它提供了继承,即豹可以继承自动物,所以在计算机语言中同样可以提供如生物学分类中那样复杂的从属关系。

类中有类似变量和函数的概念,习惯上被称为属性和方法,以区分我们上面谈到的变量和函数。

实际上,类在计算机里并不是必要的。在一些古老的计算机语言里,甚至都不存在类这个概念。

使用类来抽象现实世界的编程方式,又叫做面向对象编程。而不使用类,仅使用函数和变量的编程方式,叫做面向过程编程。前者更接近人类的思考模式,但需要我们掌握更多的概念。

我们之前在操作表格文件时,用到过一个openpyxl包。它是一些类和函数文件的集合。

为了更精确地描述,我们有时会把包称作“库”。我们通常会把从网上找到的库叫做第三方库。

那哪些是第一方库和第二方库呢?有点意思,哈哈。

我们同样可以把单个文件作为类和函数等的集合。通常我们会把单文件的包叫做模块,比如之前的示例:

1
2
3
import os

print os.name

为了演示方便,我们使用了一个随python语言打包的模块,叫做os。它实际上就是一个模块。

碎碎念

想要装一个完美的x,熟悉并掌握这些概念是必须的。

不过不必急于求成,就算我们不掌握这些概念,对我们的学习也没有太大影响。

有很多优秀的东西,我们是可以潜移默化地吸收的。可能突然有一天,我们回头时猛然发现,以前不明不白的概念,竟然已经被我们彻底理解了。

在那之前,我们可以继续愉快地叨叨。

唠唠python(8) -- 使用脚本代替手工数据操作

这个问题已经过了很多天了,都记不太清细节了。

先捋一捋我们的需求。

需求

待处理表格

这个表格有点熟悉,好像在之前的文章里看到过。我们还是以它为例子。

我们有一张如上的物料订单表格。每两行对应一种物料,即需要的料数和订购的料数。不同的列对应不同时期需要的物料数量。

因为供应商提供给我们的物料都是以10000为单位的,所以就算我们只需要9颗料,也要订购10000颗料。也就是以10000为单位,对料数上取整。

我们看到其中有一些表格的数量是红颜色,它表示这些数据已经被手工填写了。每个单元格的料数以10000为单位上取整后,被填入它下面的一个单元格。

计算公式

我们在之前的文章里已经给出了订单数量的计算公式:(n+10000-1) / 10000 * 10000,其中n为需要的实际数量。

功能清单

我们列一下需要的功能清单:

  1. 打开或保存excel文件
  2. 读取或写入单元格

再不需要其它的功能了。

上一篇讲到的openpyxl很好地实现了我们需要的这些功能。

打开excel使用workbook = openpyxl.load_workbook("excel文件位置"),保存使用workbook.save("保存到的路径")

读取某个单元格使用print sheet["B5"].value,写入某个单元格使用sheet["B5"] = 30000。其中sheet是我们使用workbook.active获取的工作簿里的第一张表格。

几点逻辑

我们需要对上面的表格中,I3到S28的奇数行数据查看一遍,还需要根据单元格的内容和它下面一个单元格的内容来判断是否需要处理单元格数据。

挨个查看一遍我们会用到一个这样的语句:

1
2
for row in range(3, 30, 2):
处理row行数据

这条语句的意思是,row分别等于3到28(不包含28)的奇数,然后依次执行下面的语句。后面的2是说,从3开始,取相隔为2的数。

这里有一点需要注意,我们的行号是数字表示的,但是列号是字母,我们需要先把字母转换成数字,在处理每一列时,再把数字转换成字母。庆幸的是,python已为我们准备好这些了。

1
2
3
4
for row in range(3, 30, 2):
for col in range(ord("I"), ord("U")+1, 1):
col_char = chr(col)
处理(row, col_char)单元格数据

ord()是将一个字母转化为数字,chr()是将数字转化回字母。注意我们给ord(“U”)加了一个1,因为ord(“U”)是不包含U的。我们再一次与python的作者来了一场完美的合作。

判断是否需要处理单元格,我们可以使用下面这条语句:

1
2
if sheet["I3"].value is not None and sheet["I4"].value is None:
处理I3单元格

意思也很好懂,翻译过来就是,如果I3单元格不是空的,并且它下面的I4单元格是空的,我们就处理I3单元格,并将处理结果填充到I4单元格。

实现脚本

说了这么多,其实脚本已经差不多了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import openpyxl


workbook = openpyxl.load_workbook("a.xlsx")
sheet = workbook.active

def count_bill(n):
return (n+10000-1) / 10000 * 10000

for row in range(3, 30, 2):
for col in range(ord("I"), ord("U")+1, 1):
col_char = chr(col)

cell_src_idx = col_char + str(row)
cell_dst_idx = col_char + str(row + 1)

if (sheet[cell_src_idx].value is not None and
sheet[cell_dst_idx].value is None):
n = int(sheet[cell_src_idx].value)
sheet[cell_dst_idx] = count_bill(n)

sheet.save("b.xlsx")

14行代码,就实现了我们的需求。它读取当前目录下的a.xlsx文件,并根据我们之前所说的规则,处理指定范围内的表格数据,并将处理后的结果保存到一个新文件b.xlsx。

它真正将我们从枯燥无味的繁重工作中解放了出来,终于可以安心地睡个好觉了。

更好的脚本

当然,它还不够好,我们在这篇文章里不打算再把它变得更好,因为这个过程是很漫长的。但是它现在已经能帮我们节省比付出多几十倍甚至几百倍的时间。

它不够好,还能做怎么样的改进呢?

第一,里面全是代码,代码相对于人类自己的语言来说总没那么直观,毕竟我们是在迁就计算机嘛。

第二,每次执行时都要修改代码(输入输出文件名和单元格范围),十分之不友好。我们希望它可以让我们通过文件选择框选择某个文件,然后用鼠标拖选出我们要处理的范围,并询问我们要保存到哪个文件。

以上两点是显而易见的,特别是第二点。

不过最紧急的事情完成了,这个脚本已经可以帮我们节省下大量时间,以实现我们上面所说的改进了。

总结

终于把处理表格的傻逼工作交接给了计算机,可以舒一口气了。

为了不加重我们的心理负担,文章里有很多地方只是一带而过。至少对现在这个阶段来说,不需要知道的东西都是不重要的。如果哪里感到困惑,恭喜,这说明我们并不满足现状,想要一个更清晰的结果。

下一篇,我们先不急着改进上面说的两点不足,它们已经不那么紧急了,我们先来回顾一下之前提到的内容,并重新整理一下知识结构,让他们形成一个清晰的脉络,以便为以后装逼打下坚实的基础。

叶脉

碎碎念

零零散散地终于把这个脚本完成了,是时候回过头来梳理一下了。

小金约定,两天或两天以上的假期,每天一篇文章,失约100红包。本是想逼迫自己,竟然有时候写的不易乐乎,到了凌晨2、3点钟还在想读者会不会读懂这句话在说什么。

作者从读者的角度思考,读者又反过来从作者的角度思考,想想真的是一件很有趣的事情。

不得不说,写作的过程很快乐,虽然可能没有什么人看,但它至少已经帮助到了我。希望可以尽自己的一点微薄之力,让碰巧看到的人能生活得更轻松。

唠唠python(7) -- 包

这是我们打基础的最后一节。下一节,我们就来兑现之前许下的承诺,用脚本来实现表格的自动处理。

这里所说的包就是《你的背包》的包,是对背包概念的一种模拟。

这两天小郭去武汉玩,为了途中更方便一点,我就把我的背包给她用了(其实是我一直在用她的背包😁)。我担心公司有一些事务要处理,所以一直都保持着背笔记本回家的习惯。最近几天都是手拿笔记本,十分的不方便。

所以你看,包还是很重要的。

我们一般会把一些平时经常用到的物品放到书包,如果我们有多个包,则会把同类物品放到同个包。比如我们把纸笔放进背包里,只要背着这个背包出去,就可以在需要的时候写字了,而且背在肩上很方便。

python里的包也是类似的用途。

python中的包

互联网的兴起,让全球的人都可以有机会合作,大大提高了人们的工作效率。

实际上,python中的包还有另外一个异常重要的作用:复用。比如,我们之前说的表格处理的例子,世界上有很大一部分人都会用到表格处理的功能,如果大家每人做一套工具来操作表格(比如读取或写入表格中的某个单元格),成本很高,不利于人类知识的叠加。有句话叫站在巨人的肩膀上,说得很有道理。

那怎么做呢?

把我们的功能合集打个包,而且只把人们要用的东西提供出来。举个例子,我们要读取某个单元格的数据(比如我们要读取B5单元格的数据),只需要sheet["B5"].value就可以了,具体怎么获取的,交给那个精通表格文件处理的人就可以了,我们不需要理会。

背包很好理解,但python中的包听起来就有点玄乎了。python中的包到底是什么东西呢?或者说,它的外在表现是什么呢?

我们之前把洗衣服的操作封装成一台洗衣机,叫做功能。又把人的说学逗唱的功能和人的姓名等属性封装成一个人类的概念,叫做类。包其实就是一个功能和类的合集,也就是说,它里面既包含有功能,又包含有类。说白了,其实就是把我们之前所有的代码放到一块。

使用包

之前我们讨论类的时候,自己亲自写了一个类来更深入的体会类的概念。

现在我们使用包,再来亲自手写一个包?

这次我们不写了,我们之前谈到封装,其实就是把细节隐藏起来,而让一个东西更好用。比如一台洗衣机,我们只需要学会按按键就可以了。如果我们读了洗衣机的使用说明还不知道洗衣机怎么用,只能说这台洗衣机做得太烂了,或者说交互太差了。

我们直接来使用一个包体验一下:

1
2
import os
print os.name

执行上面两条命令后,会显示当前系统的名称。如在mac下,会显示”posix”。它是怎么获取的,我们并不关心,只要返回的名称正确,我们就认为,嗯,小伙子干得不错。

import意思是使用,我们请求使用os包,并要求显示我们os的name。

读取表格文件

我们已经来到了这里:读取表格文件。

只要能用语言精确表达出来的功能,大部分都很简单,而且非常有可能已被其他人实现了。表格处理就是这样。

现在有一个问题,既然这个包有可能已经被实现了,那我们怎么获取这个包呢?

我们知道肯定不止一个包可以做这种操作。一般我们会在网上搜索大量资料,然后对比选取符合我们需求的包。

假设我们已经阅读并对比了大量资料,发现了一个可以读写表格文件的库,叫做openpyxl

现在我们要准备安装这个包了。这是一个最好的时代,只要确定了使用哪个包,安装极其简单,我们甚至都不用关心去哪里下载,下载完成后要做什么,它会不会依赖其它的包,等等等等这些问题。python已为我们准备好了安装的方式。

1
$ pip install openpyxl

是的,就是这么一条简单的命令。然后稍许的等待,这个包就安装成功了。真的方便极了,美好的东西总能让我们心情愉快。我们甚至也不用关心它被安装到了哪,直接用就可以了。

想想如果我们用表格处理软件查看B5单元格的数据。先打开目标表格文件,然后翻到第一张表格(一个表格文件对应一个工作簿,里面可能有多张表格),找到B5单元格,并查看内容。

1
2
3
4
5
6
import openpyxl

workbook = openpyxl.load_workbook('a.xlsx')
sheet = workbook.active

print sheet["B5"].value

就这么简单的几行,就把B5单元格的内容显示出来了。

结语

现在我们已经可以获取表格中的某个单元格了,写入跟获取是同样简单的。下一篇我们来看如何写入,以及如何来实现我们之前的物料取整问题。

终于可以不用对着成百上千的数据心力交瘁,提心吊胆了。可以有更多时间喝咖啡了。想想好激动。

碎碎念

晚上9点30,写完了,可以去吃饭了,哈哈。

老板来一个煎饼果子,一盒水果,一个鸡腿。街边小吃让人愉悦。

想起申总的一句话:不吃街边小吃的人,不知道要丧失多少乐趣。

读《巨婴国》

读完这本书已经有两个月了。其间恰好碰上过年,我回到山东老家,冬天冰冷的雪夜里,或是乍暖还寒的清晨,看着捧在光下发黄的屏幕,别有一番滋味。

巨婴国封面

第一次看到封面上奇形怪状的小人时,以为是一本漫画书。这本书读起来很轻松,思考起来又有点沉重。

书中有很多很有意思的观点,比如谈到“普遍缺席的父亲”,“把儿子视为权利争夺战筹码的母亲”。我对这些并不感冒,我的父母给了我足够的爱与自由。

我感触最深的一点,是讲到“中国式好人”。武志红说他自己就是一个典型的“中国式好人”,可能大部分人读到都会产生共鸣吧,毕竟“中国式的好人”占了绝大多数,因为有集体主义在压着。

我们总是有意无意地被灌输集体主义的思想,一切为了集体,要牺牲小我成就大我。这其实就是在压制自己,因为自私是人的天性。

从达尔文的进化论来看,生物的进化,靠的是物竞天择,而不同物种如何竞争?

我们在初中时就已经知道了,即使是单细胞生物,也懂得“趋利避害”的道理。当然,说懂得是过分了,它们不用懂得,竞争会把基因里存在“趋利避害”特性的物种筛选出来。

我身边有很多人,包括过去的我,都不好意思为自己争取利益,唯恐被人说闲话,或者自己道德上过不去。

书中也提到一个原因,如果我们把自己的某部分毁掉,比如我们不为自己争取利益,那我们就站在了道德的制高点,“我把本属于自己的一部分都不要了,你还怎么来指责我?”但越是打压自己的天性,我们的能量就会变得越萎缩。

不自私的人还容易碰到的一个问题是,述情障碍。我们接受的教育是,个人要服从集体,这导致我们很难感受到自己的感觉。自私的人是坏的,是可耻的。所以我们拒绝自己自私的部分,我们不想让自己的那部分阴暗被别人看到,被拒绝的部分就陷入了黑暗。

“中国式好人”不懂得拒绝。一切为了他人,导致自己最后活得很累。而且情况往往是,我们一边帮助他人,一边却又在抱怨,因为我们并不是因为感觉到了他们的困难,而是集体主义教导我们要帮助他人,不然自己就太自私了。

“中国式好人”没有自己的个性。个性是对集体的背叛,在一个以集体为生活单位的环境下,被集体排斥是很恐怖的一件事情。所以很多人为了融入集体,干脆泯灭了自己的个性。

自私是个好东西。只有能充分感知到自己的感受,我们才能对别人的处境感同身受,也才能真正发自内心地想去帮助他人。

自私对公司的管理一样有很大的好处。吴军在讲到公司管理时引入了一个数学概念,我觉得很好,叫做“利益最大化函数”,即我们将公司的发展与员工的利益绑定,员工为自己获得最大利益的同时,也可以为公司做出最大的产出。但是,如果一群人都是一群集体主义下催生的僵尸,没有感情,没有创意,打一鞭子就动一动,这样的人,做出的产出能有多少?

“老油条”应该就是这么来的吧?

读《编写可读代码的艺术》

感谢公司的图书角,让我免费读到了这样一本好书。

用kindle已经有一段时间了,也陆陆续续地在上面买了一些电子书。但慢慢地发现,还是读纸质书来得舒服,翻起来也方便。当然,最好是自己买的书,可圈可点,不喜欢的书还可以拿来擦屁股。

然后我就联想到了李敖的读书秘诀,给大家摘一段体会一下:

我李敖看的书很少会忘掉,什么原因呢?方法好。什么方法?心狠手辣。剪刀美工刀全部用到,把书给分尸掉了,就是切开了。这一页我需要,这一段我需要,我把它按类别分开来。那背面有用怎么办呢?把它影印出来,或者一开始就买两本书,把两本书都切开以后整理出来,把要看的部分分类留存。结果一本书看完了,这本书也被分尸掉了。这就是我的看书方法。

虽然有点偏激,但至少说明,读书要活读,要跟书互动,产生参与感,还要温故知新,常翻常新。纸质书可以做到,电子书总归不那么方便。

《编写可读代码的艺术》,一个书名就已经道出了它接下来的目标了。谈到艺术,大多时候是从微小处着眼,细细体味,而非一个hello world那么简单。有生活,有哲学,代码也该像一件精心雕琢的艺术品。

这本书的结构已经非常清晰了,再做总结总显啰嗦。下面是我读这本书时想到的,并非笔记。

想人所想

这个标题看起来一点都不酷,但是做起来甚至会感动到我们自己。我们不想看到队友因为看不懂代码而苦恼的样子,我们的用户已经疲惫不堪,我们不想让他们再承受着蹩脚的使用体验。

写代码和写文章是极其类似的,作者要站在读者的角度思考问题。

读者读到这个词时是否会理解它,会不会产生歧义,是否需要了解某个背景,这段话是不是多余,会不会太啰嗦。

作者要思考比读者多得多的东西,因为作者这个角色本身,是包含了读者角色的双重角色。

站在再高一点,从系统用户的视角来看,这个字段的描述用户是否能看懂,是否正确理解了它的用途,有没有必要填写,用户是否可直观地了解有哪些注意事项,是否知道在做什么,当用户看到这个提示时是否知道它在说什么,blahblahblah一大坨,慢慢体会。

我们即是作者,又是读者;即是开发者,又是维护者;即是产品,又是用户。

遵循惯例

遵循惯例可以看作是“想人所想”的一部分。

每个行业都有自己的惯例。伴随着行业的发展,大家在相互配合中,整理出了一些行之有效的通用规则。

理想的情况是,这些通用规则,本行业的人,大家耳熟能详。

既然大家耳熟能详,就很少存在误解,也可能更好地被理解。这无疑对读者更友好。

当然,惯例也有可能带来相反的效果。《编写可读代码的艺术》里提到过一个例子,get_xxx()按惯例来说就该是O(1)复杂度的函数,该书作者坚信这一点,结果该函数并非O(1)函数,导致作者在优化速度时吃了点苦头。

不过就像爱情一样,惯例是有的,你坚信,准没错,至少它能保证你在80%的情况下是正确的。这很值。

讲究卫生

一张干净的脸总是讨人喜欢的。

记得小时候家里不富裕,还穿过打补丁的裤子。老师教导我们,裤子破了打补丁很正常,但即使打了补丁,也要把裤子洗干净。一个漂亮的补丁,甚至会给一条旧裤子增色,就是这样。

在我的理解里,讲究卫生意味着整洁和干净。注意变量的出场顺序,把相关的代码或变量分块,都是一些好习惯。

好的代码就是要看上去赏心悦目。

可能有的人觉得,只要让系统运行起来就万事大吉了,很明显,这是不负责任。

我维护过一些系统,杂乱且无用的旧代码像战场上的残骸,任性的空格空行天马行空放荡不羁,拷贝的痕迹随处可见。这些都会加重读者的心理负担。

当然,我们也有能力写出这样的代码,所以,可以安静下来想想,怎么对待自己。

提升技能

书中提到一点我觉得很受用:偶尔地翻一下语言内置的和常用的第三方库。

维护代码需要很大成本,甚至我们的大部分时间都是在做维护的工作。以我之前做的“代码发布系统”为例,开发用了一个月,后期的维护已经达到8个月之久,而且还会持续下去。

而大部分第三方库都已经过严格测试,而且可以节约我们的维护成本。

有的程序员喜欢重造轮子,出于学习目的或现存库不能满足需求还好,如果是为技术而技术,可要在关键时刻提醒一下自己啰。

屏蔽细节

最后一点同样重要,帮助我们的读者隐藏不必要的细节。

函数的一个好处是,它把繁杂的上下文剥离开,让理解变得简单。比如,send_mail_html(mail_address, html)这个函数可以给指定邮箱地址发送一段html类型的文本,只要函数封装地够好,用户或主调函数无需关心它的实现细节。

这也是自下而上编码的好处,上层提供了比下层更高一级的操作接口,并为用户屏蔽下层的实现细节。从这个意义上来说,编程语言也是自下而上中的一层实现,语言的细节我们无需关心,只要它封装地够好且使用无歧义。

再往大了说,一个好的团队也应该有屏蔽彼此细节的能力,团队中的成员由不同的领域专家组成,大家的职责彼此正交。这时,如果你是一个虚拟化技术的专家,而我更擅长用系统来为大家提供帮助,如果我的系统需要申请一个临时环境,调用你的一个接口,我可以获得一个可用的ip,你怎么生成这个环境我不关心,我怎么用你也不用关心。再这时,如果我们要改变容器的管理方式,这对我们的用户是透明的,也就是说,他们甚至都不知道我们在做这一操作,用户只需关心他们所应关心的内容即可。

函数的使用亦如是,彼此一体却又边界清晰。

总结

编码如是,生活亦如是。

唠唠python(6) -- 事物的边界和内在

上一篇文章,我们讲到可以将一批操作打包成一个“功能”,从而屏蔽其内部实现的细节。

这篇文章,我们将对屏蔽细节做出进一步的努力,并让计算机的世界更符合我们的直观感受。

边界的重要性

昨天走在街上,看到一只动物,长得个绵羊样,体型却小得多。想起之前看到狗伪装成熊猫的样子,我就在心想,这可能也是一只演技派超强的狗。

在平日里,我们会有意无意地对看到的事物进行分类,这有很多好处。

首先,它能减少我们大脑的记忆量,屏蔽具体事物的一些不必要的细节。比如,我们现在肚子很饿,想吃点东西。我们发现路边有一个服装店,正常的人类,不用走进去就知道,里面没有我们急需的东西:食物。

其次,分类可以帮助我们更快地认识新事物。迎面奔来一只恶狠狠地狗,经验告诉我们,狗是会咬人的。所以,我们远远地躲开了它。狗喜欢骨头,喜欢吃屎,只要是一只狗,就可以对它的这些行为做出判断。

既然分类这么重要,如何进行分类呢?

凭直觉,可以。我们长这么大,没吃过猪肉也见过猪上树,看到长得像猪,八成就是猪了。

当然,凭直觉有时不那么灵。我就没凭直觉一眼看出那只迷你小绵羊其实是一只狗,而且直觉不方便数字化,我们无法直观地向计算机描述我们的直觉。

所以,除了直觉,我们应该还有其它的分类方法。

我们可以通过事物之间可识别的独特区别来区分事物,也就是通过事物之间的边界。比如,男人有鸡鸡,而女人没有,只要检查一下一个人有没有鸡鸡,就能判断他是个男人还是女人了。只要交给计算机这条规则,计算机也能很容易地判断一个人的性别了。当然,反之亦然,我们猥琐地猜想眼前的这个男人是有鸡鸡的。

划分边界

我们平时接触最多的是人,叫做“人类”。我们就以人为例,来想一下我们“人类”的一些事情。

首先,人有名字,有出生年月,有属于一个人具体的属性。

其次,我们知道,人可以说话,可以写字,可以吃喝拉撒。我们可以把这些看作一个人所具有的“功能”。这里的功能,其实就是我们上一篇所说的“功能”。比如,吃可能是由一系列的动作组成的,把食物塞到嘴里,咀嚼,下咽,blahblah…一系列的动作,我们把它们打包成一个功能,就叫做“吃”。

再次,当我们说“人类”的时候,实际上,我们在说的是一个概念,一个通过边界区分出来的分类,而并非特指老王。而当我们讨论老王的时候,我们就有了更具体的信息,我们不仅知道他有名字,还知道他叫“老王”,而且知道他出生于1962年(这让我们觉得“老”字合情合理)。很有意思的一点是,我们之前说的吃喝拉撒,老王全会,而且做起来跟其他人差别不大(不像名字那样千差万别)。老王就是人类具体化后的一个对象。

介绍隔壁老王

现在我们知道了更多隔壁老王的秘密,下面我们来做一些更有意义的事:把老王介绍给计算机。

听起来怪怪的,我们先来做点稍微正常点的事,把老王介绍给楼上老赵先。

“老赵,早啊。这位是我的隔壁,叫老王。60年代的人,跟你差不多大,以后有人陪你下象棋了。”

嗯,这个介绍还算正常。

我们试着再迈出一步,把老王介绍给计算机。

这时,有一个问题出现了。老王是个人,但计算机并不知道人是个什么东西。首先,我们需要跟计算机解释人是什么。我们不需要严格的人的定义,而只要梳理一下人自身有哪些信息,可以做哪些事情就可以了。

人有名字,有出生年月,可以说话,可以写字,可以吃喝拉撒。

我们只要把这些规则简单地转化成代码:

1
2
3
4
5
6
7
8
9
10
11
12
class Person:               # 定义人类
name = '' # 人类有名字
birthday = '' # 而且有生日

def speak(self): # 人类会说话
pass

def write(self): # 会写字
pass

def trivia(self): # 吃喝拉撒样样精通
pass

这里有几个新东西。class 意思是“定义一个类”。pass 是为了先占个位,我们在这里还不想涉及到 说话 的细节。

我们在这里看到了上一篇讲到的“功能”,这里的功能跟上一篇的“功能”并无二致。在执行这几个功能时,会传入一个 self,我们可以先暂时不予理会。

好了,现在计算机已经知道人类是个什么东西了,可以介绍老王了。

“老王是一个出生于1962年的老头。”

1
2
3
4
5
mr_wang = Person()              # 有一个人
mr_wang.name = '老王' # 他的名字叫“老王”
mr_wang.birthday = '1962-10-13' # 出生于1962年10月17日

print mr_wang.name # 打印老王的名字

这时,计算机就知道老王是怎么一回事了。尽管有点明知顾问,我们最后还是打印了一下老王的名字,计算机显示了一个“老王”。

想获取老王的个人信息,使用这种方式就可以了:mr_wang.name,很简单,对吧?

重新理解老王

上面的解释很容易懂,但是有几一点没有说清楚。mr_wang = Person() 意思是“有一个人”?

很明显这是一个动作,而不是一种状态(有一个人)的描述。

为了更清楚地了解 mr_wang = Person() 的实际意义,我们来回想一下钥匙的制作过程:如果我们的钥匙丢了,要去重新配一把,来到配钥匙的地方,会发现老师傅那里有很多没齿的钥匙。这时,老师傅会根据我们那把正常钥匙的齿,来打磨一把新的钥匙给我们。mr_wang = Person() 就是取没齿的钥匙的过程。打磨的过程就是将一个对象的特殊性添加到这个对象中的过程。

也就是说,我们先从人这个类里生成一个新的人(没有姓名、没有生日),然后把老王的数据打磨到这个人里(具体化)。

开口说话

我们之前说人类可以说话,但上面这个例子并不能讲话。我们来修改一下代码,让老王可以自报家门:

1
2
3
4
5
6
7
8
9
class Person():
...
def speak(self):
print "Hello, I'm", self.name
...

...

mr_wang.speak()

为了方便说明,这里只显示了关键代码。我们实现了之前 speak() 功能里的 pass 部分,使用了之前使用过的 print

这里有几点要说明:

  1. self 是我们之前留下的小尾巴,现在可以清晰地看到,self 实际上是实例的别称。当我们通过 mr_wang.speak() 来使用 speak() 功能时,self 就是我们亲爱的老王。
  2. 我们发现,使用一个对象的功能,和使用一个对象属性形式一下,只是多了一个功能所特有的 ()

小结

需要说明一点的是,计算机中的类很少是用来分类的,因为大部分情况下,我们已经提前知道一个对象是人还是狗了。它主要是用来划分边界,让我们意识到,这个对象有哪些信息,可以做什么。

小结不应该太多废话,我们把之前的代码整理一下,看懂即可,下载链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 定义人类
class Person:
name = ''
birthday = ''

def speak(self):
print "Hello, I'm", self.name

def write(self):
pass

def trivia(self):
pass

# 具体化一个人,并写入老王的信息
mr_wang = Person()
mr_wang.name = '老王'
mr_wang.birthday = '1962-10-13'

# 让老王开口说话
mr_wang.speak()

尾注

嗯,到现在为止,我们已经忽略了很多不需要知道的细节。

但可能在实操的过程中,我们的好奇心没有停下来,也或者因为小手一抖敲错个空格,而让问题变得非常复杂。我的建议是,保存好有错误的代码,然后重新来过,毕竟到现在为止,长点的代码也不过20行。

错误都非常有价值,多犯不同的错误会让我们更有经验。当你完成了整篇文章的内容,就可以回过头来重新查看之前的错误了。询问旁边的人,或者借助互联网,都是不错的选择。

在完成一个重要的里程碑后,我们会重点整理一下之前最有可能产生误解的点。

最后,毫无疑问的是,如果你发现某一句话没看明白,或者实操时老出问题,那一定是我出问题了。请给我一个改正的机会。