小黄

黄小黄的幸福生活!


  • 首页

  • 标签

  • 分类

  • 归档

  • Java

Dubbo框架学习总结

发表于 2019-04-21 | 分类于 Java , Dubbo , RPC

Dubbo的定义

官方定义:Apache Dubbo是一个由阿里提供开源的基于Java的高性能的RPC框架。遵循RPC框架的设计原则,Dubbo也定义了服务的概念,可以远程根据参数和返回值来调用具体的方法。在服务侧,服务侧实现这个接口并运行一个Dubbo服务来处理客户端的请求,在客户端保存有服务端相同的方法存根。

Dubbo有三个主要特性:

  • 基于接口的远程的调用
  • 容错和负载均衡
  • 服务的子自动注册和发现。

Dubbo相关的文档:
Dubbo用户手册
Dubbo开发手册
Dubbo管理手册
Dubbo GitHub

Dubbo结构

Alt text

  1. 服务提供者-对外暴露自己的服务,服务提供者会将自己提供的服务注册到注册中心
  2. 容器-初始化服务,加载服务,运行服务
  3. 服务消费者-调用远程服务,会向注册中心订阅他需要的服务。
  4. 注册中心-服务注册与发现
  5. 监控中心-记录服务相关的数据,比如服务的调用频率,调用次数等

服务提供者,服务消费者和注册中心的连接是持久的,所以当一个服务提供者挂掉之后,注册中心可以检测到服务提供者的失效并将此信息通知给服务消费者

注册中心和监控中心是可选的,服务消费者可以直接连接到服务提供者,但是这样会影响整个系统的稳定性,不推荐直接连接

调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己需要的服务。
  4. 注册中心返回服务提供者列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者经调用,如果调用失败,再选一台调用。
  6. 服务消费者和提供者,在内存中累积调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

Dubbo架构的特点

连通性

  • 注册中心负责服务地址的注册于查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发消息,压力较小。
  • 监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示。
  • 服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销。
  • 服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销。
  • 注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心不是。
  • 注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者。
  • 注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地魂村了提供者列表。
  • 注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。

健壮性

  • 监控中心宕机不影响使用,只是丢失部分统计数据。
  • 数据库宕机,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
  • 注册中心对等集群,任意一台宕机之后,将自动切换到另一台
  • 注册中心全部宕机后,服务提供者和服务消费者仍能通过本地换粗通讯
  • 服务提供者无状态,任意一台宕机后,不影响使用。
  • 服务提供者全部宕机之后,服务消费者应用将无法使用,并无限次重连等待服务提供者回复。

伸缩性

  • 注册中心为了对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心
  • 服务提供者无状态,可动态增加及其部署实例,注册中心将推送新的服务提供者信息给消费者。

升级性

  • 当服务集群规模进一步扩大,带动IT治理结构进一步升级,需要实现动态部署,进行流动计算,现有的分布式架构不会带来阻力,下图为一种可能结果:

Alt text

Dubbo依赖要求

  • 工具版本要求:

    • JDK:6及以上
    • Maven:3及以上
  • Maven依赖配置:

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
</dependency>
  • Dubbo缺省依赖以下库:
1
2
3
4
[INFO] +- com.alibaba:dubbo:jar:2.5.9-SNAPSHOT:compile
[INFO] | +- org.springframework:spring-context:jar:4.3.10.RELEASE:compile
[INFO] | +- org.javassist:javassist:jar:3.21.0-GA:compile
[INFO] | \- org.jboss.netty:netty:jar:3.2.5.Final:compile
  • 可选依赖
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
netty-all 4.0.35.Final
mina: 1.1.7
grizzly: 2.1.4
httpclient: 4.5.3
hessian_lite: 3.2.1-fixed
fastjson: 1.2.31
zookeeper: 3.4.9
jedis: 2.9.0
xmemcached: 1.3.6
hessian: 4.0.38
jetty: 6.1.26
hibernate-validator: 5.4.1.Final
zkclient: 0.2
curator: 2.12.0
cxf: 3.0.14
thrift: 0.8.0
servlet: 3.0 6
validation-api: 1.1.0.GA 6
jcache: 1.0.0 6
javax.el: 3.0.1-b08 6
kryo: 4.0.1
kryo-serializers: 0.42
fst: 2.48-jdk-6
resteasy: 3.0.19.Final
tomcat-embed-core: 8.0.11
slf4j: 1.7.25
log4j: 1.2.16

Spring中Dubbo应用示例

  • 定义服务接口:由于服务的提供者和消费者都依赖于同一个接口,因此强烈建议将接口定义在一个单独的模块里面,从而方便服务提供者模块和消费者模块来依赖。
1
2
3
4
5
package com.alibaba.dubbo.demo;
public interface DemoService {
String sayHello(String name);
}
  • 实现服务的提供者
1
2
3
4
5
6
7
8
package com.alibaba.dubbo.demo.provider;
import com.alibaba.dubbo.demo.DemoService;
public class DemoServiceImpl implements DemoService {
public String sayHello(String name) {
return "Hello " + name;
}
}
  • 配置服务的提供者–在spring中如何配置Dubbo服务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 提供方应用信息,用于计算依赖关系 -->
<dubbo:application name="hello-world-app" />
<!-- 使用multicast广播注册中心暴露服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" />
<!-- 和本地bean一样实现服务 -->
<bean id="demoService" class="com.alibaba.dubbo.demo.provider.DemoServiceImpl" />
</beans>

从配置文件中可以看到我们只将demoService的接口暴露给一个URL,dubbo://127.0.0.1:20880,并将该服务注册给一个多媒体地址:multicast://224.5.6.7:1234

dubbo协议是Dubbo框架支持的众多协议之一,dubbo协议是在Java NIO特性的基础之上的,为Dubbo架构的默认协议。

  • 开启服务
1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Provider {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
new String[] {"META-INF/spring/dubbo-demo-provider.xml"});
context.start();
// press any key to exit
System.in.read();
}
}
  • 配置服务消费者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
<dubbo:application name="consumer-of-helloworld-app" />
<!-- 使用multicast广播注册中心暴露发现服务地址 -->
<dubbo:registry address="multicast://224.5.6.7:1234" />
<!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" />
</beans>

这个过程看起来和传统的web服务调用很想,但是dubbo协议使得其调用更加简单,轻量化,效率相对要高。

  • 运行服务消费者
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.alibaba.dubbo.demo.DemoService;
public class Consumer {
public static void main(String[] args) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/consumer.xml"});
context.start();
DemoService demoService = (DemoService)context.getBean("demoService"); // 获取远程服务代理
String hello = demoService.sayHello("world"); // 执行远程方法
System.out.println( hello ); // 显示调用结果
}
}
  • 配置简单注册中心

当我们使用多点传播注册中心时,注册服务不是单独的,因此适用于局域网场景。为了适应更加便于管理的注册中心,可以使用SimpleRegistryService。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dubbo:application name="simple-registry" />
<dubbo:protocol port="9090" />
<dubbo:service interface="com.alibaba.dubbo.registry.RegistryService"
ref="registryService" registry="N/A" ondisconnect="disconnect">
<dubbo:method name="subscribe">
<dubbo:argument index="1" callback="true" />
</dubbo:method>
<dubbo:method name="unsubscribe">
<dubbo:argument index="1" callback="true" />
</dubbo:method>
</dubbo:service>
<bean class="com.alibaba.dubbo.registry.simple.SimpleRegistryService"
id="registryService" />

可以用于测试,但是实际工程中不推荐使用。

Dubbo配置

XML配置

xml配置Dubbo可以参照上一节的实例,spring中可以直接通过dubbo标签库来对其进行配置设定等操作,使用广泛且条理清晰。所有的标签都支持自定义参数,用于不同扩展点实现的特殊配置:

1
2
3
<dubbo:protocol name="jms">
<dubbo:parameter key="queue" value="your_queue" />
</dubbo:protocol>

或者

1
2
//2.1之后开始支持这种自定义参数的方式
<dubbo:protocol name="jms" p:queue="your_queue" />
  • 常用标签
标签 用途 解释
<dubbo:service/> 服务配置 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心
<dubbo:reference/> 引用配置 用于创建一个远程服务代理,一个引用可以指向多个注册中心
<dubbo:protocol/> 协议配置 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受
<dubbo:application/> 应用配置 用于配置当前应用信息,不管该应用是提供者还是消费者
<dubbo:module/> 模块配置 用于配置当前模块信息,可选
<dubbo:registry/> 注册中心配置 用于配置连接注册中心相关信息
<dubbo:monitor/> 监控中心配置 用于配置连接监控中心相关信息,可选
<dubbo:provider/> 提供方配置 当ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选
<dubbo:consumer/> 消费方配置 当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选
<dubbo:method/> 方法配置 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息
<dubbo:argument/> 参数配置 用于指定方法参数配置
  • 标签之间的关系

Alt text

配置覆盖关系为方法级别的优先,接口几倍的次之,全局配置再次之,级别一样消费者优先,提供者次之

属性配置

公共配置简单,没有多注册中心,多协议等情况,或者多个Spring容器想要共享配置,可以使用dubbo.properties作为缺省配置。Dubbo会自动加载classpath路径下的dubbo.properties文件,可以通过JVM启动参数-Ddubbo.properties.file=xxx.properties改动缺省位置。

  • 映射规则:将XML配置的标签名,属性名,用点分隔,多个属性拆分成多行。
1
2
3
dubbo.application.name=foo
dubbo.application.owner=bar
dubbo.registry.address=10.20.153.10:9090
  • 覆盖策略:JVM启动,-D参数优先,XML次之,Properties文件最后。只有XML没有配置时,dubbo.properties才会生效。

Java API配置Dubbo

除了spring配置文件来配置Dubbo之外,通过Java API,property文件以及注解来配置Dubbo同样可以。配置文件和注解配置适用于结构不是很复杂的场景。下面介绍如何使用API来配置Dubbo。

  • 服务提供者
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
import com.alibaba.dubbo.rpc.config.ApplicationConfig;
import com.alibaba.dubbo.rpc.config.RegistryConfig;
import com.alibaba.dubbo.rpc.config.ProviderConfig;
import com.alibaba.dubbo.rpc.config.ServiceConfig;
import com.xxx.XxxService;
import com.xxx.XxxServiceImpl;
// 服务实现
XxxService xxxService = new XxxServiceImpl();
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("xxx");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("10.20.130.230:9090");
registry.setUsername("aaa");
registry.setPassword("bbb");
// 服务提供者协议配置
ProtocolConfig protocol = new ProtocolConfig();
protocol.setName("dubbo");
protocol.setPort(12345);
protocol.setThreads(200);
// 注意:ServiceConfig为重对象,内部封装了与注册中心的连接,以及开启服务端口
// 服务提供者暴露服务配置
ServiceConfig<XxxService> service = new ServiceConfig<XxxService>(); // 此实例很重,封装了与注册中心的连接,请自行缓存,否则可能造成内存和连接泄漏
service.setApplication(application);
service.setRegistry(registry); // 多个注册中心可以用setRegistries()
service.setProtocol(protocol); // 多个协议可以用setProtocols()
service.setInterface(XxxService.class);
service.setRef(xxxService);
service.setVersion("1.0.0");
// 暴露及注册服务
service.export();
  • 此时,服务已经暴露给多点传播的注册中心了,然后配置服务消费者:
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
import com.alibaba.dubbo.rpc.config.ApplicationConfig;
import com.alibaba.dubbo.rpc.config.RegistryConfig;
import com.alibaba.dubbo.rpc.config.ConsumerConfig;
import com.alibaba.dubbo.rpc.config.ReferenceConfig;
import com.xxx.XxxService;
// 当前应用配置
ApplicationConfig application = new ApplicationConfig();
application.setName("yyy");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("10.20.130.230:9090");
registry.setUsername("aaa");
registry.setPassword("bbb");
// 注意:ReferenceConfig为重对象,内部封装了与注册中心的连接,以及与服务提供方的连接
// 引用远程服务
ReferenceConfig<XxxService> reference = new ReferenceConfig<XxxService>(); // 此实例很重,封装了与注册中心的连接以及与提供者的连接,请自行缓存,否则可能造成内存和连接泄漏
reference.setApplication(application);
reference.setRegistry(registry); // 多个注册中心可以用setRegistries()
reference.setInterface(XxxService.class);
reference.setVersion("1.0.0");
// 和本地bean一样使用xxxService
XxxService xxxService = reference.get(); // 注意:此代理对象内部封装了所有通讯细节,对象较重,请缓存复用

注解配置

服务提供方

  • Service注解暴露服务
1
2
3
4
5
6
import com.alibaba.dubbo.config.annotation.Service;
@Service(timeout = 5000)
public class AnnotateServiceImpl implements AnnotateService {
// ...
}
  • javaconfig形式配置公共模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class DubboConfiguration {
@Bean
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("provider-test");
return applicationConfig;
}
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
registryConfig.setClient("curator");
return registryConfig;
}
}
  • 指定Java扫描路径
1
2
3
4
5
@SpringBootApplication
@DubboComponentScan(basePackages = "com.alibaba.dubbo.test.service.impl")
public class ProviderTestApp {
// ...
}

服务消费者

  • reference 注解引用服务
1
2
3
4
5
6
7
public class AnnotationConsumeService {
@com.alibaba.dubbo.config.annotation.Reference
public AnnotateService annotateService;
// ...
}
  • javaconfig形式配置公共模块
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
@Configuration
public class DubboConfiguration {
@Bean
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("consumer-test");
return applicationConfig;
}
@Bean
public ConsumerConfig consumerConfig() {
ConsumerConfig consumerConfig = new ConsumerConfig();
consumerConfig.setTimeout(3000);
return consumerConfig;
}
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("zookeeper://127.0.0.1:2181");
registryConfig.setClient("curator");
return registryConfig;
}
}
  • 指定dubbo扫描路径
1
2
3
4
5
@SpringBootApplication
@DubboComponentScan(basePackages = "com.alibaba.dubbo.test.service")
public class ConsumerTestApp {
// ...
}

可以看出,还是XML配置更加的简洁明了。

Dubbo支持的协议

Dubbo框架支持多种协议,包括dubbo、RMI、hessian、HTTP、web service、thrift、memcached、redis等。大多数协议之前都已经了解过,除了dubbo这个协议。

dubbo协议在服务提供者和消费者之间建立了长连接,这种长连接和非阻塞的网络传输在传输数据小于100k的时候效率非常高。dubbo协议中的参数包含端口,每个消费者的连接数,最大接受的连接数等。

<dubbo:protocol name="dubbo" port="20880" connections="2" accepts="1000" />

Dubbo还支持通过不同协议来同时暴露服务。

1
2
3
4
5
<dubbo:protocol name="dubbo" port="20880" />
<dubbo:protocol name="rmi" port="1099" />
<dubbo:service interface="com.baeldung.dubbo.remote.GreetingsService" version="1.0.0" ref="greetingsService" protocol="dubbo" />
<dubbo:service interface="com.bealdung.dubbo.remote.AnotherService" version="1.0.0" ref="anotherService" protocol="rmi" />

结果缓存

Dubbo对于远程调用的结果进行了本地的缓存来提高对于热点数据的访问速度。实现添加cache属性即可:

1
<dubbo:reference interface="com.baeldung.dubbo.remote.GreetingsService" id="greetingsService" cache="lru" />

缓存策略选择的是最少最近使用缓存,当我们队服务的提供者的实现类进行修改之后:

1
2
3
4
5
6
7
8
9
public class GreetingsServiceSpecialImpl implements GreetingsService {
@Override
public String sayHi(String name) {
try {
SECONDS.sleep(5);
} catch (Exception ignored) { }
return "hi, " + name;
}
}

对调用速度进行一下测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void givenProvider_whenConsumerSaysHi_thenGotResponse() {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("multicast/consumer-app.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
long before = System.currentTimeMillis();
String hiMessage = greetingsService.sayHi("baeldung");
long timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed > 5000);
assertNotNull(hiMessage);
assertEquals("hi, baeldung", hiMessage);
before = System.currentTimeMillis();
hiMessage = greetingsService.sayHi("baeldung");
timeElapsed = System.currentTimeMillis() - before;
assertTrue(timeElapsed < 1000);
assertNotNull(hiMessage);
assertEquals("hi, baeldung", hiMessage);
}

除了第一次调用,后面的调用几乎是立刻完成的,说明缓存生效了。

zookeeper

Dubbo通过负载均衡和集中容错策略可以支持我们来自由的扩展我们的服务。假定我们使用zookeeper来做我们管理服务的集群。服务提供者在zookeeper上注册服务:

1
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>

为了引入使用以上的注册方式,需要引入zookeeper的相关依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.11</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>

负载均衡

  • 随机负载均衡
    • 随机,按照权重设置随机概率
    • 在一个截面上碰撞的概率高,单调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
  • 轮询负载均衡
    • 按公约后的权重设置轮询比率。
    • 存在慢的提供者累积请求的问题,比如第二台机器很慢,当请求调到第二台就卡在那里,时间久了所有请求都卡在第二台机器上。
  • 最少活跃数负载均衡
    • 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
    • 使慢的提供者受到更少的请求,因为越慢的提供者的调用前后计数差会越大。
  • 一致性哈希负载均衡
    • 相同参数的请求总是发到同一个提供者
    • 当某一台提供者挂掉时,原本发往该提供者的请求,基于虚拟节点,平摊到其他提供者,不会引起剧烈变动。
    • 缺醒只对第一个参数哈希,也可以配置修改。
    • 缺醒使用160份虚拟节点,如果要修改,请配置

配置:

  • 服务端

    • 服务级别<dubbo:service interface="..." loadbalance="roundrobin" />
    • 方法级别
      1
      2
      3
      <dubbo:service interface="...">
      <dubbo:method name="..." loadbalance="roundrobin"/>
      </dubbo:service>
  • 客户端

    • 服务级别<dubbo:reference interface="..." loadbalance="roundrobin" />
    • 方法级别
      1
      2
      3
      <dubbo:reference interface="...">
      <dubbo:method name="..." loadbalance="roundrobin"/>
      </dubbo:reference>

轮询负载均衡举例

假定在一个集群里面存在两这个服务的实现作为服务提供者,请求时采用轮询方式做负载均衡。首先,创建服务提供者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Before
public void initRemote() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
ClassPathXmlApplicationContext remoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-default.xml");
remoteContext.start();
});
executorService.submit(() -> {
ClassPathXmlApplicationContext backupRemoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-special.xml");
backupRemoteContext.start();
});
}

然后我们有一个标准的高速服务提供者,可以立即给出相应结果,一个低速服务提供者,每个响应需要等候5s。执行六次请求,此时采用轮询的负载均衡侧率,则期待的是平均相应时间为2.5s。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void givenProviderCluster_whenConsumerSaysHi_thenResponseBalanced() {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("cluster/consumer-app-lb.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
List<Long> elapseList = new ArrayList<>(6);
for (int i = 0; i < 6; i++) {
long current = System.currentTimeMillis();
String hiMessage = greetingsService.sayHi("baeldung");
assertNotNull(hiMessage);
elapseList.add(System.currentTimeMillis() - current);
}
OptionalDouble avgElapse = elapseList
.stream()
.mapToLong(e -> e)
.average();
assertTrue(avgElapse.isPresent());
assertTrue(avgElapse.getAsDouble() > 2500.0);
}

同时,动态负载均衡是支持的,下面的例子说明这一点,采用的是轮询的负载均衡策略,当新的服务注册时候,消费者会选择新的服务。低俗的服务提供者在系统启动后2s注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Before
public void initRemote() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(() -> {
ClassPathXmlApplicationContext remoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-default.xml");
remoteContext.start();
});
executorService.submit(() -> {
SECONDS.sleep(2);
ClassPathXmlApplicationContext backupRemoteContext
= new ClassPathXmlApplicationContext("cluster/provider-app-special.xml");
backupRemoteContext.start();
return null;
});
}

