分类 软件开发 下的文章

Go 是一个内置支持并发编程的语言。借助使用 go 关键字去创建 协程 goroutine (轻量级线程)和在 Go 中提供的 使用 信道其它的并发 同步方法,使得并发编程变得很容易、很灵活和很有趣。

另一方面,Go 并不会阻止一些因 Go 程序员粗心大意或者缺乏经验而造成的并发编程错误。在本文的下面部分将展示一些在 Go 编程中常见的并发编程错误,以帮助 Go 程序员们避免再犯类似的错误。

需要同步的时候没有同步

代码行或许 不是按出现的顺序运行的

在下面的程序中有两个错误。

  • 第一,在 main 协程中读取 b 和在新的 协程 中写入 b 可能导致数据争用。
  • 第二,条件 b == true 并不能保证在 main 协程 中的 a != nil。在新的协程中编译器和 CPU 可能会通过 重排序指令 进行优化,因此,在运行时 b 赋值可能发生在 a 赋值之前,在 main 协程 中当 a 被修改后,它将会让部分 a 一直保持为 nil
package main

import (
    "time"
    "runtime"
)

func main() {
    var a []int // nil
    var b bool  // false

    // a new goroutine
    go func () {
        a = make([]int, 3)
        b = true // write b
    }()

    for !b { // read b
        time.Sleep(time.Second)
        runtime.Gosched()
    }
    a[0], a[1], a[2] = 0, 1, 2 // might panic
}

上面的程序或者在一台计算机上运行的很好,但是在另一台上可能会引发异常。或者它可能运行了 N 次都很好,但是可能在第 (N+1) 次引发了异常。

我们将使用 sync 标准包中提供的信道或者同步方法去确保内存中的顺序。例如,

package main

func main() {
    var a []int = nil
    c := make(chan struct{})

    // a new goroutine
    go func () {
        a = make([]int, 3)
        c <- struct{}{}
    }()

    <-c
    a[0], a[1], a[2] = 0, 1, 2
}

使用 time.Sleep 调用去做同步

我们先来看一个简单的例子。

package main

import (
    "fmt"
    "time"
)

func main() {
    var x = 123

    go func() {
        x = 789 // write x
    }()

    time.Sleep(time.Second)
    fmt.Println(x) // read x
}

我们预期程序将打印出 789。如果我们运行它,通常情况下,它确定打印的是 789。但是,这个程序使用的同步方式好吗?No!原因是 Go 运行时并不保证 x 的写入一定会发生在 x 的读取之前。在某些条件下,比如在同一个操作系统上,大部分 CPU 资源被其它运行的程序所占用的情况下,写入 x 可能就会发生在读取 x 之后。这就是为什么我们在正式的项目中,从来不使用 time.Sleep 调用去实现同步的原因。

我们来看一下另外一个示例。

package main

import (
    "fmt"
    "time"
)

var x = 0

func main() {
    var num = 123
    var p = &num

    c := make(chan int)

    go func() {
        c <- *p + x
    }()

    time.Sleep(time.Second)
    num = 789
    fmt.Println(<-c)
}

你认为程序的预期输出是什么?123 还是 789?事实上它的输出与编译器有关。对于标准的 Go 编译器 1.10 来说,这个程序很有可能输出是 123。但是在理论上,它可能输出的是 789,或者其它的随机数。

现在,我们来改变 c <- *p + xc <- *p,然后再次运行这个程序。你将会发现输出变成了 789 (使用标准的 Go 编译器 1.10)。这再次说明它的输出是与编译器相关的。

是的,在上面的程序中存在数据争用。表达式 *p 可能会被先计算、后计算、或者在处理赋值语句 num = 789 时计算。time.Sleep 调用并不能保证 *p 发生在赋值语句处理之前进行。

对于这个特定的示例,我们将在新的协程创建之前,将值保存到一个临时值中,然后在新的协程中使用临时值去消除数据争用。

...
    tmp := *p + x
    go func() {
        c <- tmp
    }()
...

使协程挂起

挂起协程是指让协程一直处于阻塞状态。导致协程被挂起的原因很多。比如,

  • 一个协程尝试从一个 nil 信道中或者从一个没有其它协程给它发送值的信道中检索数据。
  • 一个协程尝试去发送一个值到 nil 信道,或者发送到一个没有其它的协程接收值的信道中。
  • 一个协程被它自己死锁。
  • 一组协程彼此死锁。
  • 当运行一个没有 default 分支的 select 代码块时,一个协程被阻塞,以及在 select 代码块中 case 关键字后的所有信道操作保持阻塞状态。

除了有时我们为了避免程序退出,特意让一个程序中的 main 协程保持挂起之外,大多数其它的协程挂起都是意外情况。Go 运行时很难判断一个协程到底是处于挂起状态还是临时阻塞。因此,Go 运行时并不会去释放一个挂起的协程所占用的资源。

谁先响应谁获胜 的信道使用案例中,如果使用的 future 信道容量不够大,当尝试向 Future 信道发送结果时,一些响应较慢的信道将被挂起。比如,如果调用下面的函数,将有 4 个协程处于永远阻塞状态。

func request() int {
    c := make(chan int)
    for i := 0; i < 5; i++ {
        i := i
        go func() {
            c <- i // 4 goroutines will hang here.
        }()
    }
    return <-c
}

为避免这 4 个协程一直处于挂起状态, c 信道的容量必须至少是 4

实现谁先响应谁获胜的第二种方法 的信道使用案例中,如果将 future 信道用做非缓冲信道,那么有可能这个信息将永远也不会有响应而挂起。例如,如果在一个协程中调用下面的函数,协程可能会挂起。原因是,如果接收操作 <-c 准备就绪之前,五个发送操作全部尝试发送,那么所有的尝试发送的操作将全部失败,因此那个调用者协程将永远也不会接收到值。

func request() int {
    c := make(chan int)
    for i := 0; i < 5; i++ {
        i := i
        go func() {
            select {
            case c <- i:
            default:
            }
        }()
    }
    return <-c
}

将信道 c 变成缓冲信道将保证五个发送操作中的至少一个操作会发送成功,这样,上面函数中的那个调用者协程将不会被挂起。

sync 标准包中拷贝类型值

在实践中,sync 标准包中的类型值不会被拷贝。我们应该只拷贝这个值的指针。

下面是一个错误的并发编程示例。在这个示例中,当调用 Counter.Value 方法时,将拷贝一个 Counter 接收值。作为接收值的一个字段,Counter 接收值的各个 Mutex 字段也会被拷贝。拷贝不是同步发生的,因此,拷贝的 Mutex 值可能会出错。即便是没有错误,拷贝的 Counter 接收值的访问保护也是没有意义的。

import "sync"

type Counter struct {
    sync.Mutex
    n int64
}

// This method is okay.
func (c *Counter) Increase(d int64) (r int64) {
    c.Lock()
    c.n += d
    r = c.n
    c.Unlock()
    return
}

// The method is bad. When it is called, a Counter
// receiver value will be copied.
func (c Counter) Value() (r int64) {
    c.Lock()
    r = c.n
    c.Unlock()
    return
}

我们只需要改变 Value 接收类型方法为指针类型 *Counter,就可以避免拷贝 Mutex 值。

在官方的 Go SDK 中提供的 go vet 命令将会报告潜在的错误值拷贝。

在错误的地方调用 sync.WaitGroup 的方法

每个 sync.WaitGroup 值维护一个内部计数器,这个计数器的初始值为 0。如果一个 WaitGroup 计数器的值是 0,调用 WaitGroup 值的 Wait 方法就不会被阻塞,否则,在计数器值为 0 之前,这个调用会一直被阻塞。

