用Python写算法 | 蓄水池算法实现随机抽样

现在有一组数,不知道这组数的总量有多少,请描述一种算法能够在这组数据中随机抽取k个数,使得每个数被取出来的概率相等。

如果这组数有n个,那么每个数字取到的概率就是k/n,但是这个问题的难点在于不知道这组数的总数,也就是不知道n,那么该怎么计算每个数取到的概率呢?

蓄水池算法

游泳池(蓄水池)大家都不陌生,有些游泳池中的水是活的,有入水管也有出水管,那么和泳池体积相当的水流过之后,是不是泳池中所有的水都会被替换呢?当然不是,有的水在泳池中可能会存留很久,有的可能刚进去就流走了。仿照这种现象,蓄水池抽样算法诞生了,蓄水池算法的关键在于保证流入蓄水池的水和已经在池中的水以相同的概率留存在蓄水池中。并且蓄水池算法可以在不预先知道总量的情况下,在时间复杂度O(N)的情况下,来解决这类采样问题。

核心原理

这一部分涉及公式,为了保证效果直接贴了图过来。

Python实现

接下来尝试用Python实现一下蓄水池算法,由于蓄水池算法是在事先不知道总量的情况下抽样的,所以定义一个方法来接收单个元素,并且把这个方法放在类中,以持有采样后的数据。

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


class ReservoirSample(object):

def __init__(self, size):
self._size = size
self._counter = 0
self._sample = []

def feed(self, item):
self._counter += 1
# 第i个元素(i <= k),直接进入池中
if len(self._sample) < self._size:
self._sample.append(item)
return self._sample
# 第i个元素(i > k),以k / i的概率进入池中
rand_int = random.randint(1, self._counter)
if rand_int <= self._size:
self._sample[rand_int - 1] = item
return self._sample

测试代码

接下来实现一个测试用例验证实现的算法是否正确,既然是随机抽样,无法通过单词测试来验证是否正确,所以通过多次执行的方式来验证,比如从1-10里随机取样3个数,然后执行10000次取样,如果算法正确,最后结果中1-10被取样的次数应该是相同的,都是3000上下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import unittest
from collections import Counter

from reservoir_sample import ReservoirSample


class TestMain(unittest.TestCase):

def test_reservoir_sample(self):
samples = []
for i in range(10000):
sample = []
rs = ReservoirSample(3)
for item in range(1, 11):
sample = rs.feed(item)
samples.extend(sample)
r = Counter(samples)
print(r)

if __name__ == '__main__':
unittest.main()

输出的结果如下

1
Counter({7: 3084, 6: 3042, 10: 3033, 3: 3020, 8: 3016, 5: 2997, 4: 2986, 2: 2972, 9: 2932, 1: 2918})

上面输出了每个数字被取样到的次数,通过图表可以清晰的看到分布情况

可以看出蓄水池算法对于随机抽样还是非常适合的,每个元素的抽样概率都相同。

代码

上述的算法和测试代码已经放在Github,可以直接下载使用。

关注公众号【Python私房菜】

使用gofmt格式化代码

对于一门编程语言来说,代码格式化是最容易引起争议的一个问题,不同的开发者可能会有不同的编码风格和习惯,但是如果所有开发者都能使用同一种格式来编写代码,开发者就可以将宝贵的时间专注在语言要解决的问题上。

gofmt介绍

Golang的开发团队制定了统一的官方代码风格,并且推出了gofmt工具(gofmt或go fmt)来帮助开发者格式化他们的代码到统一的风格。gofmt是一个cli程序,会优先读取标准输入,如果传入了文件路径的话,会格式化这个文件,如果传入一个目录,会格式化目录中所有.go文件,如果不传参数,会格式化当前目录下的所有.go文件。

gofmt默认不对代码进行简化,使用-s参数可以开启简化代码功能,具体来说会进行如下的转换:

  • 去除数组、切片、Map初始化时不必要的类型声明:
1
2
3
4
如下形式的切片表达式:
    []T{T{}, T{}}
将被简化为:
    []T{{}, {}}
  • 去除数组切片操作时不必要的索引指定
1
2
3
4
如下形式的切片表达式:
    s[a:len(s)]
将被简化为:
    s[a:]
  • 去除迭代时非必要的变量赋值
1
2
3
4
5
6
7
8
如下形式的迭代:
    for x, _ = range v {...}
将被简化为:
    for x = range v {...}
如下形式的迭代:
    for _ = range v {...}
将被简化为:
    for range v {...}

gofmt命令参数列表如下:

1
2
3
4
5
6
7
8
9
10
usage: gofmt [flags] [path ...]
  -cpuprofile string
        write cpu profile to this file
  -d    display diffs instead of rewriting files
  -e    report all errors (not just the first 10 on different lines)
  -l    list files whose formatting differs from gofmt's
  -r string
        rewrite rule (e.g., 'a[b:len(a)] -> a[b:]')
  -s    simplify code
  -w    write result to (source) file instead of stdout

可以看到,gofmt命令还支持自定义的重写规则,使用-r参数,按照pattern -> replacement的格式传入规则。