服务提供者此时每秒调用一次服务,6次之后,我们期望平均响应时间大于1.6s。

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
@Test
public void givenProviderCluster_whenConsumerSaysHi_thenResponseBalanced()
throws InterruptedException {
ClassPathXmlApplicationContext localContext
= new ClassPathXmlApplicationContext("cluster/consumer-app-lb.xml");
localContext.start();
GreetingsService greetingsService
= (GreetingsService) localContext.getBean("greetingsService");
List<Long> elapseList = new ArrayList<>(6);
for (int i = 0; i < 6; i++) {
long current = System.currentTimeMillis();
String hiMessage = greetingsService.sayHi("baeldung");
assertNotNull(hiMessage);
elapseList.add(System.currentTimeMillis() - current);
SECONDS.sleep(1);
}
OptionalDouble avgElapse = elapseList
.stream()
.mapToLong(e -> e)
.average();
assertTrue(avgElapse.isPresent());
assertTrue(avgElapse.getAsDouble() > 1666.0);
}

负载均衡在服务提供者和服务消费者都可以使用。

集群容错

Dubbo支持多种容错机制,默认为failover重试。

Alt text

各节点的关系

  • Invoke是Provider的一个可调用Service的抽象,Invoker封装了Provider地址及Service接口信息。
  • Directory代表多个Invoker,List,值可以动态变化,比如注册中心推送变更
  • Cluster将Director中的多个Invoker封装成一个Invoker,对上层逃命,伪装过程包含了容错逻辑,如果失败后,重试另一个
  • Route负责从多个Invoker中按照路由规则选出子集没比如读写分离,应用隔离等
  • loadbalance负责从多个Invoker中选出一个具体用于本地调用,选的过程中包含了负载均衡该算法。

集群容错模式

  • fail-over
    • 当一个消费者获取一个服务失败的时候,会尝试访问集群中其他的服务提供者
    • 通常用于读操作,但是重试会带来更长的延迟
    • 可通过retries=“2”来设置重试次数,其中第一次不包含在内。
  • fail-safe
    • 失败安全,出现异常时,直接忽略
    • 通常用于写入审计日志等操作
  • fail-fast
    • 快速失败,只发起一次调用,失败立即报错
    • 通常用于非幂等性的写操作,比如新增记录
  • fail-back
    • 失败自动恢复,后台记录失败请求,定时重发
    • 通常用于消息通知操作
  • forking
    • 并行调度多个服务器,只要一个成功及返回。
    • 通常实时性要求比较高的读操作,但是需要浪费很多服务资源,可以通过forks=”2”来设置最大并行数
  • broadcast
    • 广播调用所有的提供者,逐个调用,任意一台报错则报错。
    • 通常用于通知所有提供者更新缓存或者日志等本地资源信息。

集群模式配置

1
2
3
4
//服务端
<dubbo:service cluster="failsafe" />
//客户端
<dubbo:reference cluster="failsafe" />

线性模型

Alt text

  • 如果事件的处理逻辑能够迅速完成,并且不会发起新的IO请求,比如只是在内存中标记个标识,则直接在IO线程上处理更快,因为这样减少了线程池的调度。
  • 如果事件处理逻辑较慢,或者需要发起新的IO请求,比如需要查询数据库,则必须派发到线程池,否则IO线程阻塞,将导致不能接受其他请求。
  • 如果用IO线程处理事件,又在事件处理过程中发起新的IO请求,比如在链接事件中发起登录请求,会报可能发生死锁异常。

针对不同的场景,需要设定相应的派发侧率和不同的线程池:

1
<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" />

Dispatcher

  • all,所有消息都派发到线程池,包括请求,相应,连接事件,断开事件,心跳等。
  • direct,所有消息都不派发到线程池,全部在IO线程上直接执行
  • message,只有请求相应消息派发到线程池,其他直接在IO线程执行
  • execution,只将请求消息派发到线程池,其他直接在IO线程执行
  • connection,在IO线程上,将连接断开事件放入队列,顺序执行,其他消息派发到线程池

threadPool

  • fixed,固定大小线程池,启动时创建线程,不关闭,一致持有,缺省
  • cached,缓存线程池,线程空闲一分钟自动删除,需要时创建
  • limited,可伸缩性线程池,单线程池中的线程数只会增长不会收缩,避免收缩带来的性能问题。
  • eager,优先创建worker线程池,当任务数量大于corePoolSize但小于maximumPoolSize时,优先创建Worker来处理任务。当任务数量大于maximumpoolSize时,将任务放入阻塞队列,阻塞队列充满时抛出RejectedExecutionException。

客户端和服务端直连

测试过程中,可以绕过注册中心来直接连接,此时会忽略注册中心的提供者列表,A接口配置点对点,不影响B接口从注册中心获取列表。

配置方法:

  • XML:<dubbo:reference id="xxxService" interface="com.alibaba.xxx.XxxService" url="dubbo://localhost:20890" />

  • JVM:java -Dcom.alibaba.xxx.XxxService=dubbo://localhost:20890

  • properties: com.alibaba.xxx.XxxService=dubbo://localhost:20890

ActiveMQ学习总结(一)

发表于 2019-04-21 | 分类于 JMS , ActiveMQ

自己写的网上商城项目中使用了ActiveMQ,虽然相比于RabbitMQ,kafka,RocketMQ等相比,ActiveMQ可能性能方面不是最好的选择,不过消息队列其实原理区别不大,这里对学过的关于消息队列的知识进行一下总结,并结合自己面试中关于这方面遇到的问题做一个整理,为后面秋招找工作做准备。这一篇主要介绍一下JMS,ActiveMQ安装及其常用接口,两种队列模式,如何集成到Spring项目,面试总结等。

JMS

  • Java Message Service,Java消息服务应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个程序之间,或分布式系统中发送消息,进行异步通信,JMS是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。这是比较详细的关于JMS的定义,而比较直观的说,JMS是一组消息服务的API,也就是说JMS只有接口,其具体实现类交给了各种MOM厂家来做。

  • JMS使用场景,应用程序A部署在北京,应用程序B部署在上海,每当A触发某个事件之后,B向获取A中的一些信息,也可能有很多个B都想获取A中的信息。这种情况下,Java提供了最佳的解决方案-JMS。JMS同样适用于基于事件的应用程序,如聊天服务,他需要一种发布事件机制向所有与服务器连接的客户端发送消息。JMS与RMI不同,不需要接受者在线。也就是服务器发送完消息,这个事件就与他无关了。

  • JMS的优势:

    • 异步,JMS天生就是异步的,客户端获取消息的时候,不需要主动发送请求,消息会自动发送给可用的客户端。
    • 可靠,JMS保证消息只会被递送一次。大家都遇到过重复创建消息的问题,JMS可以帮你避免这个问题,但是不能杜绝,需要MOM厂家来做更加完备的机制来改善。
  • JMS常用的一些概念:

    • Provider/MessageProvider:生产者
    • Consumer/MessageConsumer:消费者
    • PTP:Point To Point,点对点通信消息模型
    • Pub/Sub:Publish/Subscribe,发布订阅消息模型
    • Queue:队列,目标类型之一,和PTP结合
    • Topic:主题,目标类型之一,和Pub/Sub结合
    • ConnectionFactory:连接工厂,JMS用它创建连接
    • Connnection:JMS Client到JMS Provider的连接
    • Destination:消息目的地,由Session创建
    • Session:会话,由Connection创建,实质上就是发送、接受消息的一个线程,因此生产者、消费者都是Session创建的

ActiveMQ简介

ActiveMQ是Apache出品的,最流行的,能力强劲的开源消息总线。ActiveMQ是一个完全支持JMS1.1和J2EE1.4规范的JMS Provider实现,JMS上面已经有了简单的介绍。

ActiveMQ的特点:

  • 多种语言和协议编写客户端,语言包括Java、C、C++、C#、Ruby、Perl、Python、PHP,协议包括OpenWire、Stomp、REST、WS Notification、XMPP、AMQP
  • 完全支持JMS1.1和J2EE1.4规范
  • 对Spring的支持,使得ActiveMQ集成到Spring里面很方便
  • 支持多种传送协议:in-VM、TCP、SSL、NIO、UDP、JGroups、JXTA
  • 支持通过JDBC和journal提供高速的消息持久化
  • 从设计上保证了高性能的集群,客户端-服务器点对点
  • 支持Ajax
  • 支持与Axis的整合

ActiveMQ的消息形式

  • 点对点,也就是一个省车按着对一个消费者的一对一

  • 发布/订阅模式,一个生产者产生消息并进行发送后,可以由多个消费者进行接收

JMS中定义了五种不同的消息正文格式以及调用的消息信息,允许你发送并接收以一些不同形式的数据:

* StreamMessage --- 数据流
* MapMessage --- key-value
* TextMessage --- 字符串
* ObjectMessage --- 序列化的Java对象
* BytesMessage --- 一个字节的数据流

ActiveMQ的安装

  1. activemq.apache.org下载ActiveMQ
  2. 解压缩
  3. 启动, ./activemq start, 关闭 ./activemq stop, 查看状态 ./activemq status

后台管理页面:http://192.168.25.168:8161/admin, 用户名admin,密码admin

ActiveMQ的使用

添加jar包

1
2
3
4
5
<!--引入ActiveMQ的jar包-->
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-all</artifactId>
</dependency>

使用Queue形式的消息队列

Producer

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
/**
* ActiveMQ队列模式生产者
* @throws JMSException
*/
@Test
public void testQueueProducer() throws JMSException {
//1.创建一个连接工厂对象、需要指定IP和端口/消息服务端口为61616
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.128:61616");
//2.使用连接工厂来创建连接
Connection connection = connectionFactory.createConnection();
//3.开启连接
connection.start();
//4.创建一个会话,
//第一个参数为是否开启ActiveMQ的事务,一般不使用事务
//如果开启事务,第二个参数自动忽略,不开启事务,第二个参数表示消息的应答模式,自动应答、手动应答
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//5.使用Session对象来创建一个Destination对象,topic或者queue
Queue queue = session.createQueue("test-Queue");
//6.使用Session对象来创建一个生产者
MessageProducer producer = session.createProducer(queue);
//7.创建一个TextMessage对象
TextMessage textMessage = new ActiveMQTextMessage();
textMessage.setText("hello!");
//8.发送消息
producer.send(textMessage);
//9.关闭资源
producer.close();
session.close();
connection.close();
}

可以看出生产者和消费者之间传递的对象,由三个主要部分构成:消息头+消息属性+消息体。当然创建过程中也可以对消息进行持久化的选择等配置。

Consumer

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
/**
* ActiveMQ队列模式消费者
* @throws JMSException
*/
@Test
public void testQueueConsumer() throws JMSException, IOException {
//1.创建一个连接工厂对象、需要指定IP和端口/消息服务端口为61616
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.128:61616");
//2.使用连接工厂来创建连接
Connection connection = connectionFactory.createConnection();
//3.开启连接
connection.start();
//4.创建一个会话,
//第一个参数为是否开启ActiveMQ的事务,一般不使用事务
//如果开启事务,第二个参数自动忽略,不开启事务,第二个参数表示消息的应答模式,自动应答、手动应答
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
//5.使用Session对象来创建一个Destination对象,topic或者queue
Queue queue = session.createQueue("test-Queue");
//6.使用Session对象来创建一个消费者
MessageConsumer consumer = session.createConsumer(queue);
//7.接收消息
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
TextMessage textMessage = (TextMessage) message;
String text = null;
try {
text = textMessage.getText();
} catch (JMSException e) {
e.printStackTrace();
}
System.out.println(text);
}
});
//8.关闭资源
System.in.read();
consumer.close();
session.close();
connection.close();
}

注释基本解释清楚了基本的流程,唯一要注意的是接收消息的时候,我们先查看setMessageListener这个方法,其接口定义如下:

1
void setMessageListener(MessageListener var1) throws JMSException;

也就是说需要传入一个实现了MessageListener接口的对象,而MessageListener接口如下只有一个onMessage方法:

1
2
3
4
5
package javax.jms;
public interface MessageListener {
void onMessage(Message var1);
}

采用匿名类的方法来对test-Queue队列进行监控,只要有消息进来,就立即执行onMessage方法。

Topic模式

Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* ActiveMQ的订阅模式生产者
*/
@Test
public void testTopicProducer() throws Exception {
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.128:61616");
Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic("test-topic");
MessageProducer producer = session.createProducer(topic);
TextMessage activeMQ_topic = session.createTextMessage("activeMQ topic");
producer.send(activeMQ_topic);
producer.close();
session.close();
connection.close();
}

和队列模式相比,只是由session生成的消息队列模式编程了订阅发布模式,其他完全一样。

Consumer

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
/**
* ActiveMQ的订阅者模式消费者
*/
@Test
public void testTopicConsumer() throws Exception{
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://192.168.25.128:61616");
Connection connection = connectionFactory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Topic topic = session.createTopic("test-topic");
MessageConsumer consumer = session.createConsumer(topic);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
String text = textMessage.getText();
System.out.println(text);
} catch (JMSException e) {
e.printStackTrace();
}
}
}
});
System.in.read();
consumer.close();
session.close();
connection.close();
}

ActiveMQ在Spring中的使用

上一节介绍的在项目中直接使用消息队列的方式,可以看出存在很大的重复代码,而且步骤很多,将ActiveMQ整合到Spring中可以大大改善这两个问题。

  1. 引入JMS相关jar包
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
  1. 配置ActiveMQ整合Spring,配置ConnectionFactory
1
2
3
4
5
6
7
8
<!--ConnectionFactory,JMS服务厂商提供的ConnectionFactory-->
<bean id="targetConnecctionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<constructor-arg name="brokerURL" value="tcp://192.168.25.128:61616"/>
</bean>
<!--spring对ConnectionFactory的封装-->
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory" ref="targetConnecctionFactory"/>
</bean>
  1. 配置生产者,使用JMSTemplate对象,发送消息
1
2
3
4
<!--配置JMSTemplate-->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
  1. 配置Destination
1
2
3
4
5
6
7
<!--消息的目的地-->
<bean id="test-queue" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg name="name" value="test-queue"/>
</bean>
<bean id="item-add-topic" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg name="name" value="item-add-topic"/>
</bean>
  • 完整的配置文件:
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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd">
<!--ConnectionFactory,JMS服务厂商提供的ConnectionFactory-->
<bean id="targetConnecctionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<constructor-arg name="brokerURL" value="tcp://192.168.25.128:61616"/>
</bean>
<!--spring对ConnectionFactory的封装-->
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory" ref="targetConnecctionFactory"/>
</bean>
<!--配置JMSTemplate-->
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
<!--消息的目的地-->
<bean id="test-queue" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg name="name" value="test-queue"/>
</bean>
<bean id="item-add-topic" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg name="name" value="item-add-topic"/>
</bean>
</beans>

测试代码

  1. 发送消息,步骤注释中写的很清楚了,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 使用JMSTemplate来发送消息
* @throws Exception
*/
@Test
public void testJmsTemplate() throws Exception {
//初始化Spring容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext-activemq.xml");
//从容器中获得模板对象
JmsTemplate jmsTemplate = applicationContext.getBean(JmsTemplate.class);
//从容器中获得Destination对象
Destination destination = (Destination) applicationContext.getBean("test-queue");
//发送消息
jmsTemplate.send(destination, new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage("spring activemq send queue message");
}
});
}
  1. 接收消息

由于项目中使用ActiveMQ实现索引库和数据库的同步,为了接收ActiveMQ的消息,需要创建一个MessageListener实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 接收ActiveMQ发送的消息
* Created by cdx0312
* 2018/3/9
*/
public class MyMesseageListener implements MessageListener{
@Override
public void onMessage(Message message) {
//接收消息
try {
TextMessage textMessage = (TextMessage) message;
String text = textMessage.getText();
System.out.println(text);
} catch (JMSException e) {
e.printStackTrace();
}
}
}

然后在Spring中整合ActiveMQ的消息监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--ConnectionFactory,JMS服务厂商提供的ConnectionFactory-->
<bean id="targetConnecctionFactory" class="org.apache.activemq.ActiveMQConnectionFactory">
<constructor-arg name="brokerURL" value="tcp://192.168.25.128:61616"/>
</bean>
<!--spring对ConnectionFactory的封装-->
<bean id="connectionFactory" class="org.springframework.jms.connection.SingleConnectionFactory">
<property name="targetConnectionFactory" ref="targetConnecctionFactory"/>
</bean>
<!--消息的目的地-->
<bean id="test-queue" class="org.apache.activemq.command.ActiveMQQueue">
<constructor-arg name="name" value="test-queue"/>
</bean>
<bean id="item-add-topic" class="org.apache.activemq.command.ActiveMQTopic">
<constructor-arg name="name" value="item-add-topic"/>
</bean>
<!--配置消息的接受者-->
<!--配置监听器-->
<bean id="messeageListener" class="com.market.search.listen.MyMesseageListener"/>
<!--消息监听容器-->
<bean class="org.springframework.jms.listener.DefaultMessageListenerContainer">
<property name="connectionFactory" ref="connectionFactory"/>
<property name="destination" ref="test-queue"/>
<property name="messageListener" ref="messeageListener"/>
</bean>

测试代码:

1
2
3
4
5
6
@Test
public void testQueueConsumer() throws Exception {
//初始化Spring容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring/applicationContext-activemq.xml");
System.in.read();
}

ActiveMQ学习总结(三)

发表于 2019-04-21 | 分类于 JMS , ActiveMQ

总结一下消息队列面试过程中遇到的问题,各个互联网公司对于消息队列还是很感兴趣的,问的问题很多,深度也够,一遍面试一边学习的效率其实很高,希望可以通过整理更加了解ActiveMQ这个消息队列,后续计划对RocketMQ,RabbitMQ,kafka学习一下。

ActiveMQ的存储机制

  1. 通常情况下,非持久化消息时存储在内存中的,持久化消息时存储在日志文件中的,最大限制可以配置。
  2. 内存中的非持久化消息堆积到一定程度会影响内存的空间,此时ActiveMQ会将内存中的非持久化消息写入临时文件中,以释放内存。重启后非持久化的临时文件会直接删除。

ActiveMQ挂掉怎么办

  • ActiveMQ采用持久化方案的时候,当日志文件达到最大限制时会造成生产者阻塞,此时生产者不可以再生产新的服务,但是消费者可以正常连接并消费消息,当消费掉一部分消息之后,文件删除之后生产者可以继续生产消息,服务会自动回复正常运行。

  • ActiveMQ采用非持久化方案的时候,当临时文件大小达到上限时会产生生产者阻塞,消费者可以正常连接却不能正常消费消息,整个系统可连接,但是无法提供服务,ActiveMQ挂掉。

  • 解决方案:尽量不要用非持久化消息,如果要用的话,需要将临时文件限制尽可能调大。

为什么选择ActiveMQ?

  • 降低分布式系统之间的耦合度
  • 产品很成熟,在很多公司得到了应用
  • 文档众多,协议支持的很好,有封装好的Java客户端
  • ActiveMQ性能不如RabbitMQ,kafka等,后期计划更新消息队列。