为了让 WaitGroup 值的使用有意义,当一个 WaitGroup 计数器值为 0 时,必须在相应的 WaitGroup 值的 Wait 方法调用之前,去调用 WaitGroup 值的 Add 方法。

例如,下面的程序中,在不正确位置调用了 Add 方法,这将使最后打印出的数字不总是 100。事实上,这个程序最后打印的数字可能是在 [0, 100) 范围内的一个随意数字。原因就是 Add 方法的调用并不保证一定会发生在 Wait 方法调用之前。

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var wg sync.WaitGroup
    var x int32 = 0
    for i := 0; i < 100; i++ {
        go func() {
            wg.Add(1)
            atomic.AddInt32(&x, 1)
            wg.Done()
        }()
    }

    fmt.Println("To wait ...")
    wg.Wait()
    fmt.Println(atomic.LoadInt32(&x))
}

为让程序的表现符合预期,在 for 循环中,我们将把 Add 方法的调用移动到创建的新协程的范围之外,修改后的代码如下。

...
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            atomic.AddInt32(&x, 1)
            wg.Done()
        }()
    }
...

不正确使用 futures 信道

信道使用案例 的文章中,我们知道一些函数将返回 futures 信道。假设 fafb 就是这样的两个函数,那么下面的调用就使用了不正确的 future 参数。

doSomethingWithFutureArguments(<-fa(), <-fb())

在上面的代码行中,两个信道接收操作是顺序进行的,而不是并发的。我们做如下修改使它变成并发操作。

ca, cb := fa(), fb()
doSomethingWithFutureArguments(<-c1, <-c2)

没有等协程的最后的活动的发送结束就关闭信道

Go 程序员经常犯的一个错误是,还有一些其它的协程可能会发送值到以前的信道时,这个信道就已经被关闭了。当这样的发送(发送到一个已经关闭的信道)真实发生时,将引发一个异常。

这种错误在一些以往的著名 Go 项目中也有发生,比如在 Kubernetes 项目中的 这个 bug这个 bug

如何安全和优雅地关闭信道,请阅读 这篇文章

在值上做 64 位原子操作时没有保证值地址 64 位对齐

到目前为止(Go 1.10),在标准的 Go 编译器中,在一个 64 位原子操作中涉及到的值的地址要求必须是 64 位对齐的。如果没有对齐则导致当前的协程异常。对于标准的 Go 编译器来说,这种失败仅发生在 32 位的架构上。请阅读 内存布局 去了解如何在一个 32 位操作系统上保证 64 位对齐。

没有注意到大量的资源被 time.After 函数调用占用

time 标准包中的 After 函数返回 一个延迟通知的信道。这个函数在某些情况下用起来很便捷,但是,每次调用它将创建一个 time.Timer 类型的新值。这个新创建的 Timer 值在通过传递参数到 After 函数指定期间保持激活状态,如果在这个期间过多的调用了该函数,可能会有太多的 Timer 值保持激活,这将占用大量的内存和计算资源。

例如,如果调用了下列的 longRunning 函数,将在一分钟内产生大量的消息,然后在某些周期内将有大量的 Timer 值保持激活,即便是大量的这些 Timer 值已经没用了也是如此。

import (
    "fmt"
    "time"
)

// The function will return if a message arrival interval
// is larger than one minute.
func longRunning(messages <-chan string) {
    for {
        select {
        case <-time.After(time.Minute):
            return
        case msg := <-messages:
            fmt.Println(msg)
        }
    }
}

为避免在上述代码中创建过多的 Timer 值,我们将使用一个单一的 Timer 值去完成同样的任务。

func longRunning(messages <-chan string) {
    timer := time.NewTimer(time.Minute)
    defer timer.Stop()

    for {
        select {
        case <-timer.C:
            return
        case msg := <-messages:
            fmt.Println(msg)
            if !timer.Stop() {
                <-timer.C
            }
        }

        // The above "if" block can also be put here.

        timer.Reset(time.Minute)
    }
}

不正确地使用 time.Timer

在最后,我们将展示一个符合语言使用习惯的 time.Timer 值的使用示例。需要注意的一个细节是,那个 Reset 方法总是在停止或者 time.Timer 值释放时被使用。

select 块的第一个 case 分支的结束部分,time.Timer 值被释放,因此,我们不需要去停止它。但是必须在第二个分支中停止定时器。如果在第二个分支中 if 代码块缺失,它可能至少在 Reset 方法调用时,会(通过 Go 运行时)发送到 timer.C 信道,并且那个 longRunning 函数可能会早于预期返回,对于 Reset 方法来说,它可能仅仅是重置内部定时器为 0,它将不会清理(耗尽)那个发送到 timer.C 信道的值。

例如,下面的程序很有可能在一秒内而不是十秒时退出。并且更重要的是,这个程序并不是 DRF 的(LCTT 译注:data race free,多线程程序的一种同步程度)。

package main

import (
    "fmt"
    "time"
)

func main() {
    start := time.Now()
    timer := time.NewTimer(time.Second/2)
    select {
    case <-timer.C:
    default:
        time.Sleep(time.Second) // go here
    }
    timer.Reset(time.Second * 10)
    <-timer.C
    fmt.Println(time.Since(start)) // 1.000188181s
}

time.Timer 的值不再被其它任何一个东西使用时,它的值可能被停留在一种非停止状态,但是,建议在结束时停止它。

在多个协程中如果不按建议使用 time.Timer 值并发,可能会有 bug 隐患。

我们不应该依赖一个 Reset 方法调用的返回值。Reset 方法返回值的存在仅仅是为了兼容性目的。


via: https://go101.org/article/concurrent-common-mistakes.html

作者:<go101.org> 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你的下一个 Python 项目需要一个模板引擎来自动生成 HTML 吗?这有几种选择。

在我的日常工作中,我花费大量的时间将各种来源的数据转化为可读的信息。虽然很多时候这只是电子表格或某种类型的图表或其他数据可视化的形式,但也有其他时候,将数据以书面形式呈现是有意义的。

但我的头疼地方就是复制和粘贴。如果你要将数据从源头移动到标准化模板,则不应该复制和粘贴。这很容易出错,说实话,这会浪费你的时间。

因此,对于我定期发送的任何遵循一个共同的模式的信息,我倾向于找到某种方法来自动化至少一部分信息。也许这涉及到在电子表格中创建一些公式,一个快速 shell 脚本或其他解决方案,以便使用从外部源提取的信息自动填充模板。

但最近,我一直在探索 Python 模板来完成从其他数据集创建报告和图表的大部分工作。

Python 模板引擎非常强大。我的简化报告创建的使用案例仅仅触及了它的皮毛。许多开发人员正在利用这些工具来构建完整的 web 应用程序和内容管理系统。但是,你并不需要有一个复杂的 web 应用程序才能使用 Python 模板工具。

为什么选择模板?

每个模板工具都不甚相同,你应该阅读文档以了解其确切的用法。但让我们创建一个假设的例子。假设我想创建一个简短的页面,列出我最近编写的所有 Python 主题。就像这样:

<html>
  <head>
    <title>My Python articles</title>
  </head>
  <body>

    <p>These are some of the things I have written about Python:</p>
    <ul>
      <li>Python GUIs</li>
      <li>Python IDEs</li>
      <li>Python web scrapers</li>
    </ul>

  </body>
</html>