有如下内容的Golang程序,存储在main.go文件中。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
   a := 1
   b := 2
   c := a + b
   fmt.Println(c)
}

用以下规则来格式化上面的代码。

1
gofmt -r "a + b -> b + a"

格式化的结果如下。

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
   a := 1
   b := 2
   c := b + a
   fmt.Println(c)
}

*注意:Gofmt使用tab来表示缩进,并且对行宽度无限制,如果手动对代码进行了换行,gofmt也不会强制把代码格式化回一行。

go fmt和gofmt

gofmt是一个独立的cli程序,而go中还有一个go fmt命令,go fmt命令是gofmt的简单封装。

1
2
3
4
5
6
7
8
9
10
11
usage: go fmt [-n] [-x] [packages]

Fmt runs the command 'gofmt -l -w' on the packages named
by the import paths. It prints the names of the files that are modified.
For more about gofmt, see 'go doc cmd/gofmt'.
For more about specifying packages, see 'go help packages'.
The -n flag prints commands that would be executed.
The -x flag prints commands as they are executed.
To run gofmt with specific options, run gofmt itself.

See also: go fix, go vet.

go fmt命令本身只有两个可选参数-n和-x,-n仅打印出内部要执行的go fmt的命令,-x命令既打印出go fmt命令又执行它,如果需要更细化的配置,需要直接执行gofmt命令。

go fmt在调用gofmt时添加了-l -w参数,相当于执行了gofmt -l -w

goland中配置gofmt

Goland是JetBrains公司推出的Go语言IDE,是一款功能强大,使用便捷的产品。

在Goland中,可以通过添加一个File Watcher来在文件发生变化的时候调用gofmt进行代码格式化,具体方法是,点击Preferences -> Tools -> File Watchers,点加号添加一个go fmt模版,Goland中预置的go fmt模版使用的是go fmt命令,将其替换为gofmt,然后在参数中增加-l -w -s参数,启用代码简化功能。添加配置后,保存源码时,goland就会执行代码格式化了。

参考文章

https://golang.org/cmd/gofmt/

https://golang.org/doc/effective_go.html

https://openhome.cc/Gossip/Go/gofmt.html

https://github.com/hyper0x/go_command_tutorial/blob/master/0.9.md

点击关注知乎专栏Golang私房菜

搭建Kubernetes集群时DNS无法解析问题的处理过程

问题描述

在搭建Kubernetes集群过程中,安装了kube-dns插件后,运行一个ubuntu容器,发现容器内无法解析集群外域名,一开始可以解析集群内域名,一段时间后也无法解析集群内域名。

1
2
3
4
5
$ nslookup kubernetes.default
Server: 10.99.0.2
Address 1: 10.99.0.2 kube-dns.kube-system.svc.cluster.local

nslookup: can't resolve 'kubernetes.default'

排查过程

在排查问题前,先思考一下Kubernetes集群中的DNS解析过程,在安装好kube-dns的集群中,普通Pod的dnsPolicy属性是默认值ClusterFirst,也就是会指向集群内部的DNS服务器,kube-dns负责解析集群内部的域名,kube-dns Pod的dnsPolicy值是Default,意思是从所在Node继承DNS服务器,对于无法解析的外部域名,kube-dns会继续向集群外部的dns进行查询,过程如图。

Ubuntu容器是一个普通的Pod,在Linux系统中,/etc/resolv.conf是存储DNS服务器的文件,普通Pod的/etc/resolv.conf文件应该存储的是kube-dns的Service IP。

1
2
3
nameserver 10.99.0.2  # 这里存储的是kube-dns的Service IP
search default.svc.cluster.local. svc.cluster.local. cluster.local.
options ndots:5