ActiveMQ丢失消息怎么处理?

  • ActiveMQ每隔10s会发送一个心跳包,用来检测客户端是否存活。而如果采用非持久化方案的消息队列,消息堆积之后会触发一次写过程,这个过程会阻塞所有的活动,持续20-30s,并且会随着内存增大而增加。
  • 此时客户端发完消息会关闭连接,发送的消息存在服务端的缓存里面,服务器直接读取缓存时不会造成消息丢失的,但是由于心跳包的存在,发生了SocketException异常,缓存区数据失效,从而造成消息丢失。
  • 解决方案:使用持久化消息,或者非持久化消息及时处理不要堆积,或者启动事务,启动事务之后,commit方法会负责等待服务器的返回,不会关闭连接导致消息丢失。

消息的不均匀消费问题?

  • ActiveMQ的prefetch机制,消费者去获取消息时,会一次获取一批,默认1000。这些消息在没被消费之前,管理控制台可以看见这些消息,但是无法将其分配给其他消费者。消费成功,在服务器端删除对应消息,消费失败,退回服务端重新分配。
  • 解决方案,将prefetch设置为1

死信队列

  • 由于AUTO_ACKNOWLWDGE只是确保消费者收到消息,不保证消息能够正确执行。因此实际中一般采用CLIEND_ACKNOWLEDGE,自己去处理什么时候返回确认信息。

  • 使用了AUTO_ACKNOWLWDGE时,如果消费消息采用的是consumer.receive()方法,则直接确认。但是如果采用Listener回调函数,则消息到达会执行Listener接口的onMessage方法。这种情况下,只有执行完onMessage方法才会确认消息。此时如果报错,消息不会被删除,而是退回服务器。重试6次之后,会进入死信队列。

ActiveMQ中的消息重发时间间隔和重发次数

  • 消息接受者在处理完一条消息的处理过程后没有对MOM进行应答,则该消息由MOM重发。
  • 如果某个队列设置了预读参数,如果消息接受者在处理第一条消息时就宕机了,则预读数量的所有消息都会被重发。
  • 如果session是事务的,只要消息接受者有一条消息没有确认,或者发送期间MOM或者客户端宕机,则该事务范围中的所有消息都将重发。
  • 重发侧率可以自定义配置,比如配置最大重传数量,默认为6,最大传送延迟等。。。

9. 进程和线程

发表于 2019-04-21 | 分类于 Python

进程和线程

多任务:操作系统可以同时运行多个任务。比如一遍浏览器上网一遍听音乐,Word写文档等,这就是多任务。

单核CPU怎么进行多任务处理?操作系统轮流让各个任务交替进行,任务1执行0.01s,任务2执行0.01s,任务3执行0.01s,反复执行下去。表面上看每个任务都是交替执行的,但是由于CPU的执行速度很快,感觉所有任务是在同时执行一样。

真正的并行执行多任务只能在多核cpu上实现,但是由于任务数远远多于CPU的核心数,所以操作系统也会自动把很多任务轮流调度到每个核心上执行。

对于操作系统来说,一个任务是一个进程(process),比如打开一个浏览器就是启动了一个浏览器进程,打开一个记事本就是启动了一个记事本进程。

有些进程不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内这些子任务称为线程(thread)。

由于每个进程至少要做一件事,所以一个进程最少有一个线程。像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来像同时执行一样。

前面写的Python程序都是执行单任务的进程,也就是只有一个线程,如果要同时执行多个任务怎么办?有两种解决方案:

一种是启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。

还有一种方法是启动一个进程,在一个进程内启动多个线程,这样多个线程也可以一块执行多个任务。

还可以启动多个进程,每个进程启动多个线程。

同时执行的多个任务通常各个任务之间并不是没有关联的,而是需要相互通信和协调,有时,任务1必须暂停等待任务2完成后才能继续执行,任务3和任务4不能共同执行 ,所以多进程和多线程的程序的复杂度要远远高于我们前面写的单进程单线程的程序。

Python既支持多进程又支持多进程。

多进程

要让Python实现多进程(multiprocessing),我们先了解操作系统的相关知识。

Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次返回一次。但是fork()调用一次返回两次,因为操作系统自动把当前进程复制一份,然后分别在父进程和子进程内返回。

子进程永远返回0,而父进程返回子进程的ID。这样做的理由是一个父进程可以fork出很多子进程,多以父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。

Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:

1
2
3
4
5
6
7
8
9
10
import os
print('Process (%s)' %os.getpid())
pid=os.fork()
if pid==0:
print('I am child prcess (%s) and my parent is %s' %(os.getpid(),os.getppid()))
else:
print ('I (%s) just created a child process(%s)' %(os.getpid(),pid))

由于Windows系统没有fork调用,上面的代码无法再windows运行。

###multiprocessing
Linux、Unix编写多进程的服务程序是正确的选择。由于windows没有fork调用,可以用multiprocessing模块进行多进程编程。