当它仅仅是这三个项目时,维护它是很简单的。但是当我想添加第四个、第五个或第六十七个时会发生什么?我可以从包含我所有页面列表的 CSV 文件或其他数据文件生成它,而不是手动编码此页面吗?我可以轻松地为我写的每个主题创建重复内容吗?我可以以编程方式更改每个页面上的文本标题吗?这就是模板引擎可以发挥作用的地方。

有许多不同的选择,今天我将与你其中分享三个,顺序不分先后:MakoJinja2Genshi

Mako

Mako 是以 MIT 许可证发布的 Python 模板工具,专为快速展现而设计的(与 Jinja2 不同)。Reddit 已经使用 Mako 来展现他们的网页,它同时也是 Pyramid 和 Pylons 等 web 框架的默认模板语言。它相当简单且易于使用。你可以使用几行代码来设计模板;支持 Python 2.x 和 3.x,它是一个功能强大且功能丰富的工具,具有良好的文档,这一点我认为是必须的。其功能包括过滤器、继承、可调用块和内置缓存系统,这些系统可以被大型或复杂的 web 项目导入。

Jinja2

Jinja2 是另一个快速且功能全面的选项,可用于 Python 2.x 和 3.x,遵循 BSD 许可证。Jinja2 从功能角度与 Mako 有很多重叠,因此对于新手来说,你在两者之间的选择可能会归结为你喜欢的格式化风格。Jinja2 还将模板编译为字节码,并具有 HTML 转义、沙盒、模板继承和模板沙盒部分的功能。其用户包括 Mozilla、 SourceForge、 NPR、 Instagram 等,并且还具有强大的文档。与 Mako 在模板内部使用 Python 逻辑不同的是,Jinja2 使用自己的语法。

Genshi

Genshi 是我会提到的第三个选择。它是一个 XML 工具,具有强大的模板组件,所以如果你使用的数据已经是 XML 格式,或者你需要使用网页以外的格式,Genshi 可能成为你的一个很好的解决方案。HTML 基本上是一种 XML(好吧,不是精确的,但这超出了本文的范围,有点卖弄学问了),因此格式化它们非常相似。由于我通常使用的很多数据都是 XML 或其他类型的数据,因此我非常喜欢使用我可以用于多种事物的工具。

发行版目前仅支持 Python 2.x,尽管 Python 3 支持存在于主干中,但我提醒你,它看起来并没有得到有效的开发。Genshi 遵循 BSD 许可证提供。

示例

因此,在上面的假设示例中,我不会每次写新主题时都更新 HTML 文件,而是通过编程方式对其进行更新。我可以创建一个模板,如下所示:

<html>
  <head>
    <title>My Python articles</title>
  </head>
  <body>

    <p>These are some of the things I have written about Python:</p>
    <ul>
      %for topic in topics:
      <li>${topic}</li>
      %endfor
    </ul>

  </body>
</html>

然后我可以使用我的模板库来迭代每个主题,比如使用 Mako,像这样:

from mako.template import Template

mytemplate = Template(filename='template.txt')
print(mytemplate.render(topics=("Python GUIs","Python IDEs","Python web scrapers")))

当然,在现实世界的用法中,我不会将这些内容手动地列在变量中,而是将它们从外部数据源(如数据库或 API)中提取出来。

这些不是仅有的 Python 模板引擎。如果你正在开始创建一个将大量使用模板的新项目,那么你考虑的可能不仅仅是这三种选择。在 Python 维基上查看更全面的列表,以获得更多值得考虑的项目。


via: https://opensource.com/resources/python/template-libraries

作者:Jason Baker 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

深入探讨 Python 的 for 循环来看看它们在底层如何工作,以及为什么它们会按照它们的方式工作。

Python 的 for 循环不会像其他语言中的 for 循环那样工作。在这篇文章中,我们将深入探讨 Python 的 for 循环来看看它们在底层如何工作,以及为什么它们会按照它们的方式工作。

循环的问题

我们将通过看一些“陷阱”开始我们的旅程,在我们了解循环如何在 Python 中工作之后,我们将再次看看这些问题并解释发生了什么。

问题 1:循环两次

假设我们有一个数字列表和一个生成器,生成器会返回这些数字的平方:

>>> numbers = [1, 2, 3, 5, 7]
>>> squares = (n**2 for n in numbers)

我们可以将生成器对象传递给 tuple 构造器,从而使其变为一个元组:

>>> tuple(squares)
(1, 4, 9, 25, 49)

如果我们使用相同的生成器对象并将其传给 sum 函数,我们可能会期望得到这些数的和,即 88

>>> sum(squares)
0

但是我们得到了 0

问题 2:包含的检查

让我们使用相同的数字列表和相同的生成器对象:

>>> numbers = [1, 2, 3, 5, 7]

>>> squares = (n**2 for n in numbers)

如果我们询问 9 是否在 squares 生成器中,Python 将会告诉我们 9 在 squares 中。但是如果我们再次询问相同的问题,Python 会告诉我们 9 不在 squares 中。

>>> 9 in squares
True
>>> 9 in squares
False

我们询问相同的问题两次,Python 给了两个不同的答案。

问题 3 :拆包

这个字典有两个键值对:

>>> counts = {'apples': 2, 'oranges': 1}

让我们使用多个变量来对这个字典进行拆包:

>>> x, y = counts

你可能会期望当我们对这个字典进行拆包时,我们会得到键值对或者得到一个错误。

但是解包字典不会引发错误,也不会返回键值对。当你解包一个字典时,你会得到键:

>>> x
'apples'

回顾:Python 的 for 循环

在我们了解一些关于这些 Python 片段的逻辑之后,我们将回到这些问题。

Python 没有传统的 for 循环。为了解释我的意思,让我们看一看另一种编程语言的 for 循环。

这是一种传统 C 风格的 for 循环,用 JavaScript 编写:

let numbers = [1, 2, 3, 5, 7];
for (let i = 0; i < numbers.length; i += 1) {
    print(numbers[i])
}

JavaScript、 C、 C++、 Java、 PHP 和一大堆其他编程语言都有这种风格的 for 循环,但是 Python 确实没有

Python 确实没有 传统 C 风格的 for 循环。在 Python 中确实有一些我们称之为 for 循环的东西,但是它的工作方式类似于 foreach 循环

这是 Python 的 for 循环的风格:

numbers = [1, 2, 3, 5, 7]
for n in numbers:
    print(n)

与传统 C 风格的 for 循环不同,Python 的 for 循环没有索引变量,没有索引变量初始化,边界检查,或者索引递增。Python 的 for 循环完成了对我们的 numbers 列表进行遍历的所有工作。

因此,当我们在 Python 中确实有 for 循环时,我们没有传统 C 风格的 for 循环。我们称之为 for 循环的东西的工作机制与之相比有很大的不同。

定义:可迭代和序列

既然我们已经解决了 Python 世界中无索引的 for 循环,那么让我们在此之外来看一些定义。

可迭代是任何你可以用 Python 中的 for 循环遍历的东西。可迭代意味着可以遍历,任何可以遍历的东西都是可迭代的。

for item in some_iterable:
    print(item)

序列是一种非常常见的可迭代类型,列表,元组和字符串都是序列。

>>> numbers = [1, 2, 3, 5, 7]
>>> coordinates = (4, 5, 7)
>>> words = "hello there"

序列是可迭代的,它有一些特定的特征集。它们可以从 0 开始索引,以小于序列的长度结束,它们有一个长度并且它们可以被切分。列表,元组,字符串和其他所有序列都是这样工作的。

>>> numbers[0]
1
>>> coordinates[2]
7
>>> words[4]
'o'