查看后发现/etc/resolv.conf文件中存储的是kube-dns的Service IP,证明这一步没有问题,接下来查看一下kube-dns的Pod,先进入kube-dns的Pod中检查一下/etc/resolv.conf文件,这里存储的应该是集群外部的DNS服务器地址,查看后发现,这里存储的地址是127.0.0.53,进一步查看kube-dns Pod的log,发现出现了非常多的i/o timeout错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:38019->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:57567->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:52599->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:42539->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:46885->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:44189->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:56505->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:47320->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:42464->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:49203->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:58103->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:47148->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:36883->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:40968->127.0.0.53:53: i/o timeout
2018/07/11 07:12:47 [ERROR] 2 [www.baidu.com](http://www.baidu.com/). A: unreachable backend: read udp 127.0.0.1:55672->127.0.0.53:53: i/o timeout

现在基本上可以发现问题的原因了,kube-dns只能解析集群内部地址,而集群外部地址应该发给外部DNS服务器进行解析,由于kube-dns Pod中的/etc/resolv.conf文件存储的DNS服务器地址是127.0.0.53,127...*都是回环地址,也就是集群外域名的DNS解析请求会再次发送回kube-dns,导致形成一个循环,这也是一秒钟会出现几十次i/o timeout日志的原因,请求会不断的在kube-dns中循环,kube-dns就像一个黑洞一样,吃掉了所有dns解析请求,不断累积的请求最终会导致整个集群的网络出现卡顿。

为什么

虽然问题的原因找到了,但是为什么kube-dns Pod中/etc/resolv.conf文件存储的DNS服务器是127.0.0.53?

kube-dns Pod的dnsPolicy值是Default,查看一下Kubernetes文档。

Default“: The Pod inherits the name resolution configuration from the node that the pods run on. See related discussion for more details.

所以kube-dns的/etc/resolv.conf文件是从Node中继承来的,查看Node中的/etc/resolv.conf文件,存储的DNS服务器地址确实是127.0.0.53,那么下一个问题出现了,在Node中发送DNS解析请求为什么不会产生回环的问题呢?

Node使用的是Ubuntu 18.04 Server,在这个版本的系统中,DNS解析请求并不是直接发给所在网络的DNS服务器的,Ubuntu 18.04中有一个systemd-resolved服务,为本地应用程序提供了DNS解析服务,例如nslookup localhost,解析程序从/etc/resolv.conf文件中找到DNS服务器127.0.0.53,发送解析请求,systemd-resolved会监听在53端口上,捕获到解析请求后,如果是自己可以解析的,例如localhost,会直接返回127.0.0.1,如果不能解析,才会发送给外部服务器,而外部服务器的地址存储在/run/systemd/resolve/resolv.conf文件中,这个文件是systemd-resolved服务器的配置文件,过程如图。

怎么破

理解了问题的来龙去脉,解决问题的办法也就应运而生。在Kubernetes集群中,kubelet是worker组建,负责管理Pod,根据kubernetes文档,kubelet默认会从Node的/etc/resolv.conf文件读取DNS服务器地址,使得dnsPolicy是Default的Pod得以继承,kubelet中的–resolv-conf参数可以指定这个配置文件的地址。在Ubuntu 18.04中,将这个参数设置为systemd-resolved的DNS服务器配置文件/run/systemd/resolve/resolv.conf,Pod就会继承真正的外部DNS服务器。

总结

通过对问题的探究,也理解了Kubernetes集群中DNS解析的完整过程,如图。

\ 在Ubuntu 16.04中也是类似的逻辑,只不过systemd-resolved换成了dnsmasq,监听地址是127.0.1.1 * 在具体实践过程中,也顺便探究了CoreDNS和KubeDNS架构和解析逻辑上的区别,不过不在此问题的讨论范围,有兴趣的朋友可以自己看一下。 * 如果Kubernetes集群是安装在NAT网络下的虚拟机上,虚拟机(也就是Kubernetes集群中的Node)中/etc/resolv.conf文件可能被修改为NAT的地址,也就不会出现上面这个问题。*

参考内容

https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/
https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/
https://kubernetes.io/docs/tasks/administer-cluster/dns-custom-nameservers/
https://www.freedesktop.org/software/systemd/man/systemd-resolved.service.html
https://github.com/kubernetes/kubernetes/issues/49411
https://github.com/kubernetes/kubernetes/issues/45828

Golang环境安装和依赖管理

2015年,Go 1.5加入了一个试验性的vendor机制(到2016年的Go 1.6版变为默认开启),vendor机制就是在项目中加入了vendor文件夹,用于存放依赖,这样就可以将不同项目的依赖隔离开。

Golang一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Golang提供了方便的安装包,支持Windows、Linux、Mac系统。

下载安装包

Golang的官网是https://golang.org/,如果官网打不开,可以访问https://golang.google.cn/这个域名。在官网点击Download Go会进入下载页,可以看到这里提供了针对各个系统的安装包,也提供了源码,可以下载源码编译安装。

下载运行安装包后,在terminal中执行go env命令,如果出现下面的输出说明已经安装成功。

GOROOT与GOPATH

仔细看上面的输出,会发现其中有一个GOPATH,又有一个GOROOT,那么到底哪个才是Golang的运行环境呢。

首先访问一下GOROOT这个路径,会发现其中包含bin、lib等文件夹。GOROOT就是Golang的安装路径,其中包含Golang编译、工具、标准库等,在安装后就会存在。

和GOROOT不同,GOPATH是工作空间路径,从go 1.8开始,如果GOPATH没有被设置,会有一个默认值,在Unix上为$HOME/go,在Windows上为%USERPROFILE%/go,当调用go build时,它会在GOPATH中寻找源码。访问一下GOPATH这个路径,会发现其中只有pkg、bin、src三个文件夹,并且里面基本是空的,这是一个约定的目录结构,src文件夹用来存放源码、pkg存放编译后生成的文件,bin存放编译后生成的可执行文件。项目代码需要在GOPATH/src路径下。

GOPATH路径下出了存放项目代码,还存放所有通过go get安装的依赖,项目代码和依赖代码是平级的,当各个项目都有很多依赖的时候,这个GOPATH路径下的代码量会多的吓人,并且难以拆分。

Vendor

当使用go run或者go build命令时,会首先从当前路径下的vendor文件夹中查找依赖,如果vendor不存在,才会从GOPATH中查找依赖。

然而我们安装依赖通常使用go get或者go install命令,这两个命令依旧会把依赖安装到GOPATH路径下。

包管理工具dep

Vendor只是go官方提供的一个机制,但是包管理的问题依然没有解决,并且也没有对依赖进行版本管理。如果要实现上述的功能,还需要借助包管理工具。

Go官方给出了包管理工具的对比:https://github.com/golang/go/wiki/PackageManagementTools

dep是官方的试验性包管理工具,可以通过如下脚本安装

curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh

安装完成后,进入项目路径,执行

1
dep init

项目中会出现两个文件一个目录

1
2
3
Gopkg.toml
Gopkg.lock
vendor

dep包管理的流程如图

  • solving功能,它将当前项目中的导入包和Gopkg.toml中的规则作为输入,不可变的依赖关系图作为传递完成后的输出,形成Gopkg.lock。

  • vendor功能,将Gopkg.lock中的信息作为输入,确保项目编译时能使用在Gopkg.lock文件中锁定的版本。

使用如下命令添加依赖

1
dep ensure -add [github.com/gin-gonic/gin](http://github.com/gin-gonic/gin)

使用如下命令更新Gopkg.lock

1
dep ensure -update

欢迎关注知乎专栏【Golang私房菜】

你所不知道的Python | 函数参数的演进之路

函数参数处理机制是Python中一个非常重要的知识点,随着Python的演进,参数处理机制的灵活性和丰富性也在不断增加,使得我们不仅可以写出简化的代码,也能处理复杂的调用。

关键字参数

调用时指定参数的名称,且与函数声明时的参数名称一致。

关键字参数是Python函数中最基础也最常见的,我们写一个记账的函数,参数是需要记录的时间和金额。

1
2
def add_record(date, amount):
print('date:', date, 'amount:', amount)

这里的amount参数就是一个关键字参数,关键字参数支持两种调用方式:

  • 位置调用
  • 关键字调用

位置调用,就是按参数的位置进行调用,例如传入两个参数,第一个是字符串2018-07-06,第二个是整数10,那么这两个参数会被分别赋予date和amount变量,如果顺序反过来,则这两个参数分别赋予amount和date变量。

1
2
add_record('2018-07-06', 10)  # 输出date: 2018-07-06 amount: 10
add_record(10, '2018-07-06') # 输出date: 10 amount: 2018-07-06

关键字调用,可以忽略参数顺序,直接指定参数。

1
add_record(amount=10, date='2018-07-06')  # 虽然参数顺序反了,但是使用了关键字调用,所以依然输出date: 2018-07-06 amount: 10

仅限关键字参数

我们定义一个Person类,并实现它的__init__方法

1
2
3
4
5
6
7
class Person(object):
def __init__(self, name, age, gender, height, weight):
self._name = name
self._age = age
self._gender = gender
self._height = height
self._weight = weight

当初始化这个类的时候,我们可以使用关键字调用,也可以使用位置调用。

1
2
Person('Wendy', 24, 'female', 160, 48)
Person('John', age=27, gender='male', height=170, weight=52)

对比上面两种方式,我们会发现参数多的时候通过关键字指定参数不仅更加清晰,也更具有可读性。如果我们希望函数只允许关键字调用,该如何做呢?Python 3.0中,引入了一种新的仅限关键字参数,能实现我们的需求。

下面将age以后的参数修改为只允许关键字调用,定义函数时想指定仅限关键字参数,要把它们放到前面有星号的参数后面,在Python中有星号的参数是可变参数的意思,如果不想支持可变参数,可以在参数中放一个星号作为分割。

1
2
3
4
5
6
7
8
9
10
11
class Person(object):
# 参数中的星号作为关键字参数和仅限关键字参数的分割
def __init__(self, name, *, age='22', gender='female', height=160, weight=50):
self._name = name
self._age = age
self._gender = gender
self._height = height
self._weight = weight

Person('Wendy', 24, 'female', 160, 48) # 报错,age以后参数不允许位置调用
Person('John', age=27, gender='male', height=170, weight=52) # 正常执行

普通参数和仅限关键字参数中间由一个星号隔离开,星号以后的都是仅限关键字参数,只可以通过关键字指定,而不能通过位置指定。

参数默认值

在函数声明时,指定参数默认值,调用时不传入参数则使用默认值,相当于可选参数。

1
2
3
4
def add_record(date, amount=0):
print('date:', date, 'amount:', amount)

add_record('2018-07-06') # 输出date: 2018-07-06 amount: 0

上面代码中没有传入amount参数,所以amount直接被置为默认值0。有一点需要注意的是,默认参数需要设置在必选参数后面,并且默认参数既可以通过位置调用,也可以通过关键字调用。

1
2
3
add_record('2018-07-06', 10)  # 通过位置指定参数
add_record('2018-07-06', amount=10) # 通过位置指定参数
add_record(amount=10, '2018-07-06') # 报错,默认参数必须在必选参数后面

参数默认值既支持关键字参数,也支持仅限关键字参数。

可变长参数

“可变长”顾名思义是允许在调用时传入多个参数,可变长参数适用于参数数量不确定的场景,可变参数有两种,一种是关键字可变长参数,另一种是非关键字可变长参数。

非关键字可变长参数的写法是在参数名前加一个星号,Python会将这些多出来的参数的值放入一个元组中,由于元组中只有参数值而没有参数名称,所以是关键字参数。

1
2
3
4
5
6
7
8
def print_args(*args):
print(args)

print_args(1, 2, 3, 4, 5) # 输出元组(1, 2, 3, 4, 5)

a = [1, 2, 3, 4, 5]
print_args(a) # 直接传入时,列表a会被当作一个元素,所以输出([1, 2, 3, 4, 5],)
print_args(*a) # 在传参时加星号可以将可迭代参数解包,所以列表a中每一个元素都被当作一个参数传入,输出(1, 2, 3, 4, 5)

关键字可变长参数的写法是在参数名前加两个星号,Python会将这些多出来的参数的值放入一个字典中,由于字典中只有参数值而没有参数名称,所以是非关键字参数。

1
2
3
4
5
def print_kwargs(**kw_args):
print(kw_args)

a = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
print_kwargs(**a) # 使用关键字可变参数时, {'a': 1, 'c': 3, 'b': 2, 'e': 5, 'd': 4}

函数注解

Python 3中为函数定义增加的另一个新功能是函数注解,所谓函数注解,就是可以在函数参数和返回值上添加任意的元数据。

1
2
def create_person(name: str, age: int, gender: str = 'female', height: int = 160)  -> bool:
return True

用create_person方法举例,可以看到在每个参数后面都跟了一个参数类型,在函数后面则是返回值类型,函数注解可以用在文档编写、类型检查中,在支持函数注解的IDE中,如果传入参数和返回的类型不符合函数注解中的类型,IDE会提示错误。

但是函数注解只是一个元数据,Python解释器执行时候并不会去检查类型,所以下面这种情况也是合法的。

1
Person(name=123, age='John')  # 并不会报错

总结

Python有着非常好入门的特点,但是随着语言本身的演进,很多高级功能也在持续加入,用好这些功能可以使我们的Python代码拥有更高的可读性,适应更加复杂的应用场景。

扫码关注【Python私房菜】,我会在这里持续发布Python相关原创技术文章

你所不知道的Python | 字符串连接的秘密

字符串连接,就是将2个或以上的字符串合并成一个,看上去连接字符串是一个非常基础的小问题,但是在Python中,我们可以用多种方式实现字符串的连接,稍有不慎就有可能因为选择不当而给程序带来性能损失。

方法1:加号连接

很多语言都支持使用加号连接字符串,Python也不例外,只需要简单的将2个或多个字符串相加就可以完成拼接。

1
2
3
a = 'Python'
b = '私房菜'
r = a + b # 输出'Python私房菜'

方法2:使用%操作符

在Python 2.6以前,%操作符是唯一一种格式化字符串的方法,它也可以用于连接字符串。

1
2
3
a = 'Python'
b = '私房菜'
r = '%s%s' % (a, b) # 输出'Python私房菜'

方法3:使用format方法

format方法是Python 2.6中出现的一种代替%操作符的字符串格式化方法,同样可以用来连接字符串。

1
2
3
a = 'Python'
b = '私房菜'
r = '{}{}'.format(a, b)

方法4:使用f-string

Python 3.6中引入了Formatted String Literals(字面量格式化字符串),简称f-string,f-string是%操作符和format方法的进化版,使用f-string连接字符串的方法和使用%操作符、format方法类似。

1
2
3
a = 'Python'
b = '私房菜'
r = f'{a}{b}'

方法5:使用str.join()方法

字符串有一个内置方法join,其参数是一个序列类型,例如数组或者元组等。

1
2
3
a = 'Python'
b = '私房菜'
r = ''.join([a, b])

对比测试

既然连接字符串有这么多方法,那么使用时到底选择哪种呢?我们从代码可读性和性能两个层面来评估一下上面5种方法。

使用timeit模块,分别执行上述5种方法的示例代码100000次,执行时间如图。

可以看到,%操作符、format和f-string都是字符串格式化方法,性能依次递增,加号连接的性能和f-string不相上下。

有一点需要注意的是,字符串类型是不可变的,所以每一次应用加号连接字符串都会生成一个新的字符串,连接多个字符串时,效率低下就是必然的了,我们将一次连接的字符串提升到10个和20个,再来进行2轮测试,下面是连接20个字符串时的耗时情况。

和连接两个字符串时的结果出现了一些不同,首先使用加号连接的方式在字符串数量较多时(大于10个),性能会急剧下降,str.join()方法在连接大量字符串时性能最好。

总结

连接少量字符串时
使用加号连接符在性能和可读性上都是明智的,如果对可读性有更高的要求,并且使用的Python 3.6以上版本,f-string也是一个非常好的选择,例如下面这种情况,f-string的可读性显然比加号连接好得多。

1
2
a = f'姓名:{name} 年龄:{age} 性别:{gender}'
b = '姓名:' + name + '年龄:' + age + '性别:' + gender

连接大量字符串时
joinf-string都是性能最好的选择,选择时依然取决于你使用的Python版本以及对可读性的要求,f-string在连接大量字符串时可读性并不一定好。切记不要使用加号连接,尤其是在for循环中。

欢迎关注我的公众号【Python私房菜】

Ubuntu 18.04 LTS安装KVM虚拟机

前一阵使用在最新的Ubuntu 18.04上安装了KVM来虚拟一个小的VM集群,将主要过程和其中遇到的一些问题记录下来。

准备工作

首先需要检查一下CPU是否支持虚拟化,执行一下命令来检查/proc/cpuinfo文件中是否又虚拟化相关的字眼,如果有的话表明CPU支持虚拟化技术。

1
egrep -c '(svm|vmx)' /proc/cpuinfo

上面命令执行结果如果返回0,表示CPU不支持虚拟化技术。当然主板BIOS中的虚拟化技术也可能不是默认开启的,如果没有开启需要手动开启一下。

安装KVM

执行以下命令安装KVM

1
2
sudo apt update
sudo apt install qemu qemu-kvm libvirt-bin bridge-utils virt-manager

将libvirtd添加自启动

1
2
sudo systemctl start libvirtd.service
sudo systemctl enable libvirtd.service

网络模式

KVM安装完成后,首先需要进行网络设定,KVM支持四种网络模式:

  • 桥接模式
  • NAT模式
  • 用户网络模式
  • 直接分配设备模式

主要讲一下前两种

桥接(Bridge)模式

在桥接模式下,宿主机和虚拟机共享同一个物理网络设备,虚拟机中的网卡和物理机中的网卡是平行关系,所以虚拟机可以直接接入外部网络,虚拟机和宿主机有平级的IP。

桥接模式

原本宿主机是通过网卡eth0连接外部网络的,网桥模式会新创建一个网桥br0,接管eth0来连接外部网络,然后将宿主机和虚拟机的网卡eth0都绑定到网桥上。

使用桥接模式需要进行以下操作:

编辑/etc/network/interfaces,增加如下内容

1
2
3
4
5
auto br0
iface br0 inet dhcp # 网桥使用DHCP模式,从DHCP服务器获取IP
bridge_ports enp3s0 # 网卡名称,网桥创建前连接外部的网卡,可通过ifconfig命令查看,有IP地址的就是
bridge_stp on # 避免数据链路出现死循环
bridge_fd 0 # 将转发延迟设置为0

接下来需要重启networking服务(如果是通过SSH连接到宿主机上的,这一步会导致网络中断,如果出现问题可能导致连不上宿主机,最好在宿主机上直接操作)

1
systemctl restart networking.service

使用ifconfig命令查看IP是否从enp3s0(网桥创建前的网卡)变到了br0上,如果没有变化则需要重启。如果宿主机ip已经成功变到网桥上,并且宿主机能正常上网而虚拟机获取不到ip,可能是ufw没有允许ip转发导致的,编辑/etc/default/ufw允许ip转发。

1
DEFAULT_FORWARD_POLICY="ACCEPT"

重启ufw服务让设置生效

1
systemctl restart ufw.service

NAT(Network Address Translation)模式

NAT模式是KVM默认的网络模式,KVM会创建一个名为virbr0的虚拟网桥,但是宿主机和虚拟机对于网桥来说不是平等的了,网桥会把虚拟机藏在背后,虚拟机会被分配一个内网IP,而从外网访问不到虚拟机。

NAT模式

安装Linux虚拟机

使用如下命令安装安装Linux虚拟机

1
2
3
4
5
6
7
8
sudo virt-install -n ubuntu_3
--description "ubuntu_3"
--os-type=linux --os-variant=ubuntu17.10 --ram=1024 --vcpus=1
--disk path=/var/lib/libvirt/images/ubuntu_3.img,bus=virtio,size=50 # 磁盘位置,大小50G
--network bridge:br0 # 这里网络选择了桥接模式
--accelerate
--graphics vnc,listen=0.0.0.0,keymap=en-us # VNC监听端口,注意要选择en-us作为key-map,否则键位布局可能会乱
--cdrom /home/zzy/Downloads/ubuntu-18.04-live-server-amd64.iso # 安装ISO路径

安装Windows 10虚拟机

安装Windows 10虚拟机会出现没有virtio驱动的问题,导致安装程序找不到硬盘,需要先下载virtio驱动。

https://fedoraproject.org/wiki/Windows_Virtio_Drivers

创建虚拟机时,将其加入到CD-ROM中

1
2
3
4
5
6
7
8
9
10
sudo virt-install -n win10
--description "win10"
--os-type=win --os-variant=win10
--ram=4096 --vcpus=2
--disk path=/var/lib/libvirt/images/win_10.img,bus=virtio,size=100
--network bridge:br0
--accelerate
--graphics vnc,listen=0.0.0.0,keymap=en-us
--cdrom =/home/zzy/Downloads/cn_windows_10_consumer_editions_version_1803_updated_march_2018_x64_dvd_12063766.iso
--cdrom=/home/zzy/Downloads/virtio-win.iso

使用VNC客户端连接虚拟机

执行以下命令查看虚拟机的列表

1
sudo virus list

通过上一步查处的虚拟机列表,查看单台机器的VNC端口

1
sudo virsh vncdisplay ubuntu_3  # ubuntu_3是虚拟机名称

知道了VNC端口号,就可以使用VNC客户端连接到虚拟机完成安装了。

大毒瘤!卸载WeGame解决XPS 15蓝屏问题

潜伏期

去年4月底买了一台美版XPS 15 9560,用了几个月之后就会偶尔出现蓝屏问题,由于之前就在论坛上看到很多吐槽XPS品控的帖子,以为自己也中了枪,好在蓝屏也不频繁,不影响使用也就没管。

上升期

进入今年5月,升级了Win10 1803后(基本上软件有更新我都会第一时间升级,体验最新的改进),蓝屏的次数开始多了起来,开始变得影响使用,于是开始着手查找问题。

由于心急,并没有看转储文件,而是直接Google了XPS 15的蓝屏问题,希望能尽快找到方案。很快发现具有类似问题的人不在少数,也有很大一部分人以此说XPS 15品控不好,看了很多内容后,把怀疑的方向放在了驱动层面(Dell有一个Dell Update软件,一有更新提示我就会更新驱动),于是从更新频率最高的显卡驱动入手,降级显卡驱动到上一个版本,然而并没有什么用。于是又降级了Wifi驱动,因为Wifi驱动也是最近更新的,并且XPS 15的Killer显卡兼容性似乎没有那么好。

XPS 15的驱动列表,可以看到Wifi和显卡驱动都是最近发布的

降级完驱动之后,电脑进入了短暂的回光返照阶段,用了几个小时都没有蓝屏,然而紧接着更加不幸的问题就要发生了。

爆发期

用QQ接收一个大文件,大概1G,进行到一半的时候忽然蓝屏,重启后重新接收,准备传完之后再解决蓝屏问题,然而不幸的是传了一半又蓝屏了… 这时我把怀疑的方向转向了硬盘,于是降级了Intel RST驱动,心想降级要是没用的话,有可能是SSD跪了… 降级后怀着忐忑的心情再次重试,竟然…又蓝屏了…

这次把怀疑的方向转到了QQ或者Wifi上,毕竟是一用QQ接收大文件就出问题,那么不是QQ就是Wifi了,于是卸载QQ,用迅雷下载一个大文件,果不其然,又跪了…

然而已经降级了Wifi驱动,之前也一直没有什么问题,难道真的是硬件出了问题?这个时候,我内心已经有点放弃PC,转而在京东上看MBP的价格了…

然而MBP高企的售价让我冷静了下来,回来死马当活马医,继续找问题吧。

既然自己怀疑的方向都不对,只能信仰科学了,安装了WinDbg和BlueScreenView这两款软件来查看和分析转储文件(具体过程不再赘述,可以参考文末的相关资源)。

从BlueScreenView里可以看到,机器以近乎疯狂的频率蓝屏

从BlueScreenView里并没有看到太多信息,现在只剩下WinDbg这一根救命稻草了。

不过好在WinDbg非常给力,分析了几个不同时间的转储文件,都提示问题可能是由tdx.sys文件造成的。

tdx是TDI Translation Driver的意思,TDI是传输层驱动接口的意思,那么可以肯定问题来自于网络,这也印证了每次使用网络传输大文件就会蓝屏的现象。紧接着,看到了另外一段话:

WARNING: Unable to verify timestamp for QMTgpNetFlow764.sys

这个QMTgpNetFlow764.sys又是什么,看前缀似乎像第三方的文件,先Google一下。

真相大白

Google了一下这个QMTgpNetFlow764.sys,终于拨云见日。

原来早有人发现了QMTgpNetFlow764.sys引发蓝屏问题的秘密

根据网友们的分析(具体可以查看文末的相关资源),这5个文件来自于腾讯的游戏平台WeGame,于是卸载了腾讯系的游戏,并且重启删除了C:\Windows\system32\drivers目录下以QMT开头的5个文件,没关机使用至今(大约半个月)再也没有出现过蓝屏的情况,至此可以证明,腾讯WeGame平台是引起蓝屏的主要原因。

毒瘤们

相关资源

BlueScreenView下载地址:https://www.nirsoft.net/utils/blue_screen_view.html

WinDbg下载地址:https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/debugger-download-tools

WinDbg使用说明:https://jingyan.baidu.com/article/9f7e7ec0b0aea36f281554df.html

知乎用户Weasley Frank的专栏文章TGP/WeGame驱动导致WSL网络服务异常 对这个问题进行了简单分析

你所不知道的Python | 字符串格式化的演进之路

字符串格式化对于每个语言来说都是一个非常基础和常用的功能,学习Python的同学大概都知道可以用%语法来格式化字符串。然而为了让我们更方便的使用这个常用功能,语言本身也在对字符串格式化方法进行迭代。

Python 2.6以前:%操作符

在Python 2.6出现之前,字符串迭代只有一种方法,就是%(也是取模)操作符,%操作符支持unicode和str类型的Python字符串,效果和C语言中的sprintf()方法相似,下面是一个使用%格式化字符串的例子:

1
print("I'm %s. I'm %d year old" % ('Tom', 27))

%符号前面使用一个字符串作为模板,模板中有标记格式的占位符号,%后面是一个tuple或者dict,用来传递需要格式化的值。占位符控制着显示的格式,下面列表展示了占位符的种类:

占位符 内容
%d 十进制整数
%i 十进制整数
%o 八进制整数
%u 无符号整数
%x 无符号十六进制(小写)
%X 无符号十六进制(大写)
%e 浮点型(科学记数法,小写)
%E 浮点型(科学记数法,大写)
%f 浮点数
%F 浮点数
%g 浮点型,如果小数位数超过4位,使用科学记数法表示(小写)
%G 浮点型,如果小数位数超过4位,使用科学记数法表示(大写)
%c 单个字符
%r 字符串(调用repr()方法生成)
%s 字符串(调用str()方法生成)

除了对数据类型的指定,%操作符还支持更复杂的格式控制:

1
%[数据名称][对齐标志][宽度].[精度]类型
名称 内容
数据名称 数据名称用于字典赋值,如果%符号后面传递的数组就不需要填写了
对齐标志 有+、-、0、‘ ’四种,+表示显示正负数符号,-表示左对齐,空格表示在左侧填充一个空格,0表示用0填充
宽度 表示格式化后的字符串长度,位数不足用0或空格补齐
精度 小数点后的位数
类型 数据类型(参考占位符种类)

例如print(‘%053f’ % ‘12.34’)会输出0012.340

Python 2.6:format函数

到Python2.6时,出现了一种新的字符串格式化方式,str.format()函数,相比于%操作符,format函数使用{}和:代替了%,威力更加强大,在映射关系方面,format函数支持位置映射、关键字映射、对象属性映射、下标映射等多种方式,不仅参数可以不按顺序,也可以不用参数或者一个参数使用多次,下面通过几个例子来说明。

1
2
3
4
5
6
7
8
9
10
11
'{1} {0}'.format('abc', 123# 可以不按顺序进行位置映射,输出'123 abc'

'{} {}'.format('abc', 123# 可以不指定参数名称,输出'abc 123'

'{1} {0} {1}'.format('abc', 123# 参数可以使用多次,输出'123 abc 123'

'{name} {age}'.format(name='tom', age=27# 可以按关键字映射,输出'tom 27'

'{person.name} {person.age}'.format(person=person)  # 可以按对象属性映射,输出'tom 27'

'{0[1]} {0[0]}'.format(lst)  # 通过下标映射

可以看到,format函数比%操作符使用起来更加方便,不需要记住太多各种占位符代表的意义,代码可读性也更高。在复杂格式控制方面,format函数也提供了更加强大的控制方式:

1
[[填充字符]对齐方式][符号标志][#][宽度][,][.精度][类型]

例如:

1
'{:S^+#016,.2f}'.format(1234# 输出'SSS+1,234.00SSSS'

我们以上面的代码为例,通过表格说明一下format格式控制参数:

类型 说明 示例说明
填充字符 不填时默认用空格填充 S表示用S填充
对齐方式 ^表示居中对齐、<表示左对齐、>表示右对齐 ^表示居中对齐,左右位数不足部分会用填充字符填充
符号标志 +表示有符号(正数前显示+,负数前显示-),空格表示整数前加一个空格以和负数对齐 +表示正数前显示空格
# 表示是否在二进制、八进制、十六进制前显示0b、0o、0x等符号 #表示显示进制符号,由于是十进制,所以不显示
宽度 表示输出字符串的宽度 16表示字符串宽度为16,不足部分会补齐
, 表示使用,作为千位分隔符 ,表示使用千位分隔符
精度 表示小数点后数字位数 .2表示精度为2为
类型 s表示字符串类型,c表示字符类型,b\o\d分别表示二八十进制,x\X表示小写和大写十六进制,e\E表示小写和大写的科学记数法,f表示浮点型 f表示浮点型数字

可以看到format函数在%基础上丰富了格式控制种类,并且使输出更容易。

Python 3.6:f-string

不少使用过ES6的小伙伴会知道其中的模板字符串,采用直接在字符串中内嵌变量的方式进行字符串格式化操作,Python在3.6版本中也为我们带来了类似的功能:Formatted String Literals(字面量格式化字符串),简称f-string。

f-string就是以f’’开头的字符串,类似u’’和b’’,字符串内容和format方法中的格式一样,但是可以直接将变量带入到字符串中,可读性进一步增加,例如:

1
2
amount = 1234
f'请转账给我{amount:,.2f}元'  # '请转账给我1,234.00元'

同时,f-string的性能是比%和format都有提升的,我们做一个简单的测试,分别使用%操作符、format和f-string将下面语句执行10000次:

1
2
3
'My name is %s and i'm %s years old.' % (name, age)
'My name is {} and i'm {} years old.'.format(name, age)
f'My name is {name} and i'm {age} years old.'

用时结果如下:

总结

如果你的项目使用的Python版本已经提升到3.6,f-string格式化是首选方式,不仅在保持功能强大的同时语义上更容易理解,而且性能也有较大的提升。如果项目还没有提升到3.6或者使用的2.7,更建议使用format,虽然性能上没有优势,但是语义上还是比%操作符更加便于理解的,功能也更加强大。

欢迎关注我的公众号【Python私房菜】