multiprocessing模块提供了一个Process类来代表一个进程对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
import multiprocessing
import os
def run_proc(name):
print('Run child process %s(%s)...' %(name,os.getpid()))
if __name__=='__main__':
print('Parent process %s' %os.getpid())
p=multiprocessing.Process(target=run_proc,args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')

执行结果如下:

1
2
3
4
Parent process 4396
Child process will start.
Run child process test(10168)...
Child process end.

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start方法启动,这样创建的进程比fork要简单。

join()方法可以等待子进程结束再继续往下运行,通常用于进程间的同步。

Pool

如果要启动大量的子进程,可以用进程池的方式批量创建子进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from multiprocessing import Pool
import os,time,random
def long_time_task(name):
print('Run task %s (%s)...' %(name,os.getpid()))
start=time.time()
time.sleep(random.random()*3)
end=time.time()
print('Task %s runs %0.2f seconds' %(name, (end-start)))
if __name__=='__main__':
print('Parent process %s' %os.getpid())
p=Pool(4)
for i in range(5):
p.apply_async(long_time_task,args=(i,))
print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
D:\笔记\Python\Notepad++>python jincheng_pool.py
Parent process 11184
Waiting for all subprocesses done...
Run task 0 (1604)...
Run task 1 (7696)...
Run task 2 (17592)...
Run task 3 (10544)...
Task 1 runs 1.20 seconds
Run task 4 (7696)...
Task 2 runs 1.73 seconds
Task 0 runs 2.17 seconds
Task 3 runs 2.98 seconds
Task 4 runs 2.09 seconds
All subprocesses done.

代码解读:

对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。

注意输出结果,task0,1,2,3是立刻执行的,而4要等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,因此最多同时执行四个进程。这是Pool有意设计的限制,并不是操作系统的限制,改为p=Pool(5)就可以同时跑五个进程。Pool的默认大小是CPU的核心数。

子进程

很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。

subprocess模块可以让我们非常方便的启动一个子进程,然后控制其输入和输出。下面例子演示了如何在python代码中运行命令nslookup www.python.org,这和行命令直接运行的效果是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> import subprocess
>>>
>>> print('$ nslookup www.python.org')
$ nslookup www.python.org
>>> r=subprocess.call(['nslookup','www.python.org'])
服务器: UnKnown
Address: 10.3.9.6
非权威应答:
名称: prod.python.map.fastlylb.net
Addresses: 2a04:4e42:4::223
151.101.16.223
Aliases: www.python.org
python.map.fastly.net
>>> print('Exit code:',r)
Exit code: 0

如果子进程还需要输入,则可以通过communicate()方法输入:

1
2
3
4
5
6
7
import subprocess
print('$ nslookup')
p = subprocess.Popen(['nslookup'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, err = p.communicate(b'set q=mx\npython.org\nexit\n')
print(output.decode('gbk'))
print('Exit code:', p.returncode)

运行结果:

1
2
3
4
5
6
7
8
9
10
$ nslookup
默认服务器: UnKnown
Address: 10.3.9.5
> > 服务器: UnKnown
Address: 10.3.9.5
python.org MX preference = 50, mail exchanger = mail.python.org
>
Exit code: 0

进程间通信

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queeu、Pipes等方式来交换数据。

以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读取数据:

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
#!/usr/bin/env python3
#-*- coding: utf-8 -*-
from multiprocessing import Process, Queue
import os, time, random
#写数据
def write(q):
print('Process to write %s' %os.getpid())
for value in ['A','B','C']:
print('Put %s to queue...' % value)
q.put(value)
time.sleep(random.random())
#读数据
def read(q):
print('Process to read %s' %os.getpid())
while True:
value=q.get(True)
print('Get %s from queue...' % value)
if __name__=='__main__':
#父进程创建Queue,并传给各个子进程
q=Queue()
pw=Process(target=write, args=(q,))
pr=Process(target=read, args=(q,))
#启动子程序pw,写入
pw.start()
pr.start()
pw.join()
pr.terminate()

运行结果:

1
2
3
4
5
6
7
8
9
D:\笔记\Python\Notepad++>python 1.py
Process to write 4288
Put A to queue...
Process to read 16844
Get A from queue...
Put B to queue...
Get B from queue...
Put C to queue...
Get C from queue...

多线程

多任务可以由多进程完成,也可以有一个进程池内的多线程完成。一个进程最少有一个线程。Python内置了多线程的支持,Python的线程是真正的Posix Thread,不是模拟出来的线程。

Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time, threading
def loop():
print('thread %s is running...' %threading.current_thread().name)
n=0
while n<5:
n=n+1
print('thread %s >>> %s' %(threading.current_thread().name,n))
time.sleep(1)
print('thread %s ended.' %threading.current_thread().name)
print('thread %s is running....'%threading.current_thread().name)
t=threading.Thread(target=loop,name='LoopThread')
t.start()
t.join()
print('thread %s ended' %threading.current_thread().name)

运行结果如下:

1
2
3
4
5
6
7
8
9
10
D:\笔记\Python\Notepad++>python 2.py
thread MainThread is running....
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended

由于任何进程默认就会启动一个线程,我们把这个线程称为主线程,主线程又可以启动信的线程,Python的threading模块有个current_thresd()函数,永远返回当前进程的实例。主线程实例的名字叫做MainThread,子线程的名字在创建时指定。名字仅仅在打印时用来显示,没有任何其他意义。

Lcok

多线程和多进程最大的不同在于,在多进程中,同一个变量,各自有一份拷贝存于每个进程当中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大 的危险在于多个线程同时修改一个变量,内容会变混乱。参看下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time, threading
balance=0
def change_it(n):
#先存后取,结果应为0
global balance
balance=balance+n
balance=balance-n
def run_thread(n):
for i in range(1000000):
change_it(n)
t1=threading.Thread(target=run_thread,args=(5,))
t2=threading.Thread(target=run_thread,args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

我们定义了一个共享变量balance,初始值为0,并且启动两个线程,先存后取,理论上结果应该为0,但是由于线程的调度是由操作系统决定的,当t1,t1交替执行时,只要循环次数够多,balance的结果就不一定是0了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
D:\笔记\Python\Notepad++>python 3.py
18
D:\笔记\Python\Notepad++>python 3.py
21
D:\笔记\Python\Notepad++>python 3.py
21
D:\笔记\Python\Notepad++>python 3.py
10
D:\笔记\Python\Notepad++>python 3.py
15

原因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算balance=balance+n也要分成两步计算:

1.计算balance+n,存入临时变量中
2.将临时变量的值赋给balance

由于x是局部变量,两个线程各自都有自己的x,当代码正常执行时:

1
2
3
4
5
6
7
8
9
10
11
12
13
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2 # balance = 0
结果 balance = 0

但是t1和t2是交替运行的,如果操作系统以下面的顺序执行t1、t2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
初始值 balance = 0
t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2 # balance = 8
t1: balance = x1 # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1 # balance = 0
t2: x2 = balance - 8 # x2 = 0 - 8 = -8
t2: balance = x2 # balance = -8
结果 balance = -8

究其原因,是因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。

两个线程同时一存一取,就可能导致余额不足,我们必须确保有一个线程在修改balance的时候,别的线程一定不能改。如果我们要确保balance计算正确,就要给change_it()上一把锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少进程,同一时刻最多只有一个线程持有该锁,所以不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:

1
2
3
4
5
6
7
8
9
10
balance=0
lock=threading.Lock()
def run_thread(n):
for i in range(1000000):
lock.acquire()
try:
change_it(n)
finally:
lock.release

当多个线程同时执行lock.acquire()时,只有一个线程能成功地获得锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

获得锁的线程用完后一定要释放锁,否则那些裤裤等待锁的线程将永远等待下去,称为死进程。所以我们用try..finally来确保锁一定会释放。

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整的执行,坏处是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大降低了。其次由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,即不能执行,也无法结束,只能靠操作系统强制终止。

多核CPU

多核可以同时执行多个线程,如果写一个死循环,会100%占用一个CPU。要把N核CPU全部跑满,就必须启动N个死循环线程。写一个python死循环:

1
2
3
4
5
6
7
8
9
10
import threading,multiprocessing
def loop():
x=0
while True:
x=x^1
for i in range(multiprocessing.cpu_count()):
t=threading.Thread(target=loop)
t.start()i

启动和CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占有率仅为102%,也就是只使用了一个核。用C、C++、Java来改写相同的死循环,直接可以把全部核新跑满,Python就不行。因为python的现场虽然是真的线程,但是解释器执行代码时,有一个GIL锁:Global Interpr Lock,任何Python线程在执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁。所以在多线程python中只能交替执行。

所以在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过线程利用多核,只能通过C扩展实现,不过这样就失去了Python简单易用的特性。

不过可以通过多进程实现多核任务,多个Python进程有各自独立的GIL锁,互不影响。


ThreadLocal

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁。

但是局部变量的问题在于函数调用的时候,传递起来很麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
def process_student(name):
std=Student(name)
#std是局部变量,但是每个函数都要用它,因此必须传进去
do_task_1(std)
do_task_2(std)
def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)
def do_task_2(std)
do_subtask_1
do_subtask_2

每个函数一层一层的调用都传参数会很麻烦。也不能用全局变量,因为每个线程处理不同的Student对象,不能共享。如果用一个全局dict存放所有的Student对象,然后以thread自身作为key获取线程对应的Student对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
global_dict={}
def std_thread(name):
std=Student(name)
#把std放到全局变量global_dict中
global_dict[threading.current_thread()]=std
do_task_1()
do_task_2()
def do_task_1():
#不传入std,根据当前线程查找
std=global_dict[threading.current_thread()]
...
def do_task_2():
std=global_dict[threading.current_thread()]
...

这种方法理论上是可行的,最大的优点是消除了std对象在每层函数中的传递问题。Python还提供了更简单的方式,ThreadLocal:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import threading
#创建全局ThreadLocal对象:
local_school=threading.local()
def process_student():
#获取当前线程关联的student:
std=local_school.student
print('Hello, %s (in %s)' %(std,threading.current_thread().name))
def process_thread(name):
#绑定ThreadLocal的student:
local_school.student=name
process_student()
t1=threading.Thread(target=process_thread,args=('Alice',),name='Thread-A')
t2=threading.Thread(target=process_thread,args=('Bob',),name='Thread-B')
t1.start()
t2.start()
t1.join()
t2.join()

执行结果:

1
2
3
D:\笔记\Python\Notepad++>python 6.py
Hello, Alice (in Thread-A)
Hello, Bob (in Thread-B)

全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。

可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量。

ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都非常方便的访问这些资源。


进程vs线程

讨论进程和线程的优缺点:

首先,要实现多任务,通常我们会设计Master-Worker模式,master负责分配任务,worker负责执行任务,因此多任务环境下,通常是一个Master,多个worker。

如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多进程实现Master-Worker,主线程就是Master,其他线程就是Worker。

多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还不算麻烦。在Windows下创建进程开销巨大。另外操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统调度都很难。

多线程模式通常比多进程快一点,但是有限。而且多线程模式致命的缺点是任何一个线程挂掉都可能造成整个进程的崩 溃,以内所有的线程共享进程的内存。在Windows上,如果一个线程的代码处理问题,操作系统会强制结束整个进程。

在WIndows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有了多进程+多进程的混合模式。

线程切换

无论是多进程还是多线程,只要数量多了,效率就低了。

操作系统在切换进程或者线程时,首先要保存当前执行的现场环境(CPU寄存器状态、内存页等),然后把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,也需要耗费时间,如果有几千个任务同时进行,操作系统可能会主要忙着切换任务,执行任务的时间会减少,造成系统处于假死状态。多任务一旦到达一个限度,就会消耗掉系统所有的资源,效率急剧下降。

计算密集型vs.IO密集型

是否采用多任务的第二个考虑是任务的类型。

计算密集型任务的特点是要进行大量的计算,消耗CPU的资源。计算密集型任务虽然也可以用多任务完成,但是任务越多,任务切换的时间就越多,CPU执行任务的效率就越低。要高效的利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适应计算密集型任务。对于计算密集型任务,最好用C语言编写。

IO密集型任务涉及到网络、磁盘IO的任务,这类任务的特点是CPU消耗很少,任务的大部分时间在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。

IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高的语言。

异步IO

考虑到CPU和IO之间巨大的速度差异,一个任务在执行过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此我们才需要多进程模型或者多线程模型来支持多任务并发执行。

现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程的模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效的支持多任务。在多核CPU上可以运行多个进程,充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。


分布式进程

在Thread和Process中,应当优选Process,因为Process更稳定,而且Process可以分布到多台机器上,而Thread最多只能分布到一台机器的多个CPU上。

Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多个进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块的封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。

例如:如果我们已经有一个通过Queue通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现呢?

原有的Queue可以继续使用,但是,通过managers模块把Queue通过网络暴露出去,就可以让其他机器的进程访问Queue了。

我们先看服务进程,服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务:

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import random, time, queue
from multiprocessing.managers import BaseManager
from multiprocessing import freeze_support
# 发送任务的队列:
task_queue = queue.Queue()
# 接收结果的队列:
result_queue = queue.Queue()
# 从BaseManager继承的QueueManager:
class QueueManager(BaseManager):
pass
def return_task_queue():
global task_queue
return task_queue
def return_result_queue():
global result_queue
return result_queue
def test():
# 把两个Queue都注册到网络上, callable参数关联了Queue对象:
QueueManager.register('get_task_queue', callable=return_task_queue)
QueueManager.register('get_result_queue', callable=return_result_queue)
# 绑定端口5000, 设置验证码'abc':
manager = QueueManager(address=('127.0.0.1', 5000), authkey=b'abc')
# 启动Queue:
manager.start()
# 获得通过网络访问的Queue对象:
task = manager.get_task_queue()
result = manager.get_result_queue()
# 放几个任务进去:
for i in range(10):
n = random.randint(0, 10000)
print('Put task %d...' % n)
task.put(n)
# 从result队列读取结果:
print('Try get results...')
for i in range(10):
r = result.get(timeout=10)
print('Result: %s' % r)
# 关闭:
manager.shutdown()
print('master exit.')
if __name__=='__main__':
freeze_support()
test()

当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但是再分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕开了QueueManager的封装,必须通过manager.get_task_queue()获得Queue接口添加。

然后在另外一台机器上启动任务程序:

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
#!/usr/bin/env python3
#-*- coding: utf-8 -*-
import sys, time, queue
from multiprocessing.manager import BaseManager
#创建类似的QueueManager:
class QueueManager(BaseManager):
pass
#由于这个QueueManager只能从网络上获取QUeue,所以注册时只提供名字:
QueueManager.register('get_task_queue')
QueueManager.register('get_result_queue')
#连接到服务器,也就是task_master.py运行的机器:
server_addr='127.0.0.1'
print('Connect to server %s...' %server_addr)
#端口和验证码注意保持与task_master.py设置的完全一致:
m=QueueManager(address=(server_addr,5000),authkey=b'abc')
#从网络连接:
m.connect()
#获取Queue的对象:
task=m.get_task_queue()
result=m.get_result_queue()
#从task队列获取任务,并把结果写入result队列:
for i in range(10):
try:
n=task.get(timeout=1)
print('run task %d*%d...'%(n,n))
r='%d*%d=%d' %(n,n,n*n)
time.sleep(1)
result.put(r)
except Queue.Empty:
print('task queue is empty.')
#处理结束:
print('worker exit')

任务进程要通过网络连接到服务进程,所以要指定任务进程的IP。

首先启动task_master.py服务进程:

1
2
3
4
5
6
7
8
9
10
11
12
D:\笔记\Python\Notepad++>python task_master1.py
Put task 9516...
Put task 24...
Put task 7549...
Put task 6291...
Put task 9628...
Put task 9821...
Put task 8142...
Put task 3530...
Put task 5915...
Put task 9900...
Try get results...

task_master.py进程发送完任务后,开始等待result队列的结果,现在启动task_worker.py进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
D:\笔记\Python\Notepad++>python task_worker.py
Connect to server 127.0.0.1...
run task 6999*6999...
run task 204*204...
run task 7592*7592...
run task 2210*2210...
run task 3849*3849...
run task 5265*5265...
run task 4221*4221...
run task 7808*7808...
run task 3512*3512...
run task 7448*7448...
worker exit

task_worker.py进程结束,在task_master.py进程中会继续打印出结果:

1
2
3
4
5
6
7
8
9
10
11
Result: 6999*6999=48986001
Result: 204*204=41616
Result: 7592*7592=57638464
Result: 2210*2210=4884100
Result: 3849*3849=14814801
Result: 5265*5265=27720225
Result: 4221*4221=17816841
Result: 7808*7808=60964864
Result: 3512*3512=12334144
Result: 7448*7448=55472704
master exit.

这个简单的Master/Worker模型就是一个简单但真正的分布式计算,把代码稍加改变,启动多个Worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n的代码换为发送邮件,就实现了邮件队列的异步发送。

Queue对象存储在task_master.py中:
Alt text

而Queue可以通过QueueManager实现通过网络访问。由于QueueManager管理的不止一个Queue,所以要给每个Queue的网络调用接口起个名字,比如get_task_queue。

authkey是为了保证两台计算机正常通信,不被其他机器恶意干扰。

Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。注意Queue的作用是用来传递任务和接收任务,每个任务的描述数据量要尽量小。

8. IO编程

发表于 2019-04-21 | 分类于 Python

IO编程

IO在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算机核心来执行,设计到数据交换的地方,通常是磁盘、网络等,就需要IO接口。

比如你打开浏览器,访问新浪首页,浏览器这个程序就需要通过网络IO获取新浪的网页。浏览器首先会发送数据给新浪服务器,告诉它我想要首页的HTML,这个动作是往外发数据,叫Output,随后新浪服务器把网页发过来,这个动作是从外面接收数据,叫Input。所以,通常,程序完成IO操作会有Input和Output两个数据流。当然也有只用一个的情况,比如,从磁盘读取文件到内存,就只有Input操作,反过来,把数据写到磁盘文件里,就只是一个Output操作。

IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。对于浏览网页来说,浏览器和新浪服务器之间至少需要建立两根水管,才可以既能发数据,又能收数据。

由于CPU和内存的速度远远高于外设的速度,所以,在IO编程中,就存在速度严重不匹配的问题。举个例子来说,比如要把100M的数据写入磁盘,CPU输出100M的数据只需要0.01秒,可是磁盘要接收这100M数据可能需要10秒,怎么办呢?有两种办法:

第一种是CPU等着,也就是程序暂停执行后续代码,等100M的数据在10秒后写入磁盘,再接着往下执行,这种模式称为同步IO;

另一种方法是CPU不等待,只是告诉磁盘,“您老慢慢写,不着急,我接着干别的事去了”,于是,后续代码可以立刻接着执行,这种模式称为异步IO。

同步和异步的区别就在于是否等待IO执行的结果。好比你去麦当劳点餐,你说“来个汉堡”,服务员告诉你,对不起,汉堡要现做,需要等5分钟,于是你站在收银台前面等了5分钟,拿到汉堡再去逛商场,这是同步IO。

你说“来个汉堡”,服务员告诉你,汉堡需要等5分钟,你可以先去逛商场,等做好了,我们再通知你,这样你可以立刻去干别的事情(逛商场),这是异步IO。

很明显,使用异步IO来编写程序性能会远远高于同步IO,但是异步IO的缺点是编程模型复杂。想想看,你得知道什么时候通知你“汉堡做好了”,而通知你的方法也各不相同。如果是服务员跑过来找到你,这是回调模式,如果服务员发短信通知你,你就得不停地检查手机,这是轮询模式。总之,异步IO的复杂度远远高于同步IO。

操作IO的能力都是由操作系统提供的,每一种编程语言都会把操作系统提供的低级C接口封装起来方便使用,Python也不例外。我们后面会详细讨论Python的IO编程接口。

注意,本章的IO编程都是同步模式,异步IO由于复杂度太高,后续涉及到服务器端程序开发时我们再讨论。

文件读写

读写文件是最常见的IO操作。python内置了读写文件的函数,用法和C是兼容的。

读写文件前,我们必须了解在磁盘读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象,然后,通过操作系统提供的接口从这个文件对象中读取数据,或者把数据写入这个文件对象。

读文件

要以读文件的模式打开一个文件对象,使用Python内置的open()函数 ,传入文件名和标示符:

1
2
3
4
5
6
>>> f=open('C:/Users/cdxu0/Desktop/open.txt','r')
>>> f.read()
'python open read '
>>> f.close
<built-in method close of _io.TextIOWrapper object at 0x000001A97E9DEB40>
>>>

标示符r标示读,这样,我们成功打开了一个文件。如果文件不存在,open()函数就会抛出一个IOError的错误,并且给出错误码和详细的信息高速你文件不存在。

1
2
3
4
>>> f=open('C:/Users/cdxu0/Desktop/open.py','r')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'C:/Users/cdxu0/Desktop/open.py'

由于文件读写时都有可能产生IOError,一旦出错,后面的f.close()就不会调用。所以为了保证无论是否出错都能正确的关闭文件,我们可以使用try...finally来实现:

1
2
3
4
5
6
7
8
>>> try:
... f=open('C:/Users/cdxu0/Desktop/test.txt','r')
... print(f.read())
... finally:
... if f:
... f.close()
...
python open read

但是每次都这么写比较繁琐,Python引入了with语句来自动帮助我们调用close()方法。

1
2
3
4
>>> with open('C:/Users/cdxu0/Desktop/test.txt','r') as f:
... print(f.read())
...
python open read

这和前面的try...finally是一样的,但是代码更为简洁,并且不用调用f.close()方法。

调用read()会一次性读取文件的全部内容,如果文件有10G,内存就爆了,保险起见,要反复调用read(size)方法,每次最多读取size个字节的内容。另外,调用readline()可以每次读取一行的内容,调用readlines()一次读取所有内容并按行返回list。因此,要根据需要决定怎么调用。

如果文件很小,read()一次性读取最方便;如果不能确定文件的大小,反复调用read(size)比较保险;如果是配置文件,调用readlines()最方便。

1
2
3
4
>>> for line in f.readlines():
... print(line.strip())
...
python open read

file-like Object

像open()函数返回的这种有个read()方法的对象,在Python中统称为file-like Object。除了file外,还可以是内存的字节流,网络流,自定义流等等。file-like Object不要求从特定类继承,只要写个read()方法就行。

StringIO就是在内存中创建的file-like Object,常用作临时缓冲。

二进制文件

前面讲的默认都是读取文本文件的,并且是UTF-8编码的文本文件。尧都区二进制文件,比如图片、时频等,用rb模式打开文件即可:

1
2
3
>>> f=open('C:/Users/cdxu0/Desktop/临时文件/1.png','rb')
>>> f.read()
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDAT\x08\x99c```\xf8\x0f\x00\x01\x04\x01\x00}\xb2\xc8\xdf\x00\x00\x00\x00I

字符编码

要读取非UTF-8编码的文本文件,需要给open()函数传入encoding参数,例如,读取GBK编码的文件:

1
2
3
>>> f=open('C:/Users/cdxu0/Desktop/临时文件/test.txt','r',encoding='gbk')
>>> f.read()
'python open read '

遇到有些编码不规范的文件,你可能会遇到UnicodeDecodeError,因为在文本文件中可能夹杂了一些非法编码的字符。遇到这种情况,open()函数还接收一个errors参数,便是如果遇到编码错误后如何处理。最简单的方式是直接忽略。

1
>>> f=open('C:/Users/cdxu0/Desktop/临时文件/test.txt','r',encoding='gbk',errors='ignore')

写文件

写文件和读文件一样 的,唯一的区别在于在调用open()函数时,传入标示符w或wb表示写文本文件或写二进制文件:

1
2
3
4
5
>>> f=open('C:/Users/cdxu0/Desktop/临时文件/open.txt','w')
>>> f.write('hehheheh')
8
>>> f.close
<built-in method close of _io.TextIOWrapper object at 0x000002846BDBEB40>

你可以反复调用write()来写入文件,但是务必要调用f.close()来关闭文件。当我们写文件时,操作系统往往不会立刻把文件写入磁盘,而是放到缓存里面,空闲时候再慢慢写入。只有调用close()方法时,操作系统才保证把没有写入的数据全部写入磁盘。忘记调close()的后果是可能导致数据只写了一部分。还是用with比较保险:

1
2
3
4
>>> with open('C:/Users/cdxu0/Desktop/临时文件/test.txt','w') as f:
... f.write('hello,python!')
...
13

要写入特定编码的文本文件,请给open()函数传入encoding参数,将字符串自动转换成指定编码。


StringIO和BytesIO

StringIO

很多时候,数据读写不一定是文件,也可以在内存中读写。StringIO顾名思义就是在内存中读写str。要把str写入StringIO,我们需要先创建一个StringIO,然后像文件一样写入就可以了:

1
2
3
4
5
6
7
8
9
10
>>> from io import StringIO
>>> f=StringIO()
>>> f.write('hello')
5
>>> f.write(' ')
1
>>> f.write('worls! ')
7
>>> print(f.getvalue())
hello worls!

getvalue()方法用于获得写入后的str。

要读取StringIO,可以用一个str初始化StringIO,然后像问价一样读取:

1
2
3
4
5
6
7
8
9
10
11
>>> from io import StringIO
>>> f=StringIO('hello\nhi\nbye!')
>>> while True:
... s=f.readline()
... if s=='':
... break
... print(s.strip())
...
hello
hi
bye!

BytesIO

StringIO操作的只能是str,如果要操作二进制数据,就需要使用BytesIO。BytesIO实现了在内存中读写bytes,我们创建一个BytesIO,然后写入一些bytes:

1
2
3
4
5
6
>>> from io import BytesIO
>>> f=BytesIO()
>>> f.write('中文'.encode('utf-8'))
6
>>> print(f.getvalue())
b'\xe4\xb8\xad\xe6\x96\x87'

请注意,写入的不是str,而是经过UTF-8编码的bytes。和StringIO类似,可以用一个bytes 初始化BytesIO,然后像读文件一样读取:

1
2
3
4
>>> from io import BytesIO
>>> f=BytesIO(b'\xe4\xb8\xad\xe6\x96\x87')
>>> f.read()
b'\xe4\xb8\xad\xe6\x96\x87'


操作文件和目录

如果我们要操作文件、目录,可以在命令行下面输入操作系统提供的各种命令来完成,比如dir、cp等命令。

如果要在python程序中执行这些目录和文件操作怎么办?其实操作系统提供的命令只是简单的调用了操作系统提供的接口函数,Python内置的os模块也可以直接调用操作系统提供的接口函数。

打开python交互式命令行,我们来看看如何使用os模块的基本功能:

1
2
3
>>> import os
>>> os.name
'nt'

如果是nt说明是Windows系统,如果是posix,说明是Linux、Unix、Mac OS X系统。要获取详细的系统信息,可以调用uname()函数,不过在Windows上不提供。

环境变量

在操作系统中定义环境变量,全部保存在os.environ这个变量中,可以直接查看:

1
2
>>> os.environ
environ({'PATH': 'C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Users\\cdxu0\\AppData\\Local\\Programs\\Python\\Python35\\Scripts\\;C:\\Users\\cdxu0\\AppData\\Local\\Programs\\Python\\Python35\\', 'PROGRAMFILES(X86)': 'C:\\Program Files (x86)', 'APPDATA': 'C:\\Users\\cdxu0\\AppData\\Roaming', 'FPS_BROWSER_USER_PROFILE_STRING': 'Default', 'COMMONPROGRAMFILES': 'C:\\Program Files\\Common Files', 'ALLUSERSPROFILE': 'C:\\ProgramData', 'COMSPEC': 'C:\\Windows\\system32\\cmd.exe', 'SESSIONNAME': 'Console', 'COMPUTERNAME': 'DESKTOP-F92KHJR', 'USERDOMAIN_ROAMINGPROFILE': 'DESKTOP-F92KHJR', 'SYSTEMDRIVE': 'C:', 'PROCESSOR_LEVEL': '6', 'COMMONPROGRAMFILES(X86)': 'C:\\Program Files (x86)\\Common Files', 'LOGONSERVER': '\\\\MicrosoftAccount', 'PROMPT': '$P$G', 'PROCESSOR_REVISION': '4501', 'PROCESSOR_IDENTIFIER': 'Intel64 Family 6 Model 69 Stepping 1, GenuineIntel', 'PROGRAMFILES': 'C:\\Program Files', 'USERNAME': 'cdxu0', 'PATHEXT': '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC', 'WINDIR': 'C:\\Windows', 'USERPROFILE': 'C:\\Users\\cdxu0', 'MOZ_PLUGIN_PATH': 'C:\\PROGRAM FILES (X86)\\FOXIT SOFTWARE\\FOXIT READER PLUS\\plugins\\', 'PROCESSOR_ARCHITECTURE': 'AMD64', 'LOCALAPPDATA': 'C:\\Users\\cdxu0\\AppData\\Local', 'PROGRAMDATA': 'C:\\ProgramData', 'HOMEPATH': '\\Users\\cdxu0', 'PSMODULEPATH': 'C:\\Program Files\\WindowsPowerShell\\Modules;C:\\Windows\\system32\\WindowsPowerShell\\v1.0\\Modules', 'TMP': 'C:\\Users\\cdxu0\\AppData\\Local\\Temp', 'FPS_BROWSER_APP_PROFILE_STRING': 'Internet Explorer', 'OS': 'Windows_NT', 'NUMBER_OF_PROCESSORS': '4', 'PROGRAMW6432': 'C:\\Program Files', 'SYSTEMROOT': 'C:\\Windows', 'COMMONPROGRAMW6432': 'C:\\Program Files\\Common Files', 'ASL.LOG': 'Destination=file', 'PUBLIC': 'C:\\Users\\Public', 'TEMP': 'C:\\Users\\cdxu0\\AppData\\Local\\Temp', 'USERDOMAIN': 'DESKTOP-F92KHJR', 'HOMEDRIVE': 'C:'})

要获取某个环境变量的值,可以调用os.environ.get('key'):

1
2
3
4
>>> os.environ.get('PATH')
'C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem;C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\;C:\\Users\\cdxu0\\AppData\\Local\\Programs\\Python\\Python35\\Scripts\\;C:\\Users\\cdxu0\\AppData\\Local\\Programs\\Python\\Python35\\'
>>> os.environ.get('OS','default')
'Windows_NT'

操作文件和目录

操作文件和目录的函数一部分放在os模块中,一部分放在`os.path模块中,查看、创建、删除目录的调用如下:

1
2
3
4
5
6
>>> os.path.abspath('.')
'C:\\Users\\cdxu0'
>>> os.path.join('C://Users//cdxu0','testdir')
'C://Users//cdxu0\\testdir'
>>> os.mkdir('C://Users/cdxu0/testdir')
>>> os.rmdir('C://Users/cdxu0/testdir')

把两个路径合成一个时,不要直接拼字符串,而要通过os.path,join()函数,这样可以正确处理不同操作系统的路径分隔符。同样拆分路径时,也不要哦去拆字符串,而要通过os.path.split()函数,这样可以把一个路径拆为两部分,后一部分总是最后级别的目录或文件名:

1
2
>>> os.path.split('C://Users/cdxu0/testdir')
('C://Users/cdxu0', 'testdir')

os.pathsplitext()可以直接让你得到文件扩展名:

1
2
>>> os.path.splitext('C://Users/cdxu0/testdir')
('C://Users/cdxu0/testdir', '')

这些合并、拆分路径的函数并不要求目录和文件要真实存在,他们只对字符串进行操作。

文件操作使用下面的函数。假定当前目录下有一个test.txt文件:

1
2
3
>>> os.mkdir('test.txt')
>>> os.rename('test.txt','test.py')
>>> os.remove('test.py')

但是复制文件的函数不在os模块中,原因是复制文件并非是由操作系统提供的系统调用。shutil模块提供了copyfile()的函数,你可以在该模块中找到很多使用函数作为os模块的补充。

最后来看看如何利用python的特性来过滤文件。比如我们要列出当前目录下的所有目录:

1
2
>>> [x for x in os.listdir('.') if os.path.isdir(x)]
['.android', '.idlerc', '3D Objects', 'AppData', 'Application Data', 'Contacts', 'Cookies', 'Desktop', 'Documents', 'Downloads', 'Evernote', 'Favorites', 'IntelGraphicsProfiles', 'Links', 'Local Settings', 'Music', 'My Documents', 'NetHood', 'OneDrive', 'Pictures', 'PrintHood', 'Recent', 'Saved Games', 'Searches', 'SendTo', 'Templates', 'test.py', 'testdir', 'Videos', '「开始」菜单']

要列出所有的,.py文件:

1
2
>>> [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py']
['wenzi.py']

序列化

在程序运行过程中,所有的变量都是在内存中,比如,定义一个dict:

1
d=dict(name='Bob',age=20,score=99)

可以随时修改变量,比如把name改为bill,但是一旦程序结束,变量所占用的内存就会被系统全部回收。如果没有把修改后的bill存储到磁盘上,下次重新运行程序,变量又被初始化为Bob。

我们把变量从内存中变为可存储或传输的过程称之为序列化,在Python中叫picking,在其他语言中也被称为serialization,marshalling,flattening等。

序列化之后,就可以把序列化后的内容写入磁盘,或者通过网络传输到别的机器上。反过来吧变量内容从序列化的对象重新读到内存里称之为反序列化,即unpicking。

Python提供了pickle模块来实现序列化。

首先,我们尝试把一个对象序列化并写入文件:

1
2
3
4
>>> import pickle
>>> d=dict(name='Bob',age=20,score=99)
>>> pickle.dumps(d)
b'\x80\x03}q\x00(X\x04\x00\x00\x00nameq\x01X\x03\x00\x00\x00Bobq\x02X\x03\x00\x00\x00ageq\x03K\x14X\x05\x00\x00\x00scoreq\x04Kcu.'

pickle.dumps()方法把任意对象序列化成一个bytes,然后,就可以把这个bytes写入文件。或者用另一个方法pickle.dump()直接把对象序列化后写入一个file-like Object:

1
2
3
>>> f=open('dump.txt','wb')
>>> pickle.dump(d,f)
>>> f.close()

看看写入的dump.txt文件,内容很乱,这些都是python保存的对象内部信息。当我们要把对象从磁盘读到内存时,可以先把内容读到一个bytes,然后用pickle.loads()方法反序列化出对象,也可以直接用pickle.load()方法从一个file-like Object中直接反序列化出对象。我们打开另一个Python命令行来反序列化刚才保存的对象:

1
2
3
4
5
6
>>> f=open('dump.txt','rb')
>>> d=pickle.load(f)
>>> f.close
<built-in method close of _io.BufferedReader object at 0x0000028B1BA07938>
>>> d
{'name': 'BOB', 'age': 20, 'score': 99}

这个变量和原来的变量是完全不相干的对象,只是内容相同而已。

JSON

如果我们要在不同的编程语言之间传递对象,就必须把对象序列化为标准格式,比如XMl,但更好的方法是序列化为JSON,因为后者表示出来就是一个字符串,可以被所有语言读取,也可以方便的存储到磁盘或者通过网络传输。JSON不仅是标准格式,并且比XML更快,而且可以直接在WEB页面中读取,非常方便。

JSON表示的对象就是标准的JavaScript语言的对象,JSON和Python内置的数据类型对应如下:

JSON Pythonv
{} dict
[] list
“string” str
1234.56 int或float
true/false True/False
null None

Python内置的json模块提供了非常完善的Python对象到JSON格式的转化。把python对象变为一个JSON:

1
2
3
4
>>> import json
>>> d=dict(name='bob',age=34,score=99)
>>> json.dumps(d)
'{"score": 99, "name": "bob", "age": 34}'

dumps()方法返回一个str内容就是标准的JSON。类似的,dump()方法可以直接把JSON写入一个file-like Object。

要把JSON反序列化为Python对象,用loads()或者对应的load()方法可以直接把JSON的字符串反序列化,后者在file-like Object中读取字符串并反序列化:

1
2
3
>>> json_str='{"score": 99, "name": "bob", "age": 34}'
>>> json.loads(json_str)
{'score': 99, 'name': 'bob', 'age': 34}

由于JSON标准规定JSON编码是UTF-8,所有我们总是能正确的在python的str和JSON的字符串之间切换。

JSON进阶

Python的dict对象可以直接序列化为JSON的{},不过我们更喜欢用class表示对象,比如定义Student类,然后序列化:

1
2
3
4
5
6
7
8
>>> class Student(object):
... def __init__(self,name,age,score):
... self.name=name
... self.age=age
... self.score=score
...
>>> s=Student('Bob',20,90)
>>> print(json.dumps(s))

运行结果是TypeError,原因是Student对象不是一个可以序列化为JSON的对象。查看dumps()方法的参数列表,发现除了第一个必须的obj参数外,dumps()方法还提供了很多可选参数:
https://docs.python.org/3/library/json.html#json.dumps
这些可选参数就是让我们来定制JSON序列化。前面Student类实例无法序列化为JSON的原因是在默认情况下,dumps()方法不知道如何将Student实例变为一个JSON的{}对象。

可选参数default就是把任意一个对象变成一个可序列化为JSON的对象,我们只需要为Student专门写一个转换函数,再把函数传进去即可:

1
2
3
4
5
>>> def student2dict(std):
... return {'name':std.name, 'age':std.age, 'score':std.score}
...
>>> print(json.dumps(s,default=student2dict))
{"score": 90, "name": "Bob", "age": 20}

这样,Student实例首先被student2dict()函数转换成dict,然后再顺利的被序列化为JSON,但是如果改变类,则仍然无法序列化为JSON。我们可以把任意class的实例变为dict:

1
2
>>> print(json.dumps(s,default=lambda obj:obj.__dict__))
{"age": 20, "score": 90, "name": "Bob"}

因为通常class的实例都有一个__dict__属性,它就是一个dict,用来存储实例变量,也有少量例外,如定义了slots的class。

同样,如果我们要把JSON反序列化为一个Student 对象实例,loads()方法首先转换出一个dict对象,然后我们传入object_hook函数负责把一个dict转换为Studnet实例。

1
2
3
4
5
6
>>> def dict2student(d):
... return Student(d['name'],d['age'],d['score'])
...
>>> json_str='{"age":20, "score":88, "name":"Eason"}'
>>> print(json.loads(json_str, object_hook=dict2student))
<__main__.Student object at 0x00000262C641C6A0>

Python语言特定的序列化模块是pickle,但是如果要把序列化搞的更通用、更符合Web标准,就可以使用json模块。

json模块的dumps()和loads()函数是定义的很好的接口的典范。当我们使用时,只需要传入一个必须的参数,但是当默认的序列化或者反序列化机制不满足我们的要求时,我们又可以传入更多参数来定制序列化或者反序列化,即做到了接口简单易用,又做到了充分的扩展性和灵活性。

6.面向对象高级编程

发表于 2019-04-21 | 分类于 Python

面向对象高级编程

数据封装、继承和多态只是面向对象程序设计中最基础的三个概念。在Python中,面向对象还有很多高级特性,诸如多重继承、定制类、元类。

使用slots

正常情况下,当我们定义一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。

1
2
3
4
5
6
7
>>> class Student(object):
... pass
...
>>> s=Student()
>>> s.name='mick'
>>> print(s.name)
mick

这是给实例绑定了一个属性,还可以给实例绑定一个方法:

1
2
3
4
5
6
7
8
>>> def set_age(self, age):#定义一个函数作为实例方法
... self.age=age
...
>>> from types import MethodType
>>> s.set_age=MethodType(set_age, s)#给实例绑定一个方法
>>> s.set_age(25)#调用实例方法
>>> s.age#测试结果
25

但是,给一个实例绑定的方法,对另一个实例是不起作用的:

1
2
3
4
5
>>> s2=Student()
>>> s2.set_age(24)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'

为了给所有实例绑定方法,可以给class绑定方法:

1
2
3
4
5
6
7
8
9
10
>>> def set_score(self,score):
... self.score=score
...
>>> Student.set_score=set_score
>>> s.set_score(100)
>>> s.score
100
>>> s2.set_score(99)
>>> s2.score
99

通常情况下,上面的set_score方法可以直接定义在class中,但是动态绑定允许我们在程序运行过程中给class加上功能,这在动态语言中很难实现。

使用slots

但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加name和age属性。为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:

1
2
3
4
5
6
7
8
9
10
>>> class Student(object):
... __slots__=('name','age')
...
>>> s=Student()
>>> s.name='mic'
>>> s.age=25
>>> s.score=23
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

由于score没有被放到__slots__中,所以不能对其绑定属性。
使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:

1
2
3
4
5
>>> class GraduateStudent(Student):
... pass
...
>>> g=GraduateStudent()
>>> g.score=100

除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__

使用@propetry

在绑定属性时,如果我们直接把属性暴露出去,虽然写起来简单,但是,没有办法检查参数,导致可以把成绩随意修改:

1
2
>>> s=Student()
>>> s.score=10000

这显然不符合逻辑,为了限制score的范围,可以通过一个set_score()的方法来设置成绩,再通过一个get_score()的方法来取得成绩,这样,在set_score()方法里,就可以检查参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> s=Student()
>>> s.age=23
>>> s.age=23
>>> class Student(object):
... def get_score(self):
... return self._score
... def set_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
...

现在,对任意的Student实例进行操作,就不能随心所欲的设置score了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> s=Student()
>>> s.set_score(60)
>>> s.get_score()
60
>>> s.set_score(121)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in set_score
ValueError: score must between 0~100!
>>> s.set_score('abc')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in set_score
ValueError: score must be an integer!

但是上面的调用方法略显复杂,没有直接用属性那么直接简单。
Python内置的@property装饰器负责把一个方法变成属性调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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
...

@property的实现比较复杂,我们先观察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@property,负责把一个setter方法变成属性赋值,于是,我们拥有一个可控的属性操作:

1
2
3
4
5
6
7
8
9
>>> s=Student()
>>> s.score=90
>>> s.score
90
>>> s.score=9900
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 12, in score
ValueError: score must between 0~100

注意到@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法实现的。还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

1
2
3
4
5
6
7
8
9
10
11
12
>>> class Student(object):
...
... @property
... def birth(self):
... return self._birth
... @birth.setter
... def birth(self, value):
... self._birth=value
... @property
... def age(self):
... return 2015-self._birth
...

上面的birth是可读写属性,而age是一个只读属性,因为age可以根据birth和当前时间计算出来。


多重继承

继承是面向对象编程的一个重要方式,因为通过继承,子类就可以扩展父类的功能。

前面讲到Animal类层次的设计,假设我们要实现下列四种动物,Dog、Bat、Parrot、Ostrich。

如果按照哺乳类动物和鸟类归类:
Alt text

如果按照能飞能跑来归类:
Alt text

把上面两种都包含进来,我们就得设计更多的层次:
哺乳类:能跑的和能飞的
鸟 类: 能跑的和能飞的
Alt text

继续这样弄下去,类的数量会呈指数增长,正确的做法是采用多重继承。首先主要的类仍然按照哺乳类和鸟类设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> class Animal(object):
... pass
...
>>> class Mammal(Animal):
... pass
...
>>> class Bird(Animal):
... pass
...
>>> class Dog(Mammal):
... pass
...
>>> class Bat(Mammal):
... pass
...
>>> class Parrot(Bird):
... pass
...
>>> class Ostrich(Bird):
... pass
...

现在,我们给动物再加上Runnable和Flyable的功能,只需要定义好这两个类:

1
2
3
4
5
6
7
8
>>> class Runnable(object):
... def run(self):
... print('Running......')
...
>>> class Flyable(object):
... def fly(self):
... print('Flying......')
...

对于需要Runnable功能的动物,就多继承一个Runnable,如:

1
2
3
4
5
6
>>> class Dog(Mammal, Runnable):
... pass
...
>>> class Bat(Mammal,Flyable):
... pass
...

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

Mixln

在设计类的继承关系的时候,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要混入额外的功能,通过多重继承就可以实现,比如,让Ostrich额外再继承Runnable。这种设计通常称为Mixln.

为了更好的看出继承关系,我们把Runnable``Flyable改为RunnableMixln和FlyableMixln。类似的,还可以定义肉食动物和植食动物,让一个动物拥有好几个Mixln。Mixln的目的是为了给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个Mixln的功能,而不是设计多层次的复杂的继承关系。

Python自带的很多库也使用了Mixln。比如,Python自带了TCPServer和UDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn和ThreadingMixIn提供,通过组合,我们可以创建出合适的服务来。
首先分别编写一个多进程的TCP服务和多线程的UDP服务,定义如下:

1
2
3
4
5
6
>>> class MyTCPServer(TCPServer,ForkingMixIn):
... pass
...
>>> class MyUDPServer(UDPServer,ThreadingMixIn):
... pass
...


定制类

看到类似__slots__这种形式的变量或者函数名要注意,这些在python中是有特殊的用途的,可以帮我们定制类。

__str__

我们先定义一个Student的类,打印一个实例:

1
2
3
4
5
6
>>> class Student(object):
... def __init__(self,name):
... self.name=name
...
>>> print(Student('BOB'))
<__main__.Student object at 0x00000153E7856CC0>

打印出来的<__main__.Student object at 0x00000153E7856CC0>并不好看,此时需要定义__str__方法,返回一个好看的字符串就好了:

1
2
3
4
5
6
7
8
>>> class Student(object):
... def __init__(self,name):
... self.name=name
... def __str__(self):
... return 'Student object (name: %s)' %self.name
...
>>> print(Student('BOB'))
Student object (name: BOB)

但是如果直接敲变量不用print,打印出来的实例和前面的结果是一样的,这是因为直接显示变量调用的不是__str__()而是__repr__,两者的区别在于前者返回用户看到的字符串,后者返回程序开发者看到的字符串,也就是说后者视为调试服务的。解决的办法是再定义一个__rper__()但是通常两者代码是一样的:

1
2
3
4
5
6
7
8
>>> class Student(object):
... def __init__(self,name):
... self.name=name
... def __str__(self):
... return 'Student object (name: %s)' %self.name
... __repr__=__str__
>>> print(Student('BOB'))
Student object (name: BOB)

__iter__

如果一个类想被用于for...in循环,类似list或tuple那样,就必须实现一个__iter__()方法拿到循环的下一个值,直到遇到StopIteration错误的时退出循环。我们以斐波那契数列为例,写一个Fib类:

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
>>> 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
...
>>> for n in Fib():
... print(n)
...
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025

__getitem__

Fib实例虽然能作用于for循环,看起来和list有点像,但是不可以把它当做list来使用。要表现的像list那样可以按照下标取出元素,需要实现__getitem__方法:

1
2
3
4
5
6
7
>>> class Fib(object):
... def __getitem__(self,n):
... a,b=1,1
... for x in range(n):
... a,b=b,a+b
... return a
...

此时,就可以按照下标来访问数列的任一项了:

1
2
3
4
5
6
7
>>> f=Fib()
>>> f[13]
377
>>> f[2]
2
>>> f[22]
28657

但是list有个神奇的切片方法:

1
2
>>> list(range(100))[5:10]
[5, 6, 7, 8, 9]

对于Fib却报错,原因是__getitem__传入的参数可能是一个int,也可能是一个切片对象slice,所以要做出判断:

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
>>> class Fib(object):
... def __getitem__(self,n):
... if isinstance(n,int):
... a,b=1,1
... for x in range(n):
... a,b=b,a+b
... return a
... if isinstance(n,slice):
... start=n.start
... stop=n.stop
... if start is None:
... start=0
... a,b=1,1
... L=[]
... for x in range(stop):
... if x >=start:
... L.append(a)
... a,b =b,a+b
... return L
...
>>> f=Fib()
>>> f[0:6]
[1, 1, 2, 3, 5, 8]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
>>> f[3:9]
[3, 5, 8, 13, 21, 34]

但是没有对step参数的处理,也没有对负数做处理,所以,要正确实现一个__getitem__还是有很多工作要做的。此外,如果把对象看成dict,__getitem__()参数也可能是一个座位key的object,例如str。与之对应的是__setitem__()方法,把对象是做list或者是dictionary来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素。总之,通过上面的方法,我们自己定义的类表现的和python自带的list。tuple,dict没什么区别。

__getattr__

正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错:

1
2
3
4
5
6
7
8
9
10
11
>>> class Student(object):
... def __init__(self):
... self.name='BOB'
...
>>> s=Student()
>>> s.name
'BOB'
>>> s.score
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

错误信息告诉我们没有找到score这个attribute。要避免这个错误,除了可以加上一个score属性之外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class Student(object):
... def __init__(self):
... self.name='BOB'
... def __getattr__(self,attr):
... if attr=='score':
... return 99
... if attr=='age':
... return lambda:25
...
>>> s=Student()
>>> s.name
'BOB'
>>> s.score
99
>>> s.age()
25

当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self,'score')来尝试获取属性,这样,我们就有机会返回score的值。也可以返回函数,注意调用方法。

只有在没有找到属性的情况下,才会调用__getattr__(),已有的属性不会再__getattr__()里面查找。此外我们注意到任意调用如s.abc都会返回None,这是因为我们定义的__getattr__()默认返回的就是None。要让class只相应特定的几个属性,我们就要按照约定抛出AttributeError的错误。

1
2
3
4
5
6
7
...
def __getattr__(self,attr):
... if attr=='score':
... return 99
... if attr=='age':
... return lambda:25
... raise AttributeError('\'Student\' object has no attribution \'%s\'' %attr)

这个实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要特殊手段,这种完全动态调用的特性有什么实际作用呢??作用就是可以针对完全动态的情况作调用。

举个例子:
现在很多网站都搞REST API,调用API的URL类似:

http://api.server/user/friends
http://api.server/user/timeline/list

如果要写SDK,给每个URL对应的API都写一个方法,工作量很大,而且API改动的话,SDK也要改动。

利用完全动态的__getattr__(),我们可以写出一个链式调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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

这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用。

__call__

一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用,也可以直接在实例本身调用。

任何类,只要定义一个__call__()方法,就可以直接对实例进行调用。请看示例:

1
2
3
4
5
6
7
8
9
10
11
>>> class Student(object):
... def __init__(self,name):
... self.name=name
...
... def __call__(self):
... print('My name is %s ' %self.name)
...
>>> s=Student('Bob')
>>>
>>> s()
My name is Bob

__call__()还可以定义参数,对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,两者之间本身就没啥根本区别。

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

那么如何判断一个变量是函数还是对象呢?更多的时候,我们需要判断一个函数能否被调用,能被调用的对象就是一个Callable函数,比如函数和我们上面定义的带有__call__()的类实例:

1
2
3
4
5
6
>>> callable(Student('ddd'))
True
>>> callable(max)
True
>>> callable([1,2,3])
False

通过callable()函数,可以判断一个对象是否可以被调用。

使用枚举类

当我们需要定义常量时,一个办法就是用大写变量通过整数来定义,例如月份:

1
2
3
4
JAN=1
FEB=2
...
DEC=12

好处是简单,缺点是类型是int,并且仍然是变量。

更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> from enum import Enum
>>> Month=Enum('Month',('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'))
>>> for name,member in Month.__members__.items():
... print(name,'=>',member,',',member.value)
...
Jan => Month.Jan , 1
Feb => Month.Feb , 2
Mar => Month.Mar , 3
Apr => Month.Apr , 4
May => Month.May , 5
Jun => Month.Jun , 6
Jul => Month.Jul , 7
Aug => Month.Aug , 8
Sep => Month.Sep , 9
Oct => Month.Oct , 10
Nov => Month.Nov , 11
Dec => Month.Dec , 12

这样我们就获得了Month的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举他的所有类型。value属性是自动赋给成员的int常量,默认从1开始。如果需要更准确的控制枚举类型,可以从Enum派生出自定义类:

1
2
3
4
5
6
7
8
9
10
11
>>> from enum import Enum,unique
>>> @unique
... class Weekday(Enum):
... Sun=0
... Mon=1
... Tue=2
... Wed=3
... Thu=4
... Fri=5
... Sat=6
...

@unique装饰器可以帮助我们检查保证没有重复值。

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
>>> day1=Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1==Weekday.Mon)
True
>>> print(day1==Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> Weekday(1)
<Weekday.Mon: 1>
>>> Weekday(7)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\cdxu0\AppData\Local\Programs\Python\Python35\lib\enum.py", line 235, in __call__
return cls.__new__(cls, value)
File "C:\Users\cdxu0\AppData\Local\Programs\Python\Python35\lib\enum.py", line 470, in __new__
raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 7 is not a valid Weekday
>>> for name,member in Weekday.__members__.items():
... print(name,'=>',member)
...
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat


使用元类

type()

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时决定的,而是运行时动态创建的。

比如我们要写一个Hello的class ,就写一个hello.py模块:

1
2
3
4
>>> class Hello(object):
... def hello(self,name='world'):
... print('Hello,%s' %name)
...

当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的对象:

1
2
3
4
5
6
7
>>> h=Hello()
>>> h.hello()
Hello,world
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,他的类型就是classHello。

我们说class的定义是运行时动态创建的,而创建出class的方法就是使用type()函数。

type()函数既可以返回一个函数的类型,也可以创建出新的类型,比如我们可以通过type()函数创造出Hello类,而无需通过class Hello(object)的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def fn(self,name='world'):
... print('Hello, %s' %name)
...
>>> Hello=type('Hello',(object,),dict(hello=fn))
>>> h=Hello()
>>> h.hello
<bound method fn of <__main__.Hello object at 0x000001C2A4956470>>
>>> h.hello()
Hello, world
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

要创建一个class对象,type()函数要依次传入3个参数:
1.class的名称
2.继承的父类集合,注意Python支持多重继承,如果只有一个父类,记得tuple的单元素写法
3.class的方法和函数名称绑定,这里我们把函数fn绑定到方法名hello上。

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅扫描一下class定义的语法,然后调用type()函数创建出class。

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

metaclass

除了使用type()动态创建类外,要控制类的创建行为,还可以用metaclass。

metaclass,直译为元类,也就是当我们定义了类之后,就可以根据这个类创建出实例,所以先定义类,然后创建出实例。但是如果我们想创建出类,就必须根据metaclass来创建出类,也就是先定义metaclass,然后创建类。
也就是先定义metaclass,然后创建类,最后创建实例。

所以metaclass允许你创建类或者修改类,可以把类看成metaclass创建出的实例。

metaclass比较难理解,可以用metaclass给我们定义的MyList增加一个add方法:

1
2
3
4
5
>>> class ListMetaclass(type):
... def __new__(cls, name, bases, attrs):
... attrs['add']=lambda self, value:self.append(value)
... return type.__new__(cls,name,bases,attrs)
...

有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass:

1
2
3
>>> class MyList(list,metaclass=ListMetaclass):
... pass
...

当我们传入关键字参数metaclass时,它指示Python解释器在创建MyList时,要通过ListMetaclass.__new__()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后返回修改后的定义。

__new__()方法接收到的参数依次是
1.当前准备创建的类的对象
2.类的名字
3.类继承的父类集合
4,类的方法集合

测试一下MyList:

1
2
3
4
>>> L=MyList()
>>> L.add(1)
>>> L
[1]

而普通的list没有add()方法:

1
2
3
4
5
>>> L2=list()
>>> L2.add(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'

动态修改的意义在哪里?正常情况下直接在MyList定义中写上add(),通过metaclass修改类的定义很麻烦,但是也有应用的地方,比如ORM。

ORM全程为’Object Relational Mapping’,也就是对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样写代码会更加简单,不用直接操作SQL语句。

要编写一个ORM框架,所有类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。

我们来编写一个ORM框架。编写底层模块的第一步,就是先把调用的接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待写出的代码:

1
2

7. 错误、调试和测试

发表于 2019-04-21 | 分类于 Python

错误、调试和测试

在程序运行过程中,总会遇到各种各样的错误。

有的错误是程序编写有问题造成的,比如本来应该输出整数结果输出了字符串,这种错误我们通常称之为bug,bug是必须修复的。

有的错误是用户输入造成的,比如让用户输入email地址,结果得到一个空字符串,这种错误可以通过检查用户输入来做相应的处理。

还有一类错误是完全无法在程序运行过程中预测的,比如写入文件的时候,磁盘满了,写不进去了,或者从网络抓取数据,网络突然断掉了。这类错误也称为异常,在程序中通常是必须处理的,否则,程序会因为各种问题终止并退出。

Python内置了一套异常处理机制,来帮助我们进行错误处理。

此外,我们也需要跟踪程序的执行,查看变量的值是否正确,这个过程称为调试。Python的pdb可以让我们以单步方式执行代码。

最后,编写测试也很重要。有了良好的测试,就可以在程序修改后反复运行,确保程序输出符合我们编写的测试。

错误处理

在程序运行过程中,如果发生了错误,可以预先约定返回一个错误代码,这样就知道是否有错,以及错误的原因了。在操作系统提供的调用中,返回错误代码非常常见。比如打开文件的函数open(),成功时返回文件描述符,出错时返回-1。

用错误码来表示是否出错十分不便,因为函数本身该返回的正常结果和错误码混杂在一起,造成调用者必须用大量的代码来判断是否出错:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def foo():
... r=some_function()
... if r==(-1):
... return (-1)
... return r
...
>>> def bar():
... r=foo()
... if r==(-1):
... print('Error')
... else:
... pass
...

一旦出错,还要一级一级的上报,直到某个函数可以处理该错误。所以高级语言通常内置了一套try...except...finally...的错误处理机制,Python也不例外。

try

让我们用一个例子来看看try的机制:

1
2
3
4
5
6
7
8
9
>>> try:
... print('trying')
... r=10/0
... print('result:',r)
... except ZeroDivisionError as e:
... print('except:',e)
... finally:
... print('finally..')
... print('End')

当我们认为某段代码可能会出错的时候,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except 语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕!

上面代码在计算10/0时会产生一个除法运算错误:

1
2
3
4
trying
except: division by zero
finally..
End

从输出可以看出,当错误发生的时候,后续语句print('result:',r)不会被执行,except由于捕获到了ZeroDivisionError,因此被执行。最后,finally语句被执行。然后程序按照流程走下去。

如果把除数0改为2,则执行结果如下:

1
2
3
4
trying
result: 5.0
finally...
end

由于没有错误发生,所以except语句块不会被执行,但是finally如果有,则一定会被执行 。

而且错误应该有很多种类,如果发生了不同类型的错误,应该由不同类型的except语句块处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> try:
... print('trying')
... r=10/int('a')
... print('result:',r)
... except ValueError as e:
... print('ValueError:',e)
... except ZeroDivisionError as e:
... print('ZeroDivisionError:',e)
... finally:
... print('finally...')
... print('END')
...
trying
ValueError: invalid literal for int() with base 10: 'a'
finally...
END

int()函数可能会抛出ValueError错误,所以我们用一个except捕获ValueError,用另一个except捕获ZeroDivisionError。

此外,如果没有错误发生,可以在except后面语句块加一个else,当没有错误发生时,会自动执行else语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> try:
... print('trying')
... 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')
...
trying
result: 5.0
no error!
finally...
END

Python 的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其他子类而已捕获了,比如:

1
2
3
4
5
6
7
>>> 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()调用foo(),foo()调用bar(),结果bar()出错了,这时只要main()捕获到了,就可以处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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:

1
2
3
4
5
6
7
8
9
def foo(s):
return 10/int(s)
def bar(s):
return foo(s)*2
def main():
bar('0')
main()

执行结果如下:

1
2
3
4
5
6
7
8
9
10
Traceback (most recent call last):
File "err.py", line 13, in <module>
main()
File "err.py", line 11, in main
bar('0')
File "err.py", line 8, in bar
return foo(s)*2
File "err.py", line 5, in foo
return 10/int(s)
ZeroDivisionError: division by zero

解读错误信息是定位错误的关键。我们从上往下可以看到整个错误的调用函数链:

错误信息第一行:

Traceback (most recent call last):

告诉我们这是错误的跟踪信息。

第二行和第三行:

File “err.py”, line 13, in
main()

调用main()出错了。在 代码文件err.py的第13行代码,但是原因在第11行:

File “err.py”, line 11, in main
bar(‘0’)

调用bar(0)出错了,在代码文件的第11行,但是原因在第8行:

File “err.py”, line 8, in bar
return foo(s)*2

原因是retrun foo(s)*2这个语句出错了,但这不是最终原因:

File “err.py”, line 5, in foo
return 10/int(s)

原因是return 10/int(s)这个语句出错了,这个是错误的源头,因为下面打印了:

ZeroDivisionError: division by zero

根据错误类型ZeroDivisionError,我们判断int(s)本身并没有错,但是int(s)返回0,在计算10/0时出错。

记录错误

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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')

同样是出错,但程序打印完错误信息会继续执行并正常退出:

1
2
3
4
5
6
7
8
9
10
ERROR:root:division by zero
Traceback (most recent call last):
File "err_logging.py", line 14, in main
bar('0')
File "err_logging.py", line 10, in bar
return foo(s)*2
File "err_logging.py", line 7, in foo
return 10/int(s)
ZeroDivisionError: division by zero
END

通过配置,logging还可以把错误记录到日志文件里,方便事后排查。

抛出错误

因为错误是class,捕获一个错误就是捕获该class的一个实例。因此错误不是凭空产生的,而是有意识的创建并抛出的。python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。

如果要抛出错误,首先要根据需要,定义一个错误的class,选择好继承关系,然后,用raise语句抛出一个错误的实例:

1
2
3
4
5
6
7
8
9
10
class FooError(ValueError):
pass
def foo(s):
n=int(s)
if n==0:
raise FooError('invald value:%s' % s)
return 10/n
foo('0')

执行,可以最后追踪到我们定义的错误:

1
2
3
4
5
6
7
D:\笔记\Python\Notepad++>python err_raise.py
Traceback (most recent call last):
File "err_raise.py", line 13, in <module>
foo('0')
File "err_raise.py", line 10, in foo
raise FooError('invald value:%s' % s)
__main__.FooError: invald value:0

只有在必要的时候才定义我们自己的错误类型。如果可以选择Python已有的内置的错误类型,尽量使用Python内置的错误类型。

最后,我们来看另一种错误处理的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def foo(s):
n=int('s')
if n==0:
raise ValueError('invalid value: %s' % s)
return 10/n
def bar(s):
try:
foo('0')
except ValueError as e:
print('ValueError!')
raise
bar()

在bar()函数中,我们明明已经捕获了错误,打印了一个ValueError!后,又把错误通过raise语句抛出去了。

捕获错误目的是记录,便于后续追踪。但是由于当前函数不知道应该怎么处理该错误,最恰当的做法就是继续上抛,让顶层调用者去处理。

raise语句如果不带参数,就会把当前错误原样抛出。此外,在except中raise一个Error,还可以把一种类型的错误转化为另一种类型:

1
2
3
4
try:
10/0
except ZeroDivisionError:
raise ValueError('input error!')

只要合理的转换逻辑都可以接受。


调试

程序能一次性写完并正常运行的概率很小,会有各种各样的bug需要修正,因此需要一整套调试程序的手段来修复。

第一种方法简单直接,就是用print()把可能有问题的变量打印出来:

1
2
3
4
5
6
7
8
9
def foo(s):
n=int(s)
print('>>>n=%d' %n)
return 10/int(s)
def main():
foo('0')
main()

执行后在输出中查找打印的变量值:

1
2
3
4
5
6
7
8
9
>>>n=0
Traceback (most recent call last):
File "err.py", line 12, in <module>
main()
File "err.py", line 10, in main
foo('0')
File "err.py", line 7, in foo
return 10/int(s)
ZeroDivisionError: division by zero

用print()最大的坏处是以后还要删掉他,运行结果中会包含很多垃圾信息。

断言

凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:

1
2
3
4
5
6
7
8
9
def foo(s):
n=int(s)
assert n!=0, 'n is zero'
return 10/int(s)
def main():
foo('0')
main()

assert的意思是,表达式n!=0应该是真,否则,根据程序运行的逻辑,后面的代码肯定会出错。如果断言失败,assert语句本身就会抛出AssertionError:

1
2
3
4
5
6
7
8
9
D:\笔记\Python\Notepad++>python err.py
Traceback (most recent call last):
File "err.py", line 12, in <module>
main()
File "err.py", line 10, in main
foo('0')
File "err.py", line 6, in foo
assert n!=0, 'n is zero'
AssertionError: n is zero

程序中如果到处都是assert,和print()相比没有好多少。不过启动Python解释器时可以用-0参数来关闭assert。关闭之后,可以把其当做pass来看。

logging

把print()替换为logging是第三种方法,和assert相比,logging不会抛出错误,而是可以输出到文件:

1
2
3
4
5
6
import logging
s='0'
n=int(s)
logging.info('n=%d' %n)
print(10/n)

logging.info()就可以输出一段文本。运行,发现除了ZeroDivisionError,没有任何信息。

1
2
3
4
Traceback (most recent call last):
File "err.py", line 9, in <module>
print(10/n)
ZeroDivisionError: division by zero

在import logging之后加上一行配置:

1
2
import logging
logging.basicConfig(level=logging.INFO)

输出为:

1
2
3
4
5
INFO:root:n=0
Traceback (most recent call last):
File "err.py", line 10, in <module>
print(10/n)
ZeroDivisionError: division by zero

这就是logging的好处,它允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起错用了。这样一来,你可以放心的输出不同级别的信息,也不用删除,最后统一控制输出那个级别的信息。

logging的另一个好处是通过简单的配置,一条语句可以同时输出到不同的地方,比如console和文件。

pdb

第四种方式是启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态:

1
2
3
s='0'
n=int(s)
print(10/n)

运行结果:

1
2
3
D:\笔记\Python\Notepad++>python -m pdb err.py
> d:\笔记\python\notepad++\err.py(4)<module>()
-> s='0'

以参数-m pdb启动后,pdb定位到下一步要执行的代码-> s='0'.输入命令l来查看代码:

1
2
3
4
5
6
7
8
(Pdb) l
1 #!/usr/bin/env python3
2 #-*- coding: utf-8 -*-
3
4 -> s='0'
5 n=int(s)
6 print(10/n)
[EOF]

输入命令n可以单步执行代码:

1
2
3
4
5
6
(Pdb) n
> d:\笔记\python\notepad++\err.py(5)<module>()
-> n=int(s)
(Pdb) n
> d:\笔记\python\notepad++\err.py(6)<module>()
-> print(10/n)

任何时候都可以输入命令p 变量名来查看变量:

1
2
3
4
(Pdb) p s
'0'
(Pdb) p n
0

输入命令q结束调制:

1
2
3
(Pdb) q
D:\笔记\Python\Notepad++>

pdb.set_trace()

这个方法也是用pdb,但是不需要单步执行,我们只需要import pdb,然后,可能出错的地方放一个pdb.set_trace(),就可以设置一个断点:

1
2
3
4
5
6
import pdb
s='0'
n=int(s)
pdb.set_trace() #断点位置
print(10/n)

运行代码,程序会在pdb.set_trace()暂停并进入pdb调试环境,可以用命令p查看变量,或者用命令c继续运行:

1
2
3
4
5
6
7
8
D:\笔记\Python\Notepad++>python -m pdb err.py
> d:\笔记\python\notepad++\err.py(4)<module>()
-> import pdb
(Pdb) c
> d:\笔记\python\notepad++\err.py(9)<module>()
-> print(10/n)
(Pdb) p n
0

IDE

可以用IDE调试,如PyCharm:enter link description here
或者Eclipse加上pydev。

IDE调试很方便,但是logging更适合python调试。

单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。比如对函数abs(),我们可以编写出以下几个测试用例:

1.输入正数,1、2.3、0.34,期待返回值和输入值相同
2.输入负数,-1、-1.2、-0.33,期待返回值与输入相反
3.输入0,期待返回0
4.输入非数值类型,比如None、[]、{},期待抛出TypeError

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

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

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

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

我们来编写一个Dict类,这个类的行为和dict一致,但是可以通过属性来访问,用起来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> 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
...
>>> d=Dict(a=1,b=2)
>>> d['a']
1
>>> d.a
1

为了编写单元测试,我们需要引入Python自带的unittest模块,编写mydict_test.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
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():

1
self.assertEqual(abs(-1),1) #断言函数返回的结果和1相等

另一种重要的断言就是期待抛出指定类型Error,比如通过d['empty']访问不存在的key时,断言会抛出KeyError:

1
2
with self.assertRaises(KeyError):
value=d['empty']

而通过d.empty访问不存在key时,我们期待抛出AttributeError:

1
2
with self.assertRaises(AttributeError):
value=d.empty

运行单元测试

一旦编写好单元测试,我们就可以运行单元测试。最简单的方式是在mydict_test.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
D:\笔记\Python\Notepad++>python mydict.py
D:\笔记\Python\Notepad++>python mydict_test.py
EE...
======================================================================
ERROR: test_attr (__main__.TestDict)
----------------------------------------------------------------------
Traceback (most recent call last):
File "mydict_test.py", line 25, in test_attr
self.assertEqual(d[key],'value')
NameError: name 'key' is not defined
======================================================================
ERROR: test_attrerror (__main__.TestDict)
----------------------------------------------------------------------
Traceback (most recent call last):
File "D:\笔记\Python\Notepad++\mydict.py", line 11, in __getattr__
return self[key]
KeyError: 'empty'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "mydict_test.py", line 35, in test_attrerror
value=d.empty
File "D:\笔记\Python\Notepad++\mydict.py", line 13, in __getattr__
raise ArithmeticError(r"'Dict' object has no attrribute '%s'" %key)
ArithmeticError: 'Dict' object has no attrribute 'empty'
----------------------------------------------------------------------
Ran 5 tests in 0.004s

或者在命令行通过参数-m unittest直接运行单元测试。

setUp与tearDown

可以在单元测试中编写两个特殊的setup()和tearDown()有什么用呢,这时,就可以在setUp()方法中连接数据库,在tearDown()方法中关闭数据库,这样不必再每个测试方法中重复相同的代码:

1
2
3
4
5
6
7
class TestDict(unittest.TestCase):
def setUp(self):
print('setUp...')
def tearDown(self):
print('tearDown...')


文档测试

很多文档都有实例代码,比如re模块:

1
2
3
4
>>> import re
>>> m=re.search('(?<=abc)def','abcdef')
>>> m.group(0)
'def

可以把这些示例代码在Python的交互环境下输入并执行,结果与文档中的实例代码显示一致。

这些代码与其他说明可以写在注释中,然后由一些工具来自动生成文档。也可以自动执行写在注释中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def abs(n):
... '''
... Function to get absolute value of number.
... Exampel:
... >>>abs(1)
... 1
... >>>abs(-1)
... 1
... >>>abs(0)
... 0
... '''
... return n if n>=0 else (-n)
...

无疑问更明确的告诉函数的调用者该函数的期望输入和输出。并且,Python内置的文档测试(doctest)模块可以直接提取注释中的diamante并执行测试。

doctest严格按照Python交互命令行的输入和输出来判断测试结果是否正确。只有测试异常的时候,可以用...表示中间一大段烦人的输入。

用doctest来测试上次编写的Dict类:

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
class Dict(dict):
'''
Simple dict but also support access as x.y style.
>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
'''
def __int__(self,**kw):
super().__int__(**kw)
def __getattr__(self,key):
try:
return self[key]
except KeyError:
raise ArithmeticError(r"'Dict' object has no attrribute '%s'" %key)
def __setattr__(self,key,value):
self[key]=value
if __name__=='__main__':
import doctest
doctest.testmod()

运行结果:

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
D:\笔记\Python\Notepad++>python mydict2.py
**********************************************************************
File "mydict2.py", line 23, in __main__.Dict
Failed example:
d2.empty
Expected:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
Got:
Traceback (most recent call last):
File "mydict2.py", line 33, in __getattr__
return self[key]
KeyError: 'empty'
<BLANKLINE>
During handling of the above exception, another exception occurred:
<BLANKLINE>
Traceback (most recent call last):
File "C:\Users\cdxu0\AppData\Local\Programs\Python\Python35\lib\doctest.py", line 1320, in __run
compileflags, 1), test.globs)
File "<doctest __main__.Dict[8]>", line 1, in <module>
d2.empty
File "mydict2.py", line 35, in __getattr__
raise ArithmeticError(r"'Dict' object has no attrribute '%s'" %key)
ArithmeticError: 'Dict' object has no attrribute 'empty'
**********************************************************************
1 items had failures:
1 of 9 in __main__.Dict
***Test Failed*** 1 failures.

5. 面向对象编程

发表于 2019-04-21 | 分类于 Python

面向对象编程.md#面向对象编程

面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。

面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。

而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发来的消息并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类Class的概念。

我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。假定我们要处理学生的成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个dict表示:

1
2
>>> std1={'name': 'Michael', 'score': 98 }
>>> std2={'name': 'Bob', 'score': 82 }

而处理学生成绩可以通过函数实现,比如打印学生的成绩:

1
2
3
4
5
>>> def print_score(std):
... print('%s: %s' %(std['name'], std['score']))
...
>>> print_score(std1)
Michael: 98

如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是student这种数据类型应该被视为一个对象,这个对象拥有自己name和score两个属性。如果要打印一个同学的成绩,必须创建出这个学生对应的对象,然后给对象发一个print_score消息,让对象自己把自己的数据打印出来。

1
2
3
4
5
6
7
8
9
>>> class Student(object):
... def __init__(self, name, score):
... self.name=name
... self.score=score
... def print_score(self):
... print('%s: %s' %(self.name, self.score))
...

给对象发消息实际上就是调用对象的相关函数,我们称之为对象的方法Method,面向对象的程序写出来就像这样:

1
2
3
4
5
6
>>> bart=Student('Bart Simpson',59)
>>> lisa=Student('Lisa Simpson',89)
>>> bart.print_score()
Bart Simpson: 59
>>> lisa.print_score()
Lisa Simpson: 89

面向对象的设计思想是从自然界中来的,因为在自然界中,类Class和实例Instance的概念是很自然的。Class是一种抽象概念,比如我们定义的Class—Student,是指学生这个概念,而实例Instance则是一个个具体的Student。

所以面向对象的抽象程度又比函数要高,因为一个Class即包含数据又包含操作数据的方法。

数据封装,继承和多态是面向对象的三大特点。

类和实例

面向对象最重要的概念就是类Class和实例Instance,必须牢记类是抽象的模板,实例是根据类创建出来的一个个具体的对象,每个对象都拥有相同的方法,但是各自的数据可能不同。

仍以Student类为例,在Python中,定义类是通过class关键字:

1
2
3
>>> class Student(object):
... pass
...

class后面紧跟着类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面会讲,通常,如果没有合适的继承类,就是用object类,这是所有类都会继承的类。

定义好了Student类,就可以根据Student类创建出Student实例,创建实例是通过类名+()来实现的:

1
2
3
4
5
>>> bart=Student()
>>> bart
<__main__.Student object at 0x000002C2CE396160>
>>> Student
<class '__main__.Student'>

可以看到,变量bart指向的就是一个Student的实例,后面的0x000002C2CE396160是内存地址,每个object的地址都不一样,而Student本身则是一个类,可以自由的给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:

1
2
3
>>> bart.name='Bart Simpson'
>>> bart.name
'Bart Simpson'

由于类可以起到模板的作用,因此在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__的方法,在创建实例的时候,就把name,score等属性绑定上去:

1
2
3
4
5
>>> class Student(object):
... def __init__(self,name,score):
... self.name=name
... self.score=score
...

注意到__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法的内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。有了__init__方法,在创建实例的时候,就不能传入空的参数了,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器会自己把实例变量传进去:

1
2
3
4
5
6
7
>>> bart=Student('Bart Simpson',98)
>>> bart.name
'Bart Simpson'
>>> bart.score
98
>>> bart
<__main__.Student object at 0x000002C2CE396278>

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数的没有什么区别,仍然可以使用默认参数,可变参数,关键字参数和命名关键字参数。

数据封装

面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:

1
2
3
4
5
>>> def print_score(std):
... print('%s:%s ' % (std.name,std.score))
...
>>> print_score(bart)
Bart Simpson:98

但是,既然Student实例本身就拥有这些数据,要访问这些数据就没有必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把数据给封装起来了。这些封装数据的函数和Student类本身是关联的,我们称之为类的方法:

1
2
3
4
5
6
7
8
9
10
>>> class Student(object):
... def __init__(self,name,score):
... self.name=name
... self.score=score
... def print_score(self):
... print('%s:%s' %(self.name,self.score))
...
>>> bart=Student('Bart Simpson',98)
>>> bart.print_score()
Bart Simpson:98

这样一来,我们从外部看Student类,就只需要知道,创建实例需要给出name和score,而如何打印,都是在Student类的内部定义的,这些数据和逻辑被封装起来了,调用很容易但是不知道内部实现的细节。

封装的另一个好处是可以给Student类增加新的方法,比如get_grade:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> class Student(object):
... def __init__(self,name,score):
... self.name=name
... self.score=score
... def print_score(self):
... print('%s:%s' %(self.name,self.score))
... def get_grade(self):
... if self.score>=90:
... return 'A'
... elif self.score>=80:
... return 'B'
... else:
... return 'C'
...
>>> bart=Student('Bart Simpson',98)
>>> bart.get_grade()
'A'

get_grade方法可以直接在实例变量上调用不需要知道内部的实现细节。

小结

类是创建实例的模板,而实例则是一个个具体的对象,各个实例拥有的数据都相互独立,互不影响;方法就是与实例绑定的函数和普通函数不同,方法可以直接访问实例的数据;通过在实例上调用 的方法,我们就直接操作了对象内部的数据,也就是说,对于两个实例变量,虽然他们都是同一个类的不同实例,但拥有的变量名称可能不同。

访问限制

在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,从前面Student来看,外部代码还是可以自由修改一个实例的name、score属性:

1
2
3
4
5
6
7
8
9
>>> bart.score
98
>>> bart.score=69
>>> bart.score
69
>>> bart.get_grade
<bound method Student.get_grade of <__main__.Student object at 0x000002C2CE3966A0>>
>>> bart.get_grade()
'C'

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在python中,实例的变量名如果以__开头,就变为了一个私有变量private,只有内部可以访问,外部不可以访问,我们把student类改一改:

1
2
3
4
5
6
7
>>> class Student(object):
... def __init__(self,name,score):
... self.__name=name
... self.__score=score
... def print_score(self):
... print('%s: %s' %(self.__name,self.__score))
...

改完后,对于外部代码来说没什么变动,但是已经无法从外部访问实例变量.__name和实例变量.__score了:

1
2
3
4
5
>>> bart=Student('Bart Simpson',99)
>>> bart.__name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'

这样就确保了外部代码不能随便修改内部的状态,这样通过访问限制的保护,代码更加稳定。但是如果外部代码要取得name和score怎么办?可以给Student类增加get_name,get_score的方法:

1
2
3
4
5
6
7
8
9
10
11
>>> class Student(object):
... def __init__(self,name,score):
... self.__name=name
... self.__score=score
... def print_score(self):
... print('%s: %s' %(self.__name,self.__score))
... def get_name(self):
... return self.__name
... def get_score(self):
... return self.__score
...

如果要允许外部代码修改score怎么办,需要给Student类增加set_score方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class Student(object):
... def __init__(self,name,score):
... self.__name=name
... self.__score=score
... def print_score(self):
... print('%s: %s' %(self.__name,self.__score))
... def get_name(self):
... return self.__name
... def get_score(self):
... return self.__score
... def set_score(self,score):
... self.__score=score
...

与原来直接通过bart.score=90相比,在方法中,可以对参数做检查,避免传入无效的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class Student(object):
... def __init__(self,name,score):
... self.__name=name
... self.__score=score
... def print_score(self):
... print('%s: %s' %(self.__name,self.__score))
... def get_name(self):
... return self.__name
... def get_score(self):
... return self.__score
... def set_score(self,score):
... if 0<=score<=100:
... self.__score=score
... else:
... raise ValueError('bad score')
...

需要注意的是,在Python中,变量名类似__xxx__的,也就是以下划线结尾的,是特殊百年来那个,特殊变量是可以直接访问的,不是private变量,所以,不能用__name__这样的变量名。
而以一个下划线开头的实例变量名,比如_name这样的实例变量外部是可以访问的,但是一般不随便访问此类实例。
双下划线开头的实例变量也不是一定不能从外部访问的,不能直接访问__name是因为Python解释器对外吧__name变量改为了_Student__,所以,仍然可以通过_Student__name来访问__name变量:

1
2
3
>>> bart=Student('Bart Simpson',99)
>>> bart._Student__name
'Bart Simpson'

但是强烈建议你不要这么做,因为不同版本的Python解释器可能会把__name改成不同的变量名。总的来说就是Python本身没有任何机制阻止你干坏事,全靠自觉。

最后注意下面这种错误写法:

1
2
3
4
5
6
7
8
>>> bart=Student('Bart Simpson',99)
>>> bart.get_name()
'Bart Simpson'
>>> bart.__name='fsfd'
>>> bart.__name
'fsfd'
>>> bart.get_name()
'Bart Simpson

表面上看,外部代码‘成功’的设置了__name变量,但是实际上这个__name变量和class内部的__name变量不是一个变量!内部的__name变量已经被Python解释器自动改成了_Student__name,而外部代码给bart增加了一个__name变量。


继承和多态

在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subcalss),而被继承的class称为基类、父类或超类(Base class、Super class)。

比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:

1
2
3
4
>>> class Animal(object):
... def run(self):
... print('Animal is running...')
...

当我们需要编写Dog和Cat类时,就可以直接从Animal类继承:

1
2
3
4
5
6
>>> class Dog(Animal):
... pass
...
>>> class Cat(Animal):
... pass
...

对于Dog来说,Animal就是它的父类,对于Animal来说,Dog就是它的子类。

继承最大的好处是获得了父类的全部功能。由于Animal实现了run()方法,因此,Dog和Cat作为他的子类,自动拥有了run()方法。

1
2
3
>>> dog=Dog()
>>> dog.run()
Animal is running...

也可以对子类增加一些方法:

1
2
3
4
5
6
7
8
9
10
>>> class Dog(Animal):
... def run(self):
... print('Dog is running...')
... def eat(self):
... print('Dog is eating...')
...
>>> dog=Dog()
>>> dog.eat()
Dog is eating...
>>>

继承的第二个好处需要我们队代码进行一点改进。当子类和父类存在相同的run()方法时,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()。这样,我们就获得了继承的另一个好处:多态。

要理解什么事多态,我们首先要对数据类型再做一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么区别:

> a=list() a是list的数据类型
    b=Animal()                       b是Animal的数据类型
    c=Dog()                            c是Dog的数据类型

判断一个变量是否是某个类型可以用isinstance()判断:

1
2
3
4
5
6
7
8
>>> isinstance(a,list)
True
>>> isinstance(b,Animal)
True
>>> isinstance(c,Animal)
True
>>> isinstance(b,Dog)
False

可以看出a,b,c确实对应着list、Animal、Dog这三种类型。而且c还对应着Animal类型。在继承关系中,如果一个实例的数据类型是某个子类,那他的数据类型可以看做是父类。但是反过来就不行。

为了更好的理解多态,我们还需要再编写一个函数,这个函数可以接受一个Animal类型的变量:

1
2
3
>>> def run_twice(animal):
... animal.run()
... animal.run()

当我们传入Animal的实例时,run_twice()就会打印出:

1
2
3
>>> run_twice(Animal())
Animal is running...
Animal is running...

当我们传入Dog实例时,run_twice()就打印出:

1
2
3
>>> run_twice(Dog())
Dog is running...
Dog is running...

看上去没什么,但是当我们再定义一个Tortoise类型,也从Animal派生:

1
2
3
>>> class Tortoise(Animal):
... def run(self):
... print('Tortoise is running slowly...')

当我们调用run_twice()时,传入Tortoise的实例:

1
2
3
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...

可以看出,新增一个Animal的子类,不必对run_twice()进行任何修改,实际上,任何依赖Animal作为参数的函数或方法都可以不加修改的正常运行,原因就在于多态。
多态的好处就是当我们需要传入Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以,因为前面三者都是Animal类型,然后按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就可以自动调用实际类型的run()方法。
对于一个变量,我们只需要知道他是Animal类型,无需确切的知道他的子类型,就可以放心的调用run()方法,而具体调用的run()方法是作用在Dog、Cat、Tortoise就是由运行时该对象的确切类型决定,也就是说多态调用中:调用方只管调用,不管细节,而当我们新增一种Aniaml的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调节的,这就是著名的开闭原则:

对扩展开放:允许新增Animal的子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
继承还可以一级一级的继承下来,就好比从爷爷到爸爸再到儿子这样的关系。而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的数。
Alt text
静态语言vs动态语言

对于静态语言来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的静态语言来说,则不一定需要传入Animal类型,我们只需要保证传入的对象有一个run()方法就可以。

这就是动态语言的‘鸭子类型’,它并不要求严格的继承体系,一个对象只要看起来像押走,走起路来像鸭子,那他就可以被看做是鸭子。
Python的“file-like object”就是一种鸭子类型。对真正的文件对象,它有一个read()的方法,返回其内容。但是,许多对象,只要有read()方法,都可以被视为“file-like object”。许多函数接收的参数就是“file-like object”,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。


获取对象信息

当我们拿到一个对象的引用时,如何知道这个对象是什么类型,有哪些方法?

type()

首先,我们判断对象类型,使用type()函数,基本类型都可以用type()判断:

1
2
3
4
5
6
>>> type(123)
<class 'int'>
>>> type('123')
<class 'str'>
>>> type(None)
<class 'NoneType'>

如果一个变量指向函数或者类,也可以用type()来判断:

1
2
3
4
5
6
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class 'list'>
>>> type(c)
<class '__main__.Dog'>

但是type()函数返回的是什么类型呢?它返回对应的Class类型。如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:

1
2
3
4
5
6
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('123')==type('abc')
True

判断基本数据类型可以直接写int、str等,但如果要判断一个对象是否是函数时,可以使用types模块中定义的常量:

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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->Husky,那么isinstance()就可以告诉我们一个对象是否是某种类型

1
2
3
4
5
6
7
8
9
>>> a=Animal()
>>> b=Dog()
>>> c=Husky()
>>> isinstance(c,Husky)
True
>>> isinstance(c,Dog)
True
>>> isinstance(c,Animal)
True

可以看出虽然c自身是Husky类型,但由于Husky是从Dog上继承下来的,所以c还是Dog类型。也就是说,isinstance()判断的是一个对象是否是该类型本身或者位于该类型的父继承链上。能用type()判断的类型都可以用isinstance()判断,并且还可以判断一个变量是否是某些类型中的一种:

1
2
3
4
5
6
7
8
>>> isinstance('a',str)
True
>>> isinstance(123,int)
True
>>> isinstance([1,2,3],(list,tuple))
True
>>> isinstance((1,2,3),(list,tuple))
True

dir()

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

1
2
>>> dir('123')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动调用该对象的__len__方法,所以下面代码是等价的:

1
2
3
4
>>> len('1234')
4
>>> '1234'.__len__()
4

我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:

1
2
3
4
5
6
7
>>> class MyDog(object):
... def __len__(self):
... return 100
...
>>> dog=MyDog()
>>> len(dog)
100

剩下的都是普通属性或方法,比如lower()返回小写的字符串:

1
2
>>> 'ADSAFD'.lower()
'adsafd'

仅仅把属性和方法列出来是不够的,配合getattr()``setattr()以及hasattr(),我们可以直接操作一个对象的状态:

1
2
3
4
5
6
7
>>> class MyObject(object):
... def __init__(self):
... self.x=9
... def power(self):
... return self.x*self.x
...
>>> obj=MyObject()

测试该对象的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> hasattr(obj,'x')
True
>>> obj.x
9
>>> hasattr(obj,'y')
False
>>> setattr(obj,'y',19)
>>> hasattr(obj,'y')
True
>>> getattr(obj,'y')
19
>>> obj.y
19

如果试图获取不存在的属性,会抛出AttributeError的错误:

1
2
3
4
>>> getattr(obj,'g')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'g'

可以传入一个default参数,如果属性不存在,就返回默认值:

1
2
>>> getattr(obj,'g',404)
404

通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。要注意的是,只有在不知道对象信息的时候,我们才会去获取对象的信息。

如果可以直接写:
sum=obj.x+obj.y
就不要写:
sum=getattr(obj,’x’)+getattr(obj,’y’)

一个正确的用法例子如下:

1
2
3
4
def readImage(fp):
if hasattr(fp, 'read'):
return readData(fp)
return None

如果我们要从文件流fp中读取图像,首先要判断该fp是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。


实例属性和类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量,或者通过self变量:

1
2
3
4
5
6
>>> class Student(object):
... def __init__(self,name):
... self.name=name
...
>>> s=Student('Bob')
>>> s.score=90

但是,如果Student类本身需要绑定一个属性的时候可以直接在class中定义属性,这种属性是类属性,归Student所有:

1
2
3
>>> class Student(object):
... name='sad'
...

当我们定义了一个类属性,这个属性虽然归类所有,但类的所有实例都可以访问到:

1
2
3
4
5
6
7
8
9
10
11
>>> s=Student()
>>> print(s.name)
sad
>>> s.name='Mick'
>>> print(s.name)
Mick
>>> print(Student.name)
sad
>>> del s.name
>>> print(s.name)
sad

可以看出,在编写程序的时候,千万不要把实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的是类属性。

3. 函数式编程

发表于 2019-04-21 | 分类于 Python

函数式编程

函数式Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。

而函数式编程—-Functional Programming,虽然也可以归结到面向过程的程序设计,但是其思想更接近数学计算。

我们首先要弄清楚计算机和计算的概念。在计算机的层次上,CPU执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。而计算则是指数学意义上的计算,越是抽象的计算,离计算机硬件越远。

对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如C语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如Lisp语言。

函数抽象编程是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入时确定的,输出就是确定的,这种函数我们称为没有副作用。而允许使用变量的程序设计语言,由于函数内部变量状态不确定,同样的输入,可能会得到不同的输出,因此这种函数式有副作用的。

函数式编程的一个特点就是,允许函数本身作为参数传入另一个函数,还允许返回一个函数!Python对函数式编程提供部分支持,由于Python允许使用变量,因此,Python不是纯函数编程式编程语言。

高阶函数

高阶函数—-Higher-order function。

变量可以指向函数

以Python内置的求绝对值的函数abs()为例子,调用该函数的代码如下:

1
2
>>> abs(-10)
10

如果只写abs:

1
2
>>> abs
<built-in function abs>

可见,abs是函数本身,abs(-10)是函数调用。

要获得函数调用结果,我们可以把结果赋值给变量:

1
2
3
>>> x=abs(-123)
>>> x
123

但是如果把函数本身赋值给变量呢:

1
2
3
>>> x=abs
>>> x(-123)
123

结论:函数本身也可以赋值给变量,即变量可以指向函数。如果一个变量指向了函数,那么,可以通过变量来调用这个函数。

函数名也是变量

函数名其实就是指向函数的变量!!!对于abs()这个函数,完全可以把函数名abs看成变量,他指向一个可以计算绝对值的函数。如果把abs指向其他对象,会发生什么??

1
2
3
4
5
>>> abs=1
>>> abs(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable

此时,abs这个变量已经不指向求绝对值函数,而是指向一个整数1。

传入函数

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数被称为高阶函数:

1
2
3
>>> def add(x,y,f):
... return f(x)*f(y)
...

当我们调用add(-4,2,abs)时,参数x,y,f分别接收-4,2,abs,验证一下:

1
2
>>> add(-4,2,abs)
8

编写高阶函数,就是让函数能够接收别的函数。

小结
把函数作为参数传入,这样的函数称之为高阶函数,函数式编程就是指这种高度抽象的编程范式。

map/reduce

Python中内建了map()和reduce()函数。

map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。举例如下:我们有一个函数f(x)=x^2,要把这个函数作用在一个list[1,2,3,4,5,6,7,8,9]上就可以用map实现:

1
2
3
4
5
6
7
>>> L=[1,2,3,4,5,6,7,8,9]
>>> def f(x):
... return x*x
...
>>> h=map(f,L)
>>> list(h)
[1, 4, 9, 16, 25, 36, 49, 64, 81]

map()传入的第一个参数是f,也就是函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列,因此需要list()函数让它把整个序列都计算出来并返回一个list。

但是其实用循环也可以实现上述功能:

1
2
3
4
5
6
>>> L=[]
>>> for x in [1,2,3,4,5,6,7,8,9]:
... L.append(f(x))
...
>>> L
[1, 4, 9, 16, 25, 36, 49, 64, 81]

这样也可以实现这个功能,但是循环中不能看出 ‘把f(x)作用于L上的每一个元素并生成一个新的list’ 的功能。所以map作为高阶函数,事实上吧运算规则抽象化了,因此,我们不仅可以计算简单的平法,也可以计算更加复杂的任意函数。比如把list 所有数字转为字符串:

1
2
>>> list(map(str,L))
['1', '4', '9', '16', '25', '36', '49', '64', '81']

reduce把一个函数作用在一个序列[x1,x2,x3,...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

1
2
3
4
5
6
>>> from functools import reduce
>>> def add(x,y):
... return x+y
...
>>> reduce(add,[1,2,3,4,5,6,7,8,9])
45

求和运算推荐用sum()函数,但是如果把序列[1,3,5,7,9]换为13579,就可以用reduce:

1
2
3
4
5
>>> def f(x,y):
... return 10*x+y
...
>>> reduce(f,[1,3,5,7,9])
13579

这个例子本身没有什么实用性,但是考虑到字符串str是一个序列,对上面个的例子进行改动并配合map()函数,我们就可以把str转为int的函数:

1
2
3
4
5
6
7
8
9
>>> from functools import reduce
>>> def f(x,y):
... return 10*x+y
...
>>> def char2num(s):
... return {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9}[s]
...
>>> reduce(f,map(char2num,'13579'))
13579

还可以用lambda函数进一步简化:

1
2
3
4
5
6
7
8
9
>>> from functools import reduce
>>> def char2num(s):
... return {'0':0,'1':1,'2':2,'3':3,'4':4,'5':5,'6':6,'7':7,'8':8,'9':9}[s]
...
>>> def str2int(s):
... return reduce(lambda x,y:x*10+y,map(char2num,s))
...
>>> str2int('13579')
13579

filter

Python内建的filter函数用于过滤序列。

和map类似,filter函数也接收一个函数和一个序列。不同的是filter把传入的函数依次作用于每个元素,然后根据返回值是Trueor False决定保留还是丢弃该元素。在一个list中,删掉偶数,保留奇数,可以这么写:

1
2
3
4
5
>>> def is_odd(s):
... return s%2==1
...
>>> list(filter(is_odd,[1,2,3,4,5,6,7,8,9]))
[1, 3, 5, 7, 9]

把一个序列中的空字符串删掉,可以这么写:

1
2
3
4
5
>>> def not_empty(s):
... return s and s.strip()
...
>>> list(filter(not_empty,['A','B',None]))
['A', 'B']

用filter求素数:

计算素数的一个方法是埃氏筛法,它的算法理解起来非常简单:
首先,列出从2开始的所有自然数,构造一个序列:2,3,4,5,6,7,8,9,10,...
取序列的第一个数2,他一定是素数,然后用2把序列的2的倍数筛掉:
3,5,7,9,11,13,...
取新数列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:
5,7,11,13,17,19,...
不断筛选下去,最终会得到所有的素数。

首先构造一个从3开始的奇数序列:

1
2
3
4
5
6
>>> def _odd_iter():
... n=1
... while True:
... n=n+2
... yield n
...

这是一个生成器,并且是一个无限序列。然后定义一个筛选函数:

1
2
3
>>> def _not_divisible(n):
... return lambda x:x%n>0
...

最后定义一个生成器,不断返回下一个素数:

1
2
3
4
5
6
7
8
>>> def primes():
... yield 2
... it=_odd_iter()
... while True:
... n=next(it)
... yield n
... it=filter(_not_divisible(n),it)
...

这个生成器先返回第一个素数2,然后利用filter不断产生筛选后的新的序列。由于primes也是一个无限序列,所以退出时需要一个退出循环的条件:

1
2
3
4
5
6
>>> for n in primes():
... if n<1000:
... print(n)
... else:
... break
...

sorted

排序算法

排序也是在程序中经常遇到的算法,无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但是如果是字符串或者两个dict时,直接比较数学书的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。

Python内置的sorted()函数就可以对list进行排序:

1
2
>>> sorted([23,43,5,-5,122])
[-5, 5, 23, 43, 122]

此外,sorted()函数也是一个高阶函数,它还可以接收一个key来实现自定义的排序,比如按照绝对值大小排序:

1
2
>>> sorted([23,43,5,-7,122],key=abs)
[5, -7, 23, 43, 122]

key指定的函数作用于list上的每一个元素,并根据key函数的返回值进行排序。对比原始的list和经过key=abd 处理过的list:

list=[23,43,5,-7,122]
key=[23,43,5,7,122]

然后sorted()函数按照key进行排序,并按照对应关系返回list相应的元素。

我们再来看一个字符串排序的例子:

1
2
>>> sorted(['bob','james','curry','Kobe'])
['Kobe', 'bob', 'curry', 'james']

默认情况下,对字符串的排序,是按照ASCII的大小比较的,由于'Z'<'a',所以大写字母Z是排在小写字母a前面的。

当我们提出排列时忽略大小写,按照字母排序,要实现这个算法,不必对现有代码加大改动,只要我们能用一个key函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(小写)再比较

这样我们给sorted()传入key函数,即可忽略大小写的排序:

1
2
>>> sorted(['bob','james','curry','Kobe'],key=str.lower)
['bob', 'curry', 'james', 'Kobe']

要进行反向排序,不必改动key函数,可以传入第三个参数reverse=True:

1
2
>>> sorted(['bob','james','curry','Kobe'],key=str.lower,reverse=True)
['Kobe', 'james', 'curry', 'bob']

sorted()也是一个高阶函数。用sorted()排序的关键是实现一个映射函数。

返回函数

函数作为返回值

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果返回。

我们来实现一个可变参数的求和。通常情况下,求和函数式这样定义的:

1
2
3
4
5
6
>>> def cal_sum(*args):
... ax=0
... for x in args:
... ax=ax+n
... return ax
...

但是,如果不需要立即求和,而是在后面的代码中根据需要再进行计算怎么办?可以不返回求和的结果,而是返回求和的函数:

1
2
3
4
5
6
7
8
>>> def lazy_sum(*args):
... def sum():
... ax=0
... for n in args:
... ax=ax+n
... return ax
... return sum
...

当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数,调用函数f时,才真正计算求和的结果:

1
2
3
4
5
>>> f=lazy_sum(1,2,3,4,5,6)
>>> f
<function lazy_sum.<locals>.sum at 0x000001BA761FF378>
>>> f()
21

在这个例子中,我们在函数lazy_sum中又定义了sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazy_sum返回函数sum时,相关参数和变量都保存在返回的函数中,这种程序结构称为‘闭包’(Closure)。

请再注意一点,当我们调用lazy_sum时,每次调用都会返回一个新的函数,即使传入相同的参数。

闭包

注意到返回的函数在其内部引用了局部变量args,所以,当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,所以,闭包用起来简单,实现起来不容易。另一个需要注意的问题是,返回的函数并没有立刻被执行,而是在调用了f()之后才执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> def count():
... fs=[]
... for i in range(1,4):
... def f():
... return i*i
... fs.append(f)
... return fs
...
>>> f1,f2,f3=count()
>>> f1
<function count.<locals>.f at 0x000001BA761FF730>
>>> f1()
9
>>> f2()
9
>>> f3()
9

在上面的例子中,每次循环都创建了一个新的函数,然后,把创建的三个函数都返回了。调用三个函数结果不是1,4,9而都是9。原因在于返回的函数引用了局部变量i,但是它不是立刻执行,等到三个函数都返回时,引用的变量i变为3,因此最终的结果为9

返回闭包时牢记的一点是,返回函数不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用循环变量时,可以新建一个函数,用该函数的参数绑定循环变量的当前值,无论该循环变量后续如何更改,已经绑定到函数参数的值不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> def count():
... def f(j):
... def g():
... return j*j
... return g
... fs=[]
... for i in range(1,4):
... fs.append(f(i))
... return fs
...
>>> f1,f2,f3=count()
>>> f1()
1
>>> f2()
4
>>> f3()
9

小结
一个函数可以返回一个计算结果也可以返回一个函数。返回一个函数时,牢记该函数并未执行,返回函数中不要引用任何可能会变化的变量。

匿名函数

当我们传入函数时,有些时候,不需要显示的定义函数,直接传入匿名函数更方便。在Python中,对匿名函数提供了优先支持。以map()函数为例,计算f(x)=x^2时,除了定义一个函数外,还可以传入匿名函数:

1
2
>>> list(map(lambda x:x*x,[1,2,3,4,5,6,7,8,9]))
[1, 4, 9, 16, 25, 36, 49, 64, 81]

通过对比可以看出,匿名函数lambda x:x*x实际上就是:

1
2
>>> def f(x):
... return x*x

关键字lambda表示匿名函数,冒号前面的x表示函数参数。

匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。用匿名函数有个好处,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

1
2
3
4
5
>>> f=lambda x:x*x+x
>>> f
<function <lambda> at 0x00000230CEB4F510>
>>> f(16)
272

同样,也可以把匿名函数作为返回值返回:

1
2
>>> def build(x,y):
... return lambda:x*x+y*y

装饰器

由于函数也是一个对象,而且函数对象可以被赋值变量,所以,通过变量能调用该函数。

1
2
3
4
5
6
>>> def now():
... print('2016-11-23')
...
>>> f=now
>>> f()
2016-11-23

函数对象有一个__name__属性,可以拿到函数的名字:

1
2
3
4
>>> now.__name__
'now'
>>> f.__name__
'now'

现在,假定我们要增强now()的功能,比如,在函数调用前后自动打印日志,但又不希望修改now()的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)

本质上,decorator就是一个返回函数的高阶函数。所以我们要定义一个能打印日志的decorator如下:

1
2
3
4
5
6
>>> def log(func):
... def wrapper(*args,**kw):
... print('call %s():' %func.__name__)
... return func(*args,**kw)
... return wrapper
...

观察上面的log,因为他是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们借助Python的@语法,把decorator置于函数的定义处,调用now函数,不仅会运行now函数本身,还会在运行该函数前打印一行日志:

1
2
3
4
5
6
7
>>> @log
... def now():
... print('2016-04-09')
...
>>> now()
call now():
2016-04-09

把@log放到now()函数的定义处,相当于执行了语句:

now=log(now)

由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了信道函数,于是调用now()将执行新函数,即在log()函数中返回的wrapper()函数。

wrapper()函数的参数定义是(*args,**kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来很复杂,比如要自定义log的文本:

1
2
3
4
5
6
7
8
>>> def log(text):
... def decorator(func):
... def wrapper(*args,**kw):
... print('%s %s():' % (text, func.__name__))
... return func(*args,**kw)
... return wrapper
... return decorator
...

这个三层嵌套的decorator用法如下:

1
2
3
4
5
6
7
>>> @log('execute')
... def now():
... print('2015-3-25')
...
>>> now()
execute now():
2015-3-25

和两层嵌套的decorator相比,3层嵌套的decorator的效果是这样的:

now=log(‘execute’)(now)

上面的语句中,首先执行log('execute'),返回的是decorator函数,再调用返回的函数,参数是now的函数,返回值最终是wrapper的函数。以上两种decorator的定义都没有问题,但还差最后一步。我们讲了函数也是对象,有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__属性已经从原来的now变为了wrapper:

1
2
>>> now.__name__
'wrapper'

因为返回的那个wrapper()函数的名字就是wrapper,所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写wrapper.__name__=func.__name__这样的代码,Python内置的functools.wraps就是干这个事情的,所以一个完整的decorator如下:

1
2
3
4
5
6
7
8
>>> import functools
>>> def log(func):
... @functools.wraps(func)
... def wrapper(*args,**kw):
... print('call %s():' % func.__name__)
... return func(*args,**kw)
... return wrapper
...

或者针对带参数的decorator:

1
2
3
4
5
6
7
8
9
10
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
print('%s %s():' % (text, func.__name__))
return func(*args, **kw)
return wrapper
return decorator

小结
在面对对象的设计模块中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。

decorator可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。

偏函数

Python的fucntools模块提供了很多有用的功能,其中一个就是偏函数(Partial function)。要注意,这里的偏函数和数学上的偏函数不一样。在介绍函数参数的时候,我们讲到,通过设定参数的默认值,可以降低函数调用的难度。而偏函数也可以做到这一点。

如int()函数可以把字符串装换为整数,当仅传入字符串时,int()函数默认按照十进制装换:

1
2
>>> int('12354')
12354

但int()函数还提供额外的base参数,默认值为10,如果传入base参数,就可以做N进制装换:

1
2
3
4
>>> int('12354',base=16)
74580
>>> int('12354',base=12)
24688

假设要装换大量二进制字符,每次都传入int(x,base=2)非常麻烦,我们可以定义一个int2()函数,默认把base=2传进去:

1
2
3
4
5
6
7
>>> def int2(x,base=2):
... return int(x,base)
...
>>> int2('111001')
57
>>> int2('111001100101010')
29482

functools.partial就是帮我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:

1
2
3
4
>>> import functools
>>> int2=functools.partial(int, base=2)
>>> int2('111001')
57

所以简单总结functllos.partial的作用就是,把一个函数的某些参数给固定住,返回一个新的函数,调用这个新函数会更简单。注意到上面的int2函数,仅仅是把base参数重新设定位2,但可以在函数调用时传入其他值:

1
2
>>> int2('1111111100',base=4)
349520

最后,创建偏函数时,实际上可以接收函数对象,args,*kw三个参数,当传入int2=functools.partial(int, base=2)实际上固定了int()函数的关键字参数base,也就是int2('10010')相当于:

kw={‘base’ : 2}
int(‘10010’,**kw)

当传入max2 = functools.partial(max, 10)实际上会把10作为*args的一部分自动加到左边,也就是max2(5,6,7)相当于:

args=(10,5,6,7)
max(*args)

4. 模块

发表于 2019-04-21 | 分类于 Python

模块

在计算机程序的开发过程中,随着程序代码越来越多,在一个文件里代码就会越来越长,越来越不容易维护。

为了编写可维护的代码,我们可以把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就越来越少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py文件就称之为一个模块。

使用模块的好处在哪里?

最大的好处是大大提高了代码的可维护性,其次,编写代码不用从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Python内置的模块和来自第三方的模块。

使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们在编写模块时,不必考虑名字会和其他模块冲突。但是也要注意,尽量不要与内置函数名字冲突。Python的内置函数在这里: [https://docs.python.org/3/library/functions.html]

同时,为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。

例如,一个abc.py的文件就是一个名字叫abc的模块,一个xyz.py的文件就是一个名字叫xyz的模块。现在我们假设abc和xyz这两个模块名字与其他模块冲突了,于是我们通过包来组织模块,避免冲突。方法是选择一个顶层包名,比如mycompany,按照如下目录来存放:
Alt text
引入了包之后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。现在,abc.py模块的名字就变为了mycompany.abc。请注意,在每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当做普通目录,而不是一个包。__init__.py可以是空文件也可以有Python代码,因为其本身就是一个模块,其模块名为mycompany。

类似的,可以有多级目录,组成多级层次的包结构,比如:
Alt text
文件www.py的模块名就是mycompany.web.www,两个文件utils.py的模块名分比为mycompany.utils和mycompany.web.utils。

自己创建模块时要注意命名,不能喝Python自带的模块名称冲突。例如,系统自带了sys模块,自己模块就不可命名为sys.py,否则就无法导入系统自带的sys模块。mycompany.web也是一个模块,对应的文件为__init__.py。


使用模块

Python本身就内置了很多非常有用的模块,只要安装完毕,这些模块就可以立即使用。

我们以内建的sys模块为例,编写一个hello的模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
#-*- coding: utf-8 -*-
'a test module'
__author__='cdx'
import sys
def test():
args=sys.argv
if len(args)==1:
print('Hello,world!')
elif len(args==2):
print('Hello,%s!' %args[1])
else:
print('too many arguments!')
if __name__=='__main__':
test()

第一行和第二行是标准注释,第一行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第二行注释表示.py文件本身使用标准的UTF-8编码。
第四行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;第六行使用__author__变量把作者写进去,开源后可以查看作者姓名。签名都是一些标准写法,虽然可以不写,但是按照标准是没有任何问题的。

使用sys的第一步是导入该模块:

import sys

导入该模块后,我们就有了变量sys指向该模块,利用sys这个变量,就可以访问sys模块的所有功能。sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:

运行python hello.py获得的sys.argv就是['hello.py','cdx']。

最后注意到这两行代码:

if name==’main‘:
test()

当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。

我们可以通过命令行运行hello.py看看效果:

1
2
D:\笔记\Python\Notepad++>python hello.py
Hello,world!

如果进入Python交互换将,再导入hello模块:

1
2
3
4
5
Python 3.5.0 (v3.5.0:374f501f4567, Sep 13 2015, 02:27:37) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
>>> hello.test()
Hello,world!

导入时,没有打印Hello,world!,因为没有执行test()函数,调用Hello,world!时就会打印了。

作用域

在一个模块中,我们会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过前缀_来实现的。

正常函数和变量名是公开的public,可以被直接引用,比如:abc,x123,PI等。

类似__xxx__这样的变量是特殊变量,可以别直接引用,但是有特殊用途,比如上面的__author__,__name__就是特殊变量,hello模块定义的文档注释也可以用特殊变量__doc__来访问,我们自己的变量一般不要用这种变量名;

类似_xxx和__xxx这种函数或变量就是非公开的private,不应该被直接引用,比如_abc``__abc_等。

之所以说,private函数和变量不应该被直接引用,而不是不能被直接引用,是因为Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。

private变量或函数的作用示例:

1
2
3
4
5
6
7
8
9
10
11
def _private_1(name):
return 'Hello, %s' % name
def _private_2(name):
return 'Hi, %s' % name
def greeting(name):
if len(name) > 3:
return _private_1(name)
else:
return _private_2(name)

我们在模块里公开greeting()函数,而把内部逻辑用private函数隐藏起来了,这样,调用greeting()函数不关心内部的private细节,这也是一种非常有用的代码封装和抽象方法,即:

外部不需要引用的函数全部定义为private,只有外部需要引用的函数才定义为public。

安装第三方模块

在Python中,安装第三方模块,是通过管理工具pip完成的。

现在我们来安装一个第三方库—-Python Imaging Library,这是Python下非常强大的处理图像的工具库。一般来说,第三方库都会在Python官方的pypi.python.org网站注册,要安装一个第三方库,必须知道该库的名称,可以在官网或者pipy上搜索,比如Pillow的名称叫pillow,因此,安装Pillow的命令就是:

1
pip install Pillow

安装了Pillow之后,就可以处理图片了,随便找一个图片生成缩略图:

1
2
3
4
5
6
>>> from PIL import Image
>>> im=Image.open(r'desktop\test.png')
>>> print(im.format, im.size, im.mode)
PNG (1841, 959) RGBA
>>> im.thumbnail((200,100))
>>>im.save(r'desktop\thumb.jpg','JPEG')

其他常用的第三方库还有MySQL的驱动,mysql-connector-python,用于科学计算的NumPy库numpy,用于生成文本的模板工具Jinja2等。

模块搜索路径

当我们试图加载一个模块时,Python会在指定的路径下搜索对应的.py文件,如果找不到,就会报错;默认情况下,Python解释器会搜索当前目录,所有已安装的内置模块和第三方模块,搜索路径存放在sys模块中的path变量中:

1
2
3
>>> import sys
>>> sys.path
['', 'C:\\Users\\cdxu0\\AppData\\Local\\Programs\\Python\\Python35\\python35.zip', 'C:\\Users\\cdxu0\\AppData\\Local\\Programs\\Python\\Python35\\DLLs', 'C:\\Users\\cdxu0\\AppData\\Local\\Programs\\Python\\Python35\\lib', 'C:\\Users\\cdxu0\\AppData\\Local\\Programs\\Python\\Python35', 'C:\\Users\\cdxu0\\AppData\\Local\\Programs\\Python\\Python35\\lib\\site-packages']

如果要添加自己的搜索目录,有两种方法:

1.直接修改sys.path,添加要搜索的目录
2.设置环境变量PYTHONPATH,该环境变量的内容会被自动添加到模块搜索路径中,设置方法和设置path 环境变量类似。

1…91011
cdx

cdx

Be a better man!

110 日志
36 分类
31 标签
GitHub E-Mail
© 2020 cdx
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.2