Python 中很多东西都是可迭代的,但不是所有可迭代的东西都是序列。集合、字典、文件和生成器都是可迭代的,但是它们都不是序列。

>>> my_set = {1, 2, 3}
>>> my_dict = {'k1': 'v1', 'k2': 'v2'}
>>> my_file = open('some_file.txt')
>>> squares = (n**2 for n in my_set)

因此,任何可以用 for 循环遍历的东西都是可迭代的,序列只是一种可迭代的类型,但是 Python 也有许多其他种类的迭代器。

Python 的 for 循环不使用索引

你可能认为,Python 的 for 循环在底层使用了索引进行循环。在这里我们使用 while 循环和索引手动遍历:

numbers = [1, 2, 3, 5, 7]
i = 0
while i < len(numbers):
    print(numbers[i])
    i += 1

这适用于列表,但它不会对所有东西都起作用。这种循环方式只适用于序列

如果我们尝试用索引去手动遍历一个集合,我们会得到一个错误:

>>> fruits = {'lemon', 'apple', 'orange', 'watermelon'}
>>> i = 0
>>> while i < len(fruits):
...     print(fruits[i])
...     i += 1
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: 'set' object does not support indexing

集合不是序列,所以它们不支持索引。

我们不能使用索引手动对 Python 中的每一个迭代对象进行遍历。对于那些不是序列的迭代器来说,这是行不通的。

迭代器驱动 for 循环

因此,我们已经看到,Python 的 for 循环在底层不使用索引。相反,Python 的 for 循环使用迭代器

迭代器就是可以驱动可迭代对象的东西。你可以从任何可迭代对象中获得迭代器,你也可以使用迭代器来手动对它的迭代进行遍历。

让我们来看看它是如何工作的。

这里有三个可迭代对象:一个集合,一个元组和一个字符串。

>>> numbers = {1, 2, 3, 5, 7}
>>> coordinates = (4, 5, 7)
>>> words = "hello there"

我们可以使用 Python 的内置 iter 函数来访问这些迭代器,将一个迭代器传递给 iter 函数总会给我们返回一个迭代器,无论我们正在使用哪种类型的迭代器。

>>> iter(numbers)
<set_iterator object at 0x7f2b9271c860>
>>> iter(coordinates)
<tuple_iterator object at 0x7f2b9271ce80>
>>> iter(words)
<str_iterator object at 0x7f2b9271c860>

一旦我们有了迭代器,我们可以做的事情就是通过将它传递给内置的 next 函数来获取它的下一项。

>>> numbers = [1, 2, 3]
>>> my_iterator = iter(numbers)
>>> next(my_iterator)
1
>>> next(my_iterator)
2

迭代器是有状态的,这意味着一旦你从它们中消耗了一项,它就消失了。

如果你从迭代器中请求 next 项,但是其中没有更多的项了,你将得到一个 StopIteration 异常:

>>> next(my_iterator)
3
>>> next(my_iterator)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

所以你可以从每个迭代中获得一个迭代器,迭代器唯一能做的事情就是用 next 函数请求它们的下一项。如果你将它们传递给 next,但它们没有下一项了,那么就会引发 StopIteration 异常。

你可以将迭代器想象成 Pez 分配器(LCTT 译注:Pez 是一个结合玩具的独特复合式糖果),不能重新分配。你可以把 Pez 拿出去,但是一旦 Pez 被移走,它就不能被放回去,一旦分配器空了,它就没用了。

没有 for 的循环

既然我们已经了解了迭代器和 iter 以及 next 函数,我们将尝试在不使用 for 循环的情况下手动遍历迭代器。

我们将通过尝试将这个 for 循环变为 while 循环:

def funky_for_loop(iterable, action_to_do):
    for item in iterable:
        action_to_do(item)

为了做到这点,我们需要:

  1. 从给定的可迭代对象中获得迭代器
  2. 反复从迭代器中获得下一项
  3. 如果我们成功获得下一项,就执行 for 循环的主体
  4. 如果我们在获得下一项时得到了一个 StopIteration 异常,那么就停止循环
def funky_for_loop(iterable, action_to_do):
    iterator = iter(iterable)
    done_looping = False
    while not done_looping:
        try:
            item = next(iterator)
        except StopIteration:
            done_looping = True
        else:
            action_to_do(item)

我们只是通过使用 while 循环和迭代器重新定义了 for 循环。

上面的代码基本上定义了 Python 在底层循环的工作方式。如果你理解内置的 iternext 函数的遍历循环的工作方式,那么你就会理解 Python 的 for 循环是如何工作的。

事实上,你不仅仅会理解 for 循环在 Python 中是如何工作的,所有形式的遍历一个可迭代对象都是这样工作的。

迭代器协议 iterator protocol 是一种很好表示 “在 Python 中遍历迭代器是如何工作的”的方式。它本质上是对 iternext 函数在 Python 中是如何工作的定义。Python 中所有形式的迭代都是由迭代器协议驱动的。

迭代器协议被 for 循环使用(正如我们已经看到的那样):

for n in numbers:
    print(n)

多重赋值也使用迭代器协议:

x, y, z = coordinates

星型表达式也是用迭代器协议:

a, b, *rest = numbers
print(*numbers)

许多内置函数依赖于迭代器协议:

unique_numbers = set(numbers)

在 Python 中任何与迭代器一起工作的东西都可能以某种方式使用迭代器协议。每当你在 Python 中遍历一个可迭代对象时,你将依赖于迭代器协议。

生成器是迭代器

所以你可能会想:迭代器看起来很酷,但它们看起来像一个实现细节,我们作为 Python 的使用者,可能不需要关心它们。

我有消息告诉你:在 Python 中直接使用迭代器是很常见的。

这里的 squares 对象是一个生成器:

>>> numbers = [1, 2, 3]
>>> squares = (n**2 for n in numbers)

生成器是迭代器,这意味着你可以在生成器上调用 next 来获得它的下一项:

>>> next(squares)
1
>>> next(squares)
4

但是如果你以前用过生成器,你可能也知道可以循环遍历生成器:

>>> squares = (n**2 for n in numbers)
>>> for n in squares:
...     print(n)
...
1
4
9

如果你可以在 Python 中循环遍历某些东西,那么它就是可迭代的

所以生成器是迭代器,但是生成器也是可迭代的,这又是怎么回事呢?

我欺骗了你

所以在我之前解释迭代器如何工作时,我跳过了它们的某些重要的细节。

生成器是可迭代的

我再说一遍:Python 中的每一个迭代器都是可迭代的,意味着你可以循环遍历迭代器。

因为迭代器也是可迭代的,所以你可以使用内置 next 函数从可迭代对象中获得迭代器:

>>> numbers = [1, 2, 3]
>>> iterator1 = iter(numbers)
>>> iterator2 = iter(iterator1)

请记住,当我们在可迭代对象上调用 iter 时,它会给我们返回一个迭代器。

当我们在迭代器上调用 iter 时,它会给我们返回它自己:

>>> iterator1 is iterator2
True

迭代器是可迭代的,所有的迭代器都是它们自己的迭代器。

def is_iterator(iterable):
    return iter(iterable) is iterable

迷惑了吗?

让我们回顾一些这些措辞。

  • 一个可迭代对象是你可以迭代的东西
  • 一个迭代对象器是一种实际上遍历可迭代对象的代理

此外,在 Python 中迭代器也是可迭代的,它们充当它们自己的迭代器。

所以迭代器是可迭代的,但是它们没有一些可迭代对象拥有的各种特性。

迭代器没有长度,它们不能被索引:

>>> numbers = [1, 2, 3, 5, 7]
>>> iterator = iter(numbers)
>>> len(iterator)
TypeError: object of type 'list_iterator' has no len()
>>> iterator[0]
TypeError: 'list_iterator' object is not subscriptable

从我们作为 Python 程序员的角度来看,你可以使用迭代器来做的唯一有用的事情是将其传递给内置的 next 函数,或者对其进行循环遍历:

>>> next(iterator)
1
>>> list(iterator)
[2, 3, 5, 7]

如果我们第二次循环遍历迭代器,我们将一无所获:

>>> list(iterator)
[]

你可以把迭代器看作是惰性迭代器,它们是一次性使用,这意味着它们只能循环遍历一次。

正如你在下面的真值表中所看到的,可迭代对象并不总是迭代器,但是迭代器总是可迭代的:

对象可迭代?迭代器?
可迭代对象V?
迭代器VV
生成器VV
列表VX

全部的迭代器协议

让我们从 Python 的角度来定义迭代器是如何工作的。

可迭代对象可以被传递给 iter 函数,以便为它们获得迭代器。

迭代器:

  • 可以传递给 next 函数,它将给出下一项,如果没有下一项,那么它将会引发 StopIteration 异常
  • 可以传递给 iter 函数,它会返回一个自身的迭代器

这些语句反过来也是正确的:

  • 任何可以在不引发 TypeError 异常的情况下传递给 iter 的东西都是可迭代的
  • 任何可以在不引发 TypeError 异常的情况下传递给 next 的东西都是一个迭代器
  • 当传递给 iter 时,任何返回自身的东西都是一个迭代器

这就是 Python 中的迭代器协议。

迭代器的惰性

迭代器允许我们一起工作,创建惰性可迭代对象,即在我们要求它们提供下一项之前,它们不做任何事情。因为可以创建惰性迭代器,所以我们可以创建无限长的迭代器。我们可以创建对系统资源比较保守的迭代器,可以节省我们的内存,节省 CPU 时间。

迭代器无处不在

你已经在 Python 中看到过许多迭代器,我也提到过生成器是迭代器。Python 的许多内置类型也是迭代器。例如,Python 的 enumeratereversed 对象就是迭代器。

>>> letters = ['a', 'b', 'c']
>>> e = enumerate(letters)
>>> e
<enumerate object at 0x7f112b0e6510>
>>> next(e)
(0, 'a')

在 Python 3 中,zip, mapfilter 也是迭代器。

>>> numbers = [1, 2, 3, 5, 7]
>>> letters = ['a', 'b', 'c']
>>> z = zip(numbers, letters)
>>> z
<zip object at 0x7f112cc6ce48>
>>> next(z)
(1, 'a')

Python 中的文件对象也是迭代器。

>>> next(open('hello.txt'))
'hello world\n'

在 Python 标准库和第三方库中内置了大量的迭代器。这些迭代器首先惰性迭代器一样,延迟工作直到你请求它们下一项。

创建你自己的迭代器

知道你已经在使用迭代器是很有用的,但是我希望你也知道,你可以创建自己的迭代器和你自己的惰性迭代器。

下面这个类构造了一个迭代器接受一个可迭代的数字,并在循环结束时提供每个数字的平方。

class square_all:
    def __init__(self, numbers):
        self.numbers = iter(numbers)
    def __next__(self):
        return next(self.numbers) * 2
    def __iter__(self):
        return self

但是在我们开始对该类的实例进行循环遍历之前,没有任何工作要做。

这里,我们有一个无限长的可迭代对象 count,你可以看到 square_all 接受 count 而不用完全循环遍历这个无限长的迭代:

>>> from itertools import count
>>> numbers = count(5)
>>> squares = square_all(numbers)
>>> next(squares)
25
>>> next(squares)
36

这个迭代器类是有效的,但我们通常不会这样做。通常,当我们想要做一个定制的迭代器时,我们会生成一个生成器函数:

def square_all(numbers):
    for n in numbers:
        yield n**2

这个生成器函数等价于我们上面所做的类,它的工作原理是一样的。

这种 yield 语句似乎很神奇,但它非常强大:yield 允许我们在调用 next 函数之间暂停生成器函数。yield 语句是将生成器函数与常规函数分离的东西。

另一种实现相同迭代器的方法是使用生成器表达式。

def square_all(numbers):
    return (n**2 for n in numbers)

这和我们的生成器函数确实是一样的,但是它使用的语法看起来像是一个列表推导一样。如果你需要在代码中使用惰性迭代,请考虑迭代器,并考虑使用生成器函数或生成器表达式。

迭代器如何改进你的代码

一旦你已经接受了在代码中使用惰性迭代器的想法,你就会发现有很多可能来发现或创建辅助函数,以此来帮助你循环遍历和处理数据。

惰性求和

这是一个 for 循环,它对 Django queryset 中的所有工作时间求和:

hours_worked = 0
for event in events:
    if event.is_billable():
        hours_worked += event.duration

下面是使用生成器表达式进行惰性评估的代码:

billable_times = (
    event.duration
    for event in events
    if event.is_billable()
)

hours_worked = sum(billable_times)

请注意,我们代码的形状发生了巨大变化。

将我们的计算工作时间变成一个惰性迭代器允许我们能够命名以前未命名(billable_times)的东西。这也允许我们使用 sum 函数,我们以前不能使用 sum 函数是因为我们甚至没有一个可迭代对象传递给它。迭代器允许你从根本上改变你组织代码的方式。

惰性和打破循环

这段代码打印出日志文件的前 10 行:

for i, line in enumerate(log_file):
    if i >= 10:
        break
    print(line)

这段代码做了同样的事情,但是我们使用的是 itertools.islice 函数来惰性地抓取文件中的前 10 行:

from itertools import islice
first_ten_lines = islice(log_file, 10)
for line in first_ten_lines:
    print(line)

我们定义的 first_ten_lines 变量是迭代器,同样,使用迭代器允许我们给以前未命名的东西命名(first_ten_lines)。命名事物可以使我们的代码更具描述性,更具可读性。

作为奖励,我们还消除了在循环中使用 break 语句的需要,因为 islice 实用函数为我们处理了中断。

你可以在标准库中的 itertools 中找到更多的迭代辅助函数,以及诸如 boltonsmore-itertools 之类的第三方库。

创建自己的迭代辅助函数

你可以在标准库和第三方库中找到用于循环的辅助函数,但你也可以自己创建!

这段代码列出了序列中连续值之间的差值列表。

current = readings[0]
for next_item in readings[1:]:
    differences.append(next_item - current)
    current = next_item

请注意,这段代码中有一个额外的变量,我们每次循环时都要指定它。还要注意,这段代码只适用于我们可以切片的东西,比如序列。如果 readings 是一个生成器,一个 zip 对象或其他任何类型的迭代器,那么这段代码就会失败。

让我们编写一个辅助函数来修复代码。

这是一个生成器函数,它为给定的迭代中的每个项目提供了当前项和下一项:

def with_next(iterable):
    """Yield (current, next_item) tuples for each item in iterable."""
    iterator = iter(iterable)
    current = next(iterator)
    for next_item in iterator:
        yield current, next_item
        current = next_item

我们从可迭代对象中手动获取一个迭代器,在它上面调用 next 来获取第一项,然后循环遍历迭代器获取后续所有的项目,跟踪后一个项目。这个函数不仅适用于序列,而且适用于任何类型迭代。

这段代码和以前代码是一样的,但是我们使用的是辅助函数而不是手动跟踪 next_item

differences = []
for current, next_item in with_next(readings):
    differences.append(next_item - current)

请注意,这段代码不会挂在我们循环周围的 next_item 上,with_next 生成器函数处理跟踪 next_item 的工作。

还要注意,这段代码已足够紧凑,如果我们愿意,我们甚至可以将方法复制到列表推导中来

differences = [
    (next_item - current)
    for current, next_item in with_next(readings)
]

再次回顾循环问题

现在我们准备回到之前看到的那些奇怪的例子并试着找出到底发生了什么。

问题 1:耗尽的迭代器

这里我们有一个生成器对象 squares

>>> numbers = [1, 2, 3, 5, 7]
>>> squares = (n**2 for n in numbers)

如果我们把这个生成器传递给 tuple 构造函数,我们将会得到它的一个元组:

>>> numbers = [1, 2, 3, 5, 7]
>>> squares = (n**2 for n in numbers)
>>> tuple(squares)
(1, 4, 9, 25, 49)

如果我们试着计算这个生成器中数字的和,使用 sum,我们就会得到 0

>>> sum(squares)
0

这个生成器现在是空的:我们已经把它耗尽了。如果我们试着再次创建一个元组,我们会得到一个空元组:

>>> tuple(squares)
()

生成器是迭代器,迭代器是一次性的。它们就像 Hello Kitty Pez 分配器那样不能重新加载。

问题 2:部分消耗一个迭代器

再次使用那个生成器对象 squares

>>> numbers = [1, 2, 3, 5, 7]
>>> squares = (n**2 for n in numbers)

如果我们询问 9 是否在 squares 生成器中,我们会得到 True

>>> 9 in squares
True

但是我们再次询问相同的问题,我们会得到 False

>>> 9 in squares
False

当我们询问 9 是否在迭代器中时,Python 必须对这个生成器进行循环遍历来找到 9。如果我们在检查了 9 之后继续循环遍历,我们只会得到最后两个数字,因为我们已经在找到 9 之前消耗了这些数字:

>>> numbers = [1, 2, 3, 5, 7]
>>> squares = (n**2 for n in numbers)
>>> 9 in squares
True
>>> list(squares)
[25, 49]

询问迭代器中是否包含某些东西将会部分地消耗迭代器。如果没有循环遍历迭代器,那么是没有办法知道某个东西是否在迭代器中。

问题 3:拆包是迭代

当你在字典上循环时,你会得到键:

>>> counts = {'apples': 2, 'oranges': 1}
>>> for key in counts:
...     print(key)
...
apples
oranges

当你对一个字典进行拆包时,你也会得到键:

>>> x, y = counts
>>> x, y
('apples', 'oranges')

循环依赖于迭代器协议,可迭代对象拆包也依赖于有迭代器协议。拆包一个字典与在字典上循环遍历是一样的,两者都使用迭代器协议,所以在这两种情况下都得到相同的结果。

回顾

序列是迭代器,但是不是所有的迭代器都是序列。当有人说“迭代器”这个词时,你只能假设他们的意思是“你可以迭代的东西”。不要假设迭代器可以被循环遍历两次、询问它们的长度或者索引。

迭代器是 Python 中最基本的可迭代形式。如果你想在代码中做一个惰性迭代,请考虑迭代器,并考虑使用生成器函数或生成器表达式。

最后,请记住,Python 中的每一种迭代都依赖于迭代器协议,因此理解迭代器协议是理解 Python 中的循环的关键。

这里有一些我推荐的相关文章和视频:

本文是基于作者去年在 DjangoCon AUPyGothamNorth Bay Python 中发表的 Loop Better 演讲。有关更多内容,请参加将于 2018 年 5 月 9 日至 17 日在 Columbus, Ohio 举办的 PYCON


via: https://opensource.com/article/18/3/loop-better-deeper-look-iteration-python

作者:Trey Hunner 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 Python 中有许多库可以很容易地测试、转换和读取日期和时间信息。

这篇文章是与 Jeff Triplett 一起合写的。

曾几何时,我们中的一个人(Lacey)盯了一个多小时的 Python 文档中描述日期和时间格式化字符串的表格。当我试图编写从 API 中将日期时间字符串转换为 Python datetime 对象时,我很难理解其中的特定部分,因此我决定请求帮助。

有人问道:“为什么你不使用 dateutil 呢?”

读者,如果你没有从这个月的 Python 专栏中获得任何东西,只是学习到有比 datetime 的 strptime 更容易地将 datetime 字符串转换为 datetime 对象的方法,那么我们觉得就已经成功了。

但是,除了将字符串转换为更有用的 Python 对象之外,还有许多库都有一些有用的方法和工具,可以让您更轻松地进行时间测试、将时间转换为不同的时区、以人类可读的格式传递时间信息,等等。如果这是你在 Python 中第一次接触日期和时间,请暂停并阅读 如何使用 Python的日期和时间 。要理解为什么在编程中处理日期和时间是困难的,请阅读 愚蠢的程序员相信时间

这篇文章将会向你介绍以下库:

随意跳过那些你已经熟悉的库,专注于那些对你而言是新的库。

内建的 datetime 模块

在跳转到其他库之前,让我们回顾一下如何使用 datetime 模块将日期字符串转换为 Python datetime 对象。

假设我们从 API 接受到一个日期字符串,并且需要它作为 Python datetime 对象存在:

2018-04-29T17:45:25Z

这个字符串包括:

  • 日期是 YYYY-MM-DD 格式的
  • 字母 T 表示时间即将到来
  • 时间是 HH:II:SS 格式的
  • 表示此时间的时区指示符 Z 采用 UTC (详细了解日期时间字符格式

要使用 datetime 模块将此字符串转换为 Python datetime 对象,你应该从 strptime 开始。 datetime.strptime 接受日期字符串和格式化字符并返回一个 Python datetime 对象。

我们必须手动将日期时间字符串的每个部分转换为 Python 的 datetime.strptime 可以理解的合适的格式化字符串。四位数年份由 %Y 表示,两位数月份是 %m,两位数的日期是 %d。在 24 小时制中,小时是 %H,分钟是 %M,秒是 %S

为了得出这些结论,需要在Python 文档的表格中多加注意。

由于字符串中的 Z 表示此日期时间字符串采用 UTC,所以我们可以在格式中忽略此项。(现在,我们不会担心时区。)

转换的代码是这样的:

$ from datetime import datetime 
$ datetime.strptime('2018-04-29T17:45:25Z', '%Y-%m-%dT%H:%M:%SZ')
datetime.datetime(2018, 4, 29, 17, 45, 25)

格式字符串很难阅读和理解。我必须手动计算原始字符串中的字母 T 和 “Z”的位置,以及标点符号和格式化字符串,如 %S%m。有些不太了解 datetime 的人阅读我的代码可能会发现它很难理解,尽管其含义已有文档记载,但它仍然很难阅读。

让我们看看其他库是如何处理这种转换的。

Dateutil

dateutil 模块datetime 模块做了一些扩展。

继续使用上面的解析示例,使用 dateutil 实现相同的结果要简单得多:

$ from dateutil.parser import parse
$ parse('2018-04-29T17:45:25Z')
datetime.datetime(2018, 4, 29, 17, 45, 25, tzinfo=tzutc())

如果字符串包含时区,那么 dateutil 解析器会自动返回字符串的时区。由于我们在 UTC 时区,你可以看到返回来一个 datetime 对象。如果你想解析完全忽略时区信息并返回原生的 datetime 对象,你可以传递 ignoretz=True 来解析,如下所示:

$ from dateutil.parser import parse
$ parse('2018-04-29T17:45:25Z', ignoretz=True)
datetime.datetime(2018, 4, 29, 17, 45, 25)

dateutil 还可以解析其他人类可读的日期字符串:

$ parse('April 29th, 2018 at 5:45 pm')
datetime.datetime(2018, 4, 29, 17, 45)

dateutil 还提供了像 relativedelta 的工具,它用于计算两个日期时间之间的时间差或向日期时间添加或删除时间,rrule 创建重复日期时间,tz 用于解决时区以及其他工具。

Arrow

Arrow 是另一个库,其目标是操作、格式化,以及处理对人类更友好的日期和时间。它包含 dateutil,根据其文档,它旨在“帮助你使用更少的包导入和更少的代码来处理日期和时间”。

要返回我们的解析示例,下面介绍如何使用 Arrow 将日期字符串转换为 Arrow 的 datetime 类的实例:

$ import arrow 
$ arrow.get('2018-04-29T17:45:25Z')
<Arrow [2018-04-29T17:45:25+00:00]>

你也可以在 get() 的第二个参数中指定格式,就像使用 strptime 一样,但是 Arrow 会尽力解析你给出的字符串,get() 返回 Arrow 的 datetime 类的一个实例。要使用 Arrow 来获取 Python datetime 对象,按照如下所示链式 datetime:

$ arrow.get('2018-04-29T17:45:25Z').datetime
datetime.datetime(2018, 4, 29, 17, 45, 25, tzinfo=tzutc())

通过 Arrow datetime 类的实例,你可以访问 Arrow 的其他有用方法。例如,它的 humanize() 方法将日期时间翻译成人类可读的短语,就像这样:

$ import arrow
$ utc = arrow.utcnow()
$ utc.humanize()
'seconds ago'

在 Arrow 的文档中阅读更多关于其有用方法的信息。

Moment

Moment 的作者认为它是“内部测试版”,但即使它处于早期阶段,它也是非常受欢迎的,我们想来讨论它。

Moment 的方法将字符转换为其他更有用的东西很简单,类似于我们之前提到的库:

$ import moment
$ moment.date('2018-04-29T17:45:25Z')
<Moment(2018-04-29T17:45:25)>

就像其他库一样,它最初返回它自己的 datetime 类的实例,要返回 Python datetime 对象,添加额外的 date() 调用即可。

$ moment.date('2018-04-29T17:45:25Z').date
datetime.datetime(2018, 4, 29, 17, 45, 25, tzinfo=<StaticTzInfo 'Z'>)

这将 Moment datetime 类转换为 Python datetime 对象。

Moment 还提供了使用人类可读的语言创建新日期的方法。例如创建一个明天的日期:

$ moment.date("tomorrow")
<Moment(2018-04-06T11:24:42)>

它的 add()subtract() 命令使用关键字参数来简化日期的操作。为了获得后天,Moment 会使用下面的代码:

$ moment.date("tomorrow").add(days=1)
<Moment(2018-04-07T11:26:48)>

Maya

Maya 包含了 Python 中其他流行处理日期时间的库,包括 Humanize、 pytz 和 pendulum 等等。这个项目旨在让人们更容易处理日期。

Maya 的 README 包含几个有用的实例。以下是如何使用 Maya 来重新处理以前的解析示例:

$ import maya
$ maya.parse('2018-04-29T17:45:25Z').datetime()
datetime.datetime(2018, 4, 29, 17, 45, 25, tzinfo=<UTC>)

注意我们必须在 maya.parse() 之后调用 datetime()。如果我们跳过这一步,Maya 将会返回一个 MayaDT 类的示例:<MayaDT epoch=1525023925.0>

由于 Maya 与 datetime 库中很多有用的方法重叠,因此它可以使用 MayaDT 类的实例执行诸如使用 slang_time() 方法将时间偏移量转换为纯文本语言,并将日期时间间隔保存在单个类的实例中。以下是如何使用 Maya 将日期时间表示为人类可读的短语:

$ import maya
$ maya.parse('2018-04-29T17:45:25Z').slang_time()
'23 days from now

显然,slang_time() 的输出将根据距离 datetime 对象相对较近或较远的距离而变化。

Delorean

Delorean,以 《返回未来》 电影中的时间旅行汽车命名,它对于操纵日期时间特别有用,包括将日期时间转换为其他时区并添加或减去时间。

Delorean 需要有效的 Python datetime 对象才能工作,所以如果你需要使用时间字符串,最好将其与上述库中的一个配合使用。例如,将 Maya 与 Delorean 一起使用:

$ import maya 
$ d_t = maya.parse('2018-04-29T17:45:25Z').datetime()

现在,你有了一个 datetime 对象 d\_t,你可以使用 Delorean 来做一些事情,例如将日期时间转换为美国东部时区:

$ from delorean import Delorean
$ d = Delorean(d_t)
$ d
Delorean(datetime=datetime.datetime(2018, 4, 29, 17, 45, 25), timezone='UTC')
$ d.shift('US/Eastern')
Delorean(datetime=datetime.datetime(2018, 4, 29, 13, 45, 25), timezone='US/Eastern')

看到小时是怎样从 17 变成 13 了吗?

你也可以使用自然语言方法来操作 datetime 对象。获取 2018 年 4 月 29 日之后的下个星期五(我们现在使用的):

$ d.next_friday()
Delorean(datetime=datetime.datetime(2018, 5, 4, 13, 45, 25), timezone='US/Eastern')

在 Delorean 的文档中阅读更多关于其的用法。

Freezegun

Freezegun 是一个可以帮助你在 Python 代码中测试特定日期的库。使用 @freeze_time 装饰器,你可以为测试用例设置特定的日期和时间,并且所有对 datetime.datetime.now()datetime.datetime.utcnow() 等的调用都将返回你指定的日期和时间。例如:

from freezegun import freeze_time
import datetime

@freeze_time("2017-04-14")
def test(): 
    assert datetime.datetime.now() == datetime.datetime(2017, 4, 14)

要跨时区进行测试,你可以将 tz_offset 参数传递给装饰器。freeze_time 装饰器也接受更简单的口语化日期,例如 @freeze_time('April 4, 2017')


上面提到的每个库都提供了一组不同的特性和功能,也许很难决定哪一个最适合你的需要。Maya 的作者, Kenneth Reitz 说到:“所有这些项目相辅相成,它们都是我们的朋友”。

这些库共享一些功能,但不是全部。有些擅长时间操作,有些擅长解析,但它们都有共同的目标,即让你对日期和时间的工作更轻松。下次你发现自己对 Python 的内置 datetime 模块感到沮丧,我们希望你可以选择其中的一个库进行试验。


via: https://opensource.com/article/18/4/python-datetime-libraries

作者: Lacey Williams Hensche 选题: lujun9972 译者: MjSeven 校对: wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这个简单优雅的模块可以让你包含调试或仅用于开发环境的代码,而在产品环境中隐藏它们。

仅用于调试或开发调整时的 Perl 代码块有时会很有用。这很好,但是这样的代码块可能会对性能产生很大的影响, 尤其是在运行时才决定是否执行它。

Curtis "Ovid" Poe 最近编写了一个可以帮助解决这个问题的模块:Keyword::DEVELOPMENT。该模块利用 Keyword::Simple 和 Perl 5.012 中引入的可插入关键字架构来创建了新的关键字:DEVELOPMENT。它使用 PERL_KEYWORD_DEVELOPMENT 环境变量的值来确定是否要执行一段代码。

使用它不能更容易了:

use Keyword::DEVELOPMENT;

sub doing_my_big_loop {
    my $self = shift;
    DEVELOPMENT {
        # insert expensive debugging code here!
    }
}

在编译时,DEVELOPMENT 块内的代码已经被优化掉了,根本就不存在。

你看到好处了么?在沙盒中将 PERL_KEYWORD_DEVELOPMENT 环境变量设置为 true,在生产环境设为 false,并且可以将有价值的调试工具提交到你的代码库中,在你需要的时候随时可用。

在缺乏高级配置管理的系统中,你也可以使用此模块来处理生产和开发或测试环境之间的设置差异:

sub connect_to_my_database {

    my $dsn = "dbi:mysql:productiondb";
    my $user = "db_user";
    my $pass = "db_pass";

    DEVELOPMENT {
        # Override some of that config information
        $dsn = "dbi:mysql:developmentdb";
    }

    my $db_handle = DBI->connect($dsn, $user, $pass);
}

稍后对此代码片段的增强使你能在其他地方,比如 YAML 或 INI 中读取配置信息,但我希望您能在此看到该工具。

我查看了关键字 Keyword::DEVELOPMENT 的源码,花了大约半小时研究,“天哪,我为什么没有想到这个?”安装 Keyword::Simple 后,Curtis 给我们的模块就非常简单了。这是我长期以来在自己的编码实践中所需要的一个优雅解决方案。


via: https://opensource.com/article/18/4/perl-module-debugging-code

作者:Ruth Holloway 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

有个简单实用的技巧可以让你的 bash 脚本更稳健 -- 确保总是执行必要的收尾工作,哪怕是在发生异常的时候。要做到这一点,秘诀就是 bash 提供的一个叫做 EXIT 的伪信号,你可以 trap 它,当脚本因为任何原因退出时,相应的命令或函数就会执行。我们来看看它是如何工作的。

基本的代码结构看起来像这样:

#!/bin/bash
function finish {
  # 你的收尾代码
}
trap finish EXIT

你可以把任何你觉得务必要运行的代码放在这个 finish 函数里。一个很好的例子是:创建一个临时目录,事后再删除它。

#!/bin/bash
scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
function finish {
  rm -rf "$scratch"
}
trap finish EXIT

这样,在你的核心代码中,你就可以在这个 $scratch 目录里下载、生成、操作中间或临时数据了。 注1

# 下载所有版本的 linux 内核…… 为了科学研究!
for major in {1..4}; do
  for minor in {0..99}; do
    for patchlevel in {0..99}; do
      tarball="linux-${major}-${minor}-${patchlevel}.tar.bz2"
      curl -q "http://kernel.org/path/to/$tarball" -o "$scratch/$tarball" || true
      if [ -f "$scratch/$tarball" ]; then
        tar jxf "$scratch/$tarball"
      fi
    done
  done
done
# 整合成单个文件
# 复制到目标位置
cp "$scratch/frankenstein-linux.tar.bz2" "$1"
# 脚本结束, scratch 目录自动被删除

比较一下如果不用 trap ,你是怎么删除 scratch 目录的:

#!/bin/bash
# 别这样做!

scratch=$(mktemp -d -t tmp.XXXXXXXXXX)

# 在这里插入你的几十上百行代码

# 都搞定了,退出之前把目录删除
rm -rf "$scratch"

这有什么问题么?很多:

  • 如果运行出错导致脚本提前退出, scratch 目录及里面的内容不会被删除。这会导致资料泄漏,可能引发安全问题。
  • 如果这个脚本的设计初衷就是在脚本末尾以前退出,那么你必须手动复制粘贴 rm 命令到每一个出口。
  • 这也给维护带来了麻烦。如果今后在脚本某处添加了一个 exit ,你很可能就忘了加上删除操作 -- 从而制造潜在的安全漏洞。

无论如何,服务要在线

另外一个场景: 想象一下你正在运行一些自动化系统运维任务,要临时关闭一项服务,最后这项服务需要重启,而且要万无一失,即使脚本运行出错。那么你可以这样做:

function finish {
    # 重启服务
    sudo /etc/init.d/something start
}
trap finish EXIT
sudo /etc/init.d/something stop
# 主要任务代码

# 脚本结束,执行 finish 函数重启服务

一个具体的实例:比如 Ubuntu 服务器上运行着 MongoDB ,你要为 crond 写一个脚本来临时关闭服务并做一些日常维护工作。你应该这样写:

function finish {
    # 重启服务
    sudo service mongdb start
}
trap finish EXIT
# 关闭 mongod 服务
sudo service mongdb stop
# (如果 mongod 配置了 fork ,比如 replica set ,你可能需要执行 “sudo killall --wait /usr/bin/mongod”)

控制开销

有一种情况特别能体现 EXIT trap 的价值:如果你的脚本运行过程中需要初始化一下成本高昂的资源,结束时要确保把它们释放掉。比如你在 AWS (Amazon Web Services) 上工作,要在脚本中创建一个镜像。

(名词解释: 在亚马逊云上的运行的服务器叫“实例”。实例从 亚马逊机器镜像 Amazon Machine Image 创建而来,通常被称为 “AMI” 或 “镜像” 。AMI 相当于某个特殊时间点的服务器快照。)

我们可以这样创建一个自定义的 AMI :

  1. 基于一个基准 AMI 运行一个实例(例如,启动一个服务器)。
  2. 在实例中手动或运行脚本来做一些修改。
  3. 用修改后的实例创建一个镜像。
  4. 如果不再需要这个实例,可以将其删除。

最后一步相当重要。如果你的脚本没有把实例删除掉,它会一直运行并计费。(到月底你的账单让你大跌眼镜时,恐怕哭都来不及了!)

如果把 AMI 的创建封装在脚本里,我们就可以利用 trap EXIT 来删除实例了。我们还可以用上 EC2 的命令行工具:

#!/bin/bash
# 定义基准 AMI 的 ID
ami=$1
# 保存临时实例的 ID
instance=''
# 作为 IT 人,让我们看看 scratch 目录的另类用法
scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
function finish {
    if [ -n "$instance" ]; then
        ec2-terminate-instances "$instance"
    fi
    rm -rf "$scratch"
}
trap finish EXIT
# 创建实例,将输出(包含实例 ID )保存到 scratch 目录下的文件里
ec2-run-instances "$ami" > "$scratch/run-instance"
# 提取实例 ID
instance=$(grep '^INSTANCE' "$scratch/run-instance" | cut -f 2)

脚本执行到这里,实例(EC2 服务器)已经开始运行 注2 。接下来你可以做任何事情:在实例中安装软件,修改配置文件等,然后为最终版本创建一个镜像。实例会在脚本结束时被删除 -- 即使脚本因错误而提前退出。(请确保实例创建成功后再运行业务代码。)

更多应用

这篇文章只讲了些皮毛。我已经使用这个 bash 技巧很多年了,现在还能不时发现一些有趣的用法。你也可以把这个方法应用到你自己的场景中,从而提升你的 bash 脚本的可靠性。

尾注

  • 注1. mktemp 的选项 -t 在 Linux 上是可选的,在 OS X 上是必需的。带上此选项可以让你的脚本有更好的可移植性。
  • 注2. 如果只是为了获取实例 ID ,我们不用创建文件,直接写成 instance=$(ec2-run-instances "$ami" | grep '^INSTANCE' | cut -f 2) 就可以。但把输出写入文件可以记录更多有用信息,便于调试 ,代码可读性也更强。

作者简介:美国加利福尼亚旧金山的作家,软件工程师,企业家。Powerful Python 的作者,他的 blog


via: http://redsymbol.net/articles/bash-exit-traps/

作者:aaron maxwell 译者:Dotcra 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出