Python-秘籍手册-二-

26 阅读49分钟

Python 秘籍手册(二)

原文:Python Recipes Handbook

协议:CC BY-NC-SA 4.0

六、函数

任何现代编程语言都需要某种方式将函数组合成可重用的代码块。在 Python 中,可重用代码的最基本单元之一是函数。在这一章中,你将会看到函数是如何在 Python 中工作的,以及它们可能会做一些令人惊讶的事情的地方。

6-1.创建基本函数

问题

您希望创建一个基本函数来处理一个简单的任务,比如求 2 的平方。

解决办法

在 Python 中,可以使用内置的def关键字来创建一个新函数。

它是如何工作的

清单 6-1 给出了一个例子。

def square_of_two():
   ans = 2 * 2
   return ans
Listing 6-1.Defining a Basic Function

如您所见,函数代码的主体是由缩进级别定义的。第一行使用def并创建一个具有给定名称的新函数。在本例中,您创建了一个名为square_of_two()的函数。现在,您可以像调用任何其他函数一样调用该函数。例如,参见清单 6-2 。

>>> square_of_two()
4
>>> my_ans = square_of_two()
Listing 6-2.Calling a Function

如果您的函数应该向调用语句返回值,那么您需要使用内置的return关键字。如果你的新函数中有条件或循环,你需要增加这些结构的缩进量,如清单 6-3 所示。

def fact_two():
   a = 2
   ans = a
   while a > 1:
      ans = ans * (a-1)
      a = a - 1
   return ans
Listing 6-3.Factorial of Two Function

这将返回 2 的阶乘,即 2。

6-2.使用命名参数而不是位置参数

问题

您希望将参数传递给函数,可以选择使用名称。这允许参数以任意顺序传递给函数。

解决办法

由于变量名称在 Python 中是非类型化的,所以只需在括号内的参数列表中添加名称。这些可以基于位置使用,也可以通过名称显式使用。

它是如何工作的

添加参数非常简单,如清单 6-4 所示。

def square_num(a):
   return a*a
Listing 6-4.Squaring Any Number

然后你可以用两种不同的方式调用这个函数,或者通过位置或者通过标签。清单 6-5 显示了这将会是什么样子。

>>> square_num(2)
4
>>> square_num(a=3)
9
Listing 6-5.Calling Functions with Parameters

如果有多个参数,可以混合使用位置参数和命名参数。唯一需要注意的是,在使用任何命名参数之前,需要包含所有位置参数。清单 6-6 显示了一个简单的例子。

def multiplication(a, b, c):
   return a*b*c
>>> multiplication(1, 2, 3)
6
>>> multiplication(2, c=3, b=1)
6
Listing 6-6.Multiplying Many Numbers

如果你尝试类似于multiplication (1, a=2, c=3)的东西,你会得到一个错误,因为根据位置规则,你已经给了 a 一个值,然后你试图用命名参数a=2给它另一个值。同样,如果您试图在一个命名参数之后使用一个位置参数,比如multiplication(a=1,b=2,3),您也会得到一个错误。

6-3.在函数中使用默认值

问题

如果没有传递默认值,您希望允许函数使用默认值。

解决办法

定义函数及其输入参数时,如果没有提供任何参数,可以包含一个默认值。

它是如何工作的

在定义函数时,您可以简单地说明需要什么样的默认值,如清单 6-7 所示。

def multiplication(a=1,b=2,c=3):
   return a*b*c
Listing 6-7.Defining Default Parameter Values

关于位置参数和命名参数的所有规则仍然适用。清单 6-8 展示了一些例子。

>>> multiplication()
6
>>> multiplication(5)
30
>>> multiplication(1,1)
3
>>> multiplication(c=1)
2
Listing 6-8.Multiplication Examples

既然您有了默认值,那么对于那些没有亲自阅读过代码的人来说,您就有了一些看不见的操作。在这种情况下,切换到使用严格命名的参数有助于澄清代码并帮助将来的代码维护是有意义的。

6-4.实现递归算法

问题

您需要在 Python 程序中实现一个递归算法。

解决办法

Python 支持递归,因此您可以从函数内部调用函数。

它是如何工作的

因为 Python 支持递归,所以可以简单地直接调用函数。典型的例子是计算阶乘,如清单 6-9 所示。

def fact(a):
    if a == 1:
        return 1
    else:
        return a * fact(a-1)
>>> fact(5)
120
Listing 6-9.Calculating Factorials Through Recursion

虽然有一些算法实际上只作为递归函数工作,但应该谨慎对待它们。递归函数实际上是嵌套的函数调用。这意味着递归的每一步都需要在下一次调用之前将当前状态推送到堆栈上。根据需要跟踪的内容,您可能会很快用完大量 RAM。此外,每个函数调用都需要更多时间来进行上下文切换。因此,在开始使用递归函数之前,一定要确保这是唯一的解决方案。

6-5.使用 Lambda 函数创建临时匿名函数

问题

您临时需要一个函数(例如,作为另一个函数的参数),并且不需要通过名称访问它。

解决办法

Python 有内置的lambda语句机制,它提供了创建和使用匿名函数的能力。

它是如何工作的

为了展示如何使用 lambda 函数,我们需要一个需要 lambda 函数的示例。清单 6-10 展示了一个带两个参数的函数和一个函数,并以两个给定的参数作为输入执行给定的函数。

def apply_operator(a, b, f):
    return f(a,b)
Listing 6-10.Applying a Function

如果您只想在上面的示例代码中使用一次性函数,可以在调用中直接使用 lambda 函数。清单 6-11 展示了如何应用乘法函数。

>>> apply_operator(2, 3, lambda x, y: x*y)
6
Listing 6-11.Applying a Multiplication Function

lambda 函数的最大限制是它们被限制在一个表达式行中。任何大于这个值的都需要定义为常规函数。

6-6.生成专门的函数

问题

您需要创建一个能够为特殊情况生成专用函数的函数。例如,您可能希望对复数而不是常规浮点数使用不同的平均函数。

解决办法

使用 lambda 函数,您可以生成专门的函数。

它是如何工作的

因为函数只是另一种类型的对象,所以它们可以从函数调用中返回。利用这一事实,您可以创建一个函数,它接受一些输入参数,并踢出由它定义的函数。例如,清单 6-12 基于输入值生成一个缩放函数。

def generate_scaler(a):
    return lambda x: a*x
Listing 6-12.Generating Scaling Functions

然后,您可以使用这个生成器创建一个将数字缩放 2 或 3 的函数,如清单 6-13 所示。

>>> doubler = generate_scaler(2)
>>> doubler(3)
6
>>> tripler = generate_scaler(3)
>>> tripler(3)
9
Listing 6-13.Function Generator

Examples

七、类和对象

一旦有了将代码块存储到可重用函数中的方法,想要将多个函数捆绑到更大的可重用代码块(称为对象)中是很自然的事情。除了这些实际的代码,您可能还想以属性的形式存储数据。为了定义这些新对象,您需要创建类,然后将它们实例化为您可以使用的实际对象。在这一章中,你将会看到在你开始定义和使用新对象时出现的一些最常见的任务。

7-1.发现对象的类型(一切都是对象)

问题

Python 中的几乎所有东西都是对象。重要的是弄清楚你拥有什么样的物品。

解决办法

内置函数type()返回作为参数传入的对象的类。您还可以使用isinstance()来测试给定的对象是否属于某个特定类的同一类型。

它是如何工作的

清单 7-1 给出了一个例子。

>>> a = 1
>>> type(a)
<class 'int'>
>>> b = "Hello World"
>>> type(b)
<class 'str'>
Listing 7-1.Checking the Type of an Object

这将查询给定的对象并返回输入对象的类型对象。您可以使用它来检查两个对象是否属于同一类。如果您想查看一个对象是否属于某个特定的类,您可以使用isinstance(),如清单 7-2 所示。

>>> a = 1
>>> isinstance(a, int)
TRUE
Listing 7-2.Is an Object of a Particular Class?

7-2.创建类

问题

您希望创建一个新的类,封装一组方法和属性,以便在其他代码中使用。

解决办法

关键字class允许你定义一个新的类。

它是如何工作的

与函数一样,定义一个新类就像使用class关键字,然后拥有一个缩进的代码块一样简单。你可以在清单 7-3 中看到一个非常简单的例子。

class my_simple_class:
    a = 3
    b = "Hello World"
Listing 7-3.Creating a Simple Class

清单 7-4 展示了一个更复杂的例子,也包括一系列方法。

class my_complex_class:
    a = 42
    def method1():
        print("The value of a is " + str(a))
    b = "Hello World"
Listing 7-4.Creating a Complex Class

7-3.添加私有字段

问题

您希望您的类的某些部分是私有的,也就是说,不能从其他代码段访问。

解决办法

在 Python 中,所有的方法和属性都是公开可见的。作为一种解决方案,在属性或方法的名称中添加下划线字符几乎被普遍接受,因为它代表了一个不适合公共使用的元素。

它是如何工作的

定义一个只能在所讨论的类中使用的属性应该至少有一个下划线字符作为前缀。通常的做法是在元素名的开头和结尾添加两个下划线字符,如清单 7-5 所示。

class priv_vars:
   __a__ = "This is a private variable
"
Listing 7-5.Creating a Private Variable

有一种特殊形式的私有名称,它促使 Python 使用名称管理系统来重命名元素。如果某个元素可能与其他元素同名,则可以添加至少两个前导下划线字符和最多一个尾随下划线字符。然后 Python 会将类名添加到元素名的前面。例如,如果你有一个像

class my_class1:
    __a_ = 1

属性__a_将被重命名为_my_class__a_以避免名称冲突。

7-4.子类

问题

您希望在另一个类中提供的功能基础上进行构建。

解决办法

子类化的能力是任何面向对象编程语言的关键。Python 支持从单个基类或多个基类继承。

它是如何工作的

从一个类继承只是在新类的定义中添加一个对相关类的引用。清单 7-6 中给出了一个简单的例子。

class animal:
    type = "mammal"
class dog(animal):
    breed = "labrador"
>>> my_dog = dog()
>>> my_dog.breed
labrador
>>> my_dog.type
mammal
Listing 7-6.Inheriting from a Class

如您所见,类dog继承了animal类型的属性。解析方法和属性的方式是首先检查主类,看它是否存在。如果没有,那么 Python 将检查任何继承的类,看看该属性或方法是否存在。这意味着您可以用自己的版本重写继承的方法或属性。清单 7-7 中animal类型改为bird

class bird(animal):
    type = "bird"
>>> my_bird = bird()
>>> my_bird.type
bird
Listing 7-7.Overriding Class Attributes

如果您从多个类继承,您可以简单地将它们作为多个参数添加到您的类定义中,如清单 7-8 所示。

class eagle(animal, bird):
    species = "eagle"
>>> my_eagle = eagle()
>>> my_eagle.type
animal
Listing 7-8.
Multiple Inheritance

如您所见,在处理多重继承时,顺序很重要。当 Python 尝试解析方法或属性的定义位置时,它从主类开始,然后从左到右依次遍历继承的类,直到找到所述方法或属性的第一个实例。以上案例中,正确的继承顺序应该是class eagle(bird, animal)

7-5.初始化对象

问题

当实例化一个新对象时,需要执行一些初始值或初始处理步骤。

解决办法

您可以使用私有方法__init__在初始化时运行设置代码。

它是如何工作的

当 Python 实例化一个新对象时,它会查看是否有一个名为__init__的方法。如果找到了,这个函数会在实例化完成后立即执行。它还可以在实例化时获取参数,这些参数可用于进一步处理设置步骤,如清单 7-9 所示。

class my_complex:
    def __init__(self, real_part, imaginary_part):
        self.r = real_part
        self.i = imaginary_part

>>> complex_num = my_complex(2, 3)
>>> complex_num.r
2

Listing 7-9.
Initialization Functions

参数self指的是被实例化的对象,允许你在初始化过程中与对象进行交互。

7-6.比较对象

问题

你需要比较两个物体,看它们是否相同。

解决办法

在比较对象时,有两种不同的相等性思想:将一个对象与其自身进行比较,并查看两个不同的对象是否具有相同的属性。

它是如何工作的

第一种类型的等式是测试两个变量名是否实际指向同一个对象。这是 Python 中对象和指向它们的标签分离的副作用。为了测试这种类型的等式,您需要使用操作符isis not,如清单 7-10 所示。

>>> a = "string1"
>>> b = a
>>> a is b
True
Listing 7-10.Comparing Object Identities

下一种类型的比较包括比较两个对象的内容,看它们是否有相同的值。值的概念在 Python 中不是一个通用的概念。对象值的计算由使用运算符==!=<=>=<>时执行的运算符代码处理。根据您定义的任何类的详细信息,您可能希望覆盖这些比较运算符的代码。例如,假设您已经创建了一个表示一本书的类,并且您希望使用页数作为给定 book 对象的值。然后,您可以用清单 7-11 中的代码覆盖这些操作符。

class book:
    def __init__(self, pages):
        self.pages = pages
    def __lt__(self, other):
        return self.pages < other
    def __gt__(self, other):
        return self.pages > other
....
Listing 7-11.Overriding Comparison Operators

依此类推,适用于所有的运算符方法。

7-7.创建后更改对象

问题

创建对象后,您需要对其进行更改。

解决办法

在 Python 中,几乎所有对象都是可塑的,可以根据它们的属性和方法进行修改。内置对象,如strint,没有可塑性。从您自己的类定义中创建的对象是可扩展的,可以动态地创建和使用新的属性。

它是如何工作的

例如,您可以使用清单 7-11 中定义的类,通过简单地使用一个名为title的新属性来添加图书的标题,如清单 7-12 所示。

>>> book1 = book(42)
>>> book1.title = "My Book"
>>> book1.title
My Book
Listing 7-12.Dynamically Created Attributes

您可以通过定义一个什么都不做的类,利用这种延展性来创建非常快速、灵活的数据结构,如清单 7-13 所示。

class my_container:
    pass
>>> container1 = my_container()
>>> container1.label = "The first container"
>>> container1.phone = "555-5555"
>>> container1.name = "John Doe"
Listing 7-13.Empty Classes for Data Storage

关键字pass是一个无操作函数。它实际上占用了表达式应该去的地方的空间,但是告诉 Python 实际上没有任何代码要运行。这使您可以创建一个完全空的对象,以后可以添加到该对象中。

7-8.实现多态行为

问题

您需要包括根据输入内容而变化的行为。

解决办法

Python 通过它是一种鸭式语言的事实来处理多态性。基本上,当一个函数试图使用一个对象作为参数时,它实际上会通过该对象需要实现的一些标准方法从该对象获取值。

它是如何工作的

展示这种技术的最好方式是使用一个例子。清单 7-14 显示了本例中使用的一系列类和函数。

class dog:
    def talk(self):
        print("bark")

class cat:
    def talk(self):
        print("meow")

def speak(animal):
    animal.talk()

Listing 7-14.Polymorphic Classes and Methods

从这里开始,根据您作为参数传递的动物类型,您将从函数speak()获得不同的行为。清单 7-15 展示了这种变化行为的一个例子。

>>> my_dog = dog()
>>> my_cat = cat()
>>> speak(my_dog)
bark
>>> speak(my_cat)
meow
Listing 7-15.Polymorphic Behavior

八、元编程

编程的一个关键原则是不要重复自己。每当你不止一次地做同样的任务时,你应该看一看它,看看是否有什么方法可以使它自动化。这是写程序的首要原因。但是这同样适用于编写代码本身的任务。如果您正在重复代码块,您应该后退一步,看看是否有一些更好的方法来达到相同的结果。

处理这个问题的一种技术是元编程。本质上,元编程是影响其他代码的代码。在 Python 中,通常的做法是使用装饰器、元类或描述符。

8-1.使用函数装饰器包装现有代码

问题

您希望通过用其他代码包装一个已经存在的函数来改变它的行为。如果不同的模块使用相同的装饰名,那么这个包装器代码可以换入或换出,允许您以不同的方式修改原始函数。

解决办法

通过在函数定义的顶部添加一个装饰符,使用一个以&符号开始的标签,可以包装一个函数。

它是如何工作的

清单 8-1 展示了一个使用由line_profile模块提供的装饰器的例子。

from line_profile import *

@profile
def my_adder(x, y):
    return x + y

Listing 8-1.Using the Profile Decorator

这段代码用来自line_profile模块的分析代码包装函数my_adder()。这个模块不是标准库的一部分,所以您需要将它安装到您的系统上。这与用另一个函数显式包装一个函数是一样的,如清单 8-2 所示。

from line_profiler import *

def my_adder(x, y):
    return x + y

my_adder = profile(my_adder)

Listing 8-2.Wrapping a Function

8-2.编写函数装饰器来包装现有代码

问题

你想为一个函数写一个包装器来增加额外的功能。

解决办法

Python 包含了wraps关键字,该关键字定义了一个可以包装另一个函数并用作装饰器的函数。

它是如何工作的

为了编写自己的装饰器,您需要使用来自functools模块的wraps关键字。这个关键字被用作装饰器来帮助定义你自己的新装饰器。清单 8-3 显示了一个打印出被修饰函数的函数名的例子。

from functools import wraps
def function_name(func):
    message = func.__qualname__
    @wraps(func)
    def wrapper((*args, **kwargs)):
        print(message)
        return func(*args, **kwargs)
    return wrapper
Listing 8-3.A Decorator to Print Out Function Names

然后你可以像其他装饰器一样使用它,如清单 8-4 所示。

@function_name
def adder(x,y):
    return x+y
Listing 8-4.Using Your Own Decorator

8-3.展开修饰函数

问题

您需要访问已被修饰的函数的功能。

解决办法

你可以通过使用函数的__wrapped__属性得到原始的解包函数。

它是如何工作的

假设装饰器是使用来自functoolswraps函数正确编码的,那么您可以通过使用__wrapped__属性获得原始函数,如清单 8-5 所示。

>>> adder(2,3)
adder
5
>>> adder.__wrapper__(2,3)
5
Listing 8-5.Getting the Unwrapped Function

8-4.使用元类改变类的结构

问题

您需要向类中添加额外的功能,类似于函数的装饰器。这可以通过使用元类改变一个类是哪个对象的实例来实现。

解决办法

元类的使用方式类似于子类。

它是如何工作的

当使用元类时,将它包含在类定义中,如清单 8-6 所示。

class my_counter(metaclass=Singleton):
    pass
Listing 8-6.Using a Metaclass

默认情况下,类是类型class的实例。清单 8-6 中的代码使新类成为Singleton类的实例,而不是类型class。您还可以在类定义中设置元类,如清单 8-7 所示。

class my_counter():
    __metaclass__ = Singleton
    pass
Listing 8-7.Setting the __metaclass__ Attribute

在清单 8-6 和 8-7 中,您的新类被创建为Singleton类的一个实例。这是在 Python 中使用 singleton 设计模式的一种方法。

8-5.编写元类

问题

您需要通过编写自己的元类来改变类的实例化方式。

解决办法

通过使用元类,您可以重新定义一个类实际上是如何实例化的,例如,允许您创建只能实例化一次的类(singleton 设计模式),或者被缓存的类。这些示例用于日志类或流数据解析器。

它是如何工作的

您可以通过构建一个覆盖实例化过程中使用的一个或多个函数的类来创建元类。例如,您可以覆盖__call__函数来创建一个不能实例化的类,如清单 8-8 所示。

class CannotInit(type):
    def __call__(self, *args, **kwargs):
        raise TypeError("Cannot instantiate")
Listing 8-8.A Metaclass That Stops Instantiation

现在,当您试图将它用作元类并直接实例化新类时,将会引发一个异常。

如果您需要更复杂的行为,例如在单例中,您可以覆盖多个函数,如清单 8-9 所示。

class MySingleton(type):
    def __init__(self, *args, **kwargs):
        self.__instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            self.__instance = super().__call__(*args, **kwargs)
            return self.__instance
        else:
            return self.__instance

Listing 8-9.Creating a Singleton Metaclass

这段代码捕获了使用这个元类的任何类的实例化和调用,因此一次只能存在一个实例。

8-6.使用签名改变函数接受的参数

问题

您希望能够在运行时控制函数的签名。这允许您动态地更改函数接受的参数。例如,可以强制函数在一种情况下只使用关键字参数,然后在另一种情况下允许使用位置参数或关键字参数。

解决办法

inspect模块包括创建和使用函数签名所需的工具。

它是如何工作的

清单 8-10 中的例子展示了如何创建一个新的签名。

>>> from inspect import Signature, Parameter
>>> params = [Parameter('x', Parameter.POSITIONAL_OR_KEYWORD),
...             Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42),
...             Parameter('z', Parameter.KEYWORD_ONLY, default=None)]
>>> my_sig = Signature(params)
>>> print(my_sig)
(x, y=42, *, z=None)
Listing 8-10.Creating a Signature

在这段代码中,您使用Parameter类创建一个函数参数列表。每种类型的参数都有关键字。需要注意的一点是,如果在一个普通的函数定义中有一个只包含关键字的参数列表,那么可以使用星号来标记哪些参数是只包含关键字的。当您打印出新创建的签名时,就会显示出来。

要使用这个签名,您可以使用bind方法获取位置和关键字参数的一般列表,并将它们绑定到您创建的签名中的参数。清单 8-11 中给出了一个例子。

def my_func(*args, **kwargs):
    bound_values = my_sig.bind(*args, **kwargs)
    for name, value in bound_values.arguments.items():
        print(name, value)
Listing 8-11.Using a Signature

这样,您可以让同一个函数使用不同的签名绑定参数,具体取决于您需要如何处理它们。

九、网络和互联网

互联网彻底改变了我们使用电脑的方式。以前,我们关注的是我们能用办公桌上的硬件做什么;现在我们可以考虑在分布在全球的机器上可以做什么工作。在本章中,您将学习通过网络(如互联网)与其他机器通信的一些核心技术。您将从查看最底层的方法之一开始:使用套接字。秘籍的其余部分将着眼于更高层次的技术,这些技术隐藏了围绕互联网通信的许多复杂性。

9-1.打开套接字连接

问题

您希望打开一个原始网络套接字连接来处理非结构化数据通信。

解决办法

Python 标准库包括一个socket类,它公开了网络通信的底层接口。

它是如何工作的

socket类提供了一种在非常低的级别访问网络硬件的方法。因此,它必须支持许多不同的网络协议。对于这些方法,您将关注最常见的情况,即希望通过以太网连接创建 TCP/IP 套接字。socket模块包含了socket类和其他几个你可以使用的实用函数和类。清单 9-1 展示了如何创建一个新的套接字并连接到一个远程机器。

import socket
host = '192.168.0.1'
port = 5050
my_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_sock.connect((host, port))
Listing 9-1.Opening a Socket to a Remote Machine

在这段代码中,机器地址以包含 IP 地址的字符串形式给出,端口以数字形式给出。这个实例化方法创建了一个socket对象,它既可以用来建立到远程机器的传出连接,也可以用来监听来自这些远程机器的传入连接请求。如果您只对建立一个到远程机器的传出连接感兴趣,您可以使用create_connection()方法,如清单 9-2 所示。

import socket
host = '192.168.0.1'
port = 5050
my_sock = socket.create_connection
((host, port))
Listing 9-2.Making an Outgoing Socket Connection

这个新创建的socket对象现在可以用来向远程机器发送数据和从远程机器接收数据。当您完成一个给定的套接字连接时,不要忘记关闭它,以便操作系统可以干净地关闭该连接并在它之后进行清理。清单 9-3 展示了这样做的样板代码。

my_sock.close()
Listing 9-3.Closing a Socket Connection

9-2.通过套接字读/写

问题

您希望通过开放的套接字连接与远程机器通信。

解决办法

socket类包含许多通过套接字连接发送和接收数据的不同方法。

它是如何工作的

一旦套接字被打开,您就可以使用来自socket对象的方法来发送和接收数据。发送数据最基本的方法是使用send()方法,如清单 9-4 所示。

msg = b'Hello World'
mesglen = len(msg)
totalsent = 0
while totalsent < msglen:
    sent = my_sock.send(msg[totalsent:])
    totalsent = totalsent + sent
Listing 9-4.Sending Data Over a Socket

这里有几点需要注意。首先,套接字通过网络发送字节,因此您的消息需要是一个字节串。第二件要注意的事情是,send()方法并不保证在任何特定的调用中会发送多少数据。它所做的只是返回在任何特定调用中成功发送的字节数,因此需要一个while循环来继续发送,直到您确定所有内容都已传输完毕。如果您正在发送简单的数据块,您可以使用sendall()方法,如清单 9-5 所示,它将处理循环,直到所有的数据都被发送完。

my_sock.sendall(b'Hello World')
Listing 9-5.Using sendall() with a Socket

接收数据的方法与发送数据的方法非常相似。一个主要的区别是,您需要告诉方法一次读入多少字节。例如,清单 9-6 展示了如何读入上面发送的数据,并确保得到了所有数据。

data_in = my_sock.recv(1024)
Listing 9-6.Reading Data from a Socket

这是可行的,因为您确实知道正在发送的消息长度小于 1,024 字节。如果消息较长,或者是可变的,就必须一遍又一遍地循环,直到收集到所有独立的块,就像发送数据时必须循环一样。与sendall()方法等效的接收方法是recv_into()方法。它允许您将数据接收到一个预先构造的缓冲区中,当所有的数据都已被接收或缓冲区已被填满时停止。清单 9-7 中给出了一个例子,展示了如何将多达 1024 个字节读入一个缓冲区。

buffer = bytearray(b' ' * 1024)
my_sock.recv_into(buffer)
Listing 9-7.Receiving Data Directly into a Buffer

9-3.用 POP 阅读电子邮件

问题

您想从 POP 电子邮件服务器上阅读电子邮件。

解决办法

Python 标准库包含一个名为poplib的模块,它封装了与 POP 服务器的通信。

它是如何工作的

与 POP 服务器通信涉及几个步骤。最基本的初始代码包括打开到 POP 服务器的连接和认证,如清单 9-8 所示。

import getpass, poplib
pop_serv = poplib.POP3('192.168.0.1')
pop_serv.user(getpass.getuser())
pop_serv.pass_(getpass.getpass())
Listing 9-8.Connecting to a POP Server and Authenticating

如您所见,您还导入了模块getpass。该模块帮助您的代码安全地向最终用户询问密码。getuser()方法还向操作系统查询最终用户的用户名。如果它与 POP 服务器的用户名相同,那就太好了。否则,您需要将它硬编码到您的脚本中,或者您必须明确地向最终用户询问他们的 POP 用户名。如果您的 POP 服务器正在监听非标准端口,您可以将其作为另一个参数。如果您使用的 POP 服务器更加安全,您需要使用POP3_SSL类来代替。现在,您可以与 POP 服务器进行交互..清单 9-9 展示了如何获取邮箱的当前状态。

msg_count, box_size = pop_serv.stat()
Listing 9-9.Getting the Status of a POP Mailbox

您可以使用清单 9-10 中的代码获得当前邮箱消息的列表。

msg_list = pop_serv.list()
Listing 9-10.Listing the Messages in a POP Mailbox

当您想查看单个电子邮件时,可以使用方法retr(),如清单 9-11 所示。

message = pop_serv.retr(1)
Listing 9-11.Retrieving Individual E-Mails from a POP Server

此方法使用消息索引。(示例中的索引 1)来决定检索哪个电子邮件。它还将选定的电子邮件标记为已在 POP 服务器上阅读。您还可以使用dele()方法清理您的邮箱,其中您将电子邮件索引作为参数。与任何与系统资源交互的代码一样,不要忘记用类似清单 9-12 的代码彻底关闭任何打开的连接。

pop_serv.quit()
Listing 9-12.Closing a POP Connection

9-4.用 IMAP 阅读电子邮件

问题

你需要从 IMAP 邮件服务器上阅读邮件。

解决办法

Python 标准库包括一个名为imaplib的模块,它简化了与 IMAP 电子邮件服务器的通信。

它是如何工作的

imaplib模块包含一个主类,它管理与 IMAP 服务器的通信。清单 9-13 展示了如何初始化和认证一个 IMAP 连接。

import imaplib, getpass
my_imap = imaplib.IMAP4('myimap.com')
my_imap.login(getpass.getuser(), getpass.getpass())
Listing 9-13.Creating an IMAP Connection

您可以使用getpass模块从最终用户那里安全地获取密码。如果您的 IMAP 服务器使用 SSL,您需要使用IMAP4_SSL类。IMAP 提供了更多的组织结构来组织您的电子邮件。其中包括拥有多个可用邮箱的能力。因此,在处理个人电子邮件之前,您需要选择一个邮箱。要获得电子邮件列表,您需要实际进行搜索。清单 9-14 展示了如何获取默认邮箱中所有邮件的列表。

my_imap.select()
typ, data = my_imap.search(None, 'ALL')
Listing 9-14.Getting a List of E-Mails from IMAP

然后可以在data变量中循环返回的电子邮件索引。对于这些索引中的每一个,您可以调用fetch()方法。如果您愿意,您可以有选择地只获取电子邮件的一部分。清单 9-15 展示了如何从 IMAP 服务器获取整个电子邮件。

email_msg = my_imap.fetch(email_id, '(RFC822)')
Listing 9-15.Fetching E-Mails from an IMAP Server

然后,您可以抽出电子邮件中您感兴趣的部分。IMAP 有一套非常完整的命令,可以用来处理邮箱和个人电子邮件。例如,清单 9-16 向您展示了如何删除一封电子邮件。

my_imap.store(email_id, '+FLAGS', '\\Deleted')
my_imap.expunge()
Listing 9-16.Deleting E-Mails from an IMAP Server

当您使用完 IMAP 服务器后,不要忘记清理所有东西,如清单 9-17 所示。

my_imap.close()
my_imap.logout()
Listing 9-17.Shutting Down an IMAP Connection

9-5.发送电子邮件

问题

你需要发一封电子邮件。

解决办法

Python 标准库包括一个名为smtplib的模块,它可以处理与 SMTP 服务器的通信。

它是如何工作的

电子邮件是使用 SMTP 协议通过互联网发送的。smtplib包括一个基类来处理与SMTP类的连接,如清单 9-18 所示。

import smtplib, getpass
my_smtp = smtplib.SMTP('my.smtp.com')
my_smtp.login(getpass.getuser(), getpass.getpass())
Listing 9-18.Connecting to an SMTP Server

只有当您的 SMTP 服务器需要身份验证时,才需要最后一行。如果您的 SMTP 服务器使用 SSL,您需要使用SMTP_SSL类来代替。一旦您的连接打开,您现在就可以发送电子邮件,如清单 9-19 所示。

from_addr = 'me@email.com'
to_addr = 'you@email.com'
msg = 'From: me@email.com\r\nTo: you@email.com\r\n\r\nHello World'
my_smtp.sendmail(from_addr, to_addr, msg)
Listing 9-19.Sending an E-Mail Message

一旦你发送了你的电子邮件,你可以用对象的方法来清理。

9-6.阅读网页

问题

你需要得到一个网页的内容。

解决办法

Python 标准库包括一个名为urllib的类模块,它处理几种不同协议上的通信。要与 web 服务器对话,您需要使用子模块urllib.request

它是如何工作的

几乎所有连接到 web 服务器的复杂性都被封装在模块urllib.request中的代码中。对于一个基本的连接,您可以使用方法urlopen(),如清单 9-20 所示。

import urllib.request
my_web = urllib.request.urlopen('http://www.python.org')
Listing 9-20.Connecting to a Web Server

然后,您可以从该 URL 读取数据。与大多数网络通信一样,数据是作为一系列字节读取的。清单 9-21 展示了如何从你所连接的 URL 中读取并打印前 100 个字节。

print(my_web.read(100))
Listing 9-21.Reading Data from a URL

9-7.张贴到网页

问题

您需要通过 GET 或 POST 与 web 表单进行通信。

解决办法

Python 标准库中的urllib.request模块支持使用 GET 或 POST 方法将表单数据发送到 web 服务器。

它是如何工作的

urllib.request模块包括一个名为Request的类,它可以处理与 web 服务器更复杂的交互。清单 9-22 展示了如何创建一个新的连接。

import urllib.request
mydata = b'some form data'
my_req = urllib.request.Request('http://form.host.com', data=mydata, method='POST')
Listing 9-22.Connecting to a Web Form

然后您可以在urlopen()方法中使用这个新的Request对象,如清单 9-23 所示。

my_form = urllib.request.urlopen(my_req)
print(my_form.status)
print(my_form.reason)
Listing 9-23.Opening a Request Object

9-8.充当服务器

问题

您希望创建一个侦听传入连接的网络应用。

解决办法

Python 标准库中的socket类支持监听传入的连接。

它是如何工作的

清单 9-24 展示了如何创建一个socket对象来监听给定的端口号。

import socket
host = ''
port = 4242
my_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_server.bind((host, port))
my_server.listen(1)
Listing 9-24.Listening on a Network Port

您应该知道,根据您的计算机上使用的设置,您可能会收到来自防火墙的警告。创建套接字后,您需要显式地接受可以读取的传入连接,如清单 9-25 所示。

conn, addr = my_server.accept()
print('Connected from host ', addr)
data = conn.recv(1024)
Listing 9-25.Accepting Incoming Connections

就像任何套接字连接一样,不要忘记使用close()方法干净地关闭网络连接。

十、模块和包

使用 Python 的最大优势之一是可用模块的大环境。几乎每一种可以想象的用途都有一个模块。事实上,这甚至是 XKCD 网络漫画的主题。在本章中,您将了解如何通过安装和使用可用的模块来利用已经完成的所有工作。您还将了解如何将您自己的工作打包成可以与其他人共享的模块。

10-1.导入模块

问题

您希望将给定模块中的功能导入到当前的 Python 程序中。

解决办法

Python 包含了import关键字来处理整个模块的导入,或者只导入可用功能的选定部分。

它是如何工作的

清单 10-1 展示了一个导入整个模块的基本例子。

>>> import math
Listing 10-1.Basic Importing of a Module

这段代码将标准模块math导入到当前 Python 解释器的名称空间中。您可以使用点符号来访问这个模块中包含的任何功能,如清单 10-2 所示。

>>> math.sin(45)
0.8509035245341184
Listing 10-2.Dot Notation for Modules

为了减少键入的数量,您可以在程序中使用别名。清单 10-3 展示了如何用as关键字为math标准模块做这件事。

>>> import math as m
>>> m.cos(45)
0.5253219888177297
Listing 10-3.Using Aliases

如果您只需要某些功能,您可以用关键字from导入特定的项目,如清单 10-4 所示。

>>> from math import sin, pi
>>> sin(pi/2)
1.0
Listing 10-4.Importing Parts of a Module

如您所见,这将math模块的导入部分放入 Python 程序的名称空间中。然后,您可以在不使用前面显示的点符号的情况下使用它们。您可以用清单 10-5 中的语句添加给定模块中的所有内容。

>>> from math import *
Listing 10-5.Importing All of a Module

请注意,每次导入都会增加名称冲突的几率。您最终可能会得到一个给定函数的多个实例。上次导入的版本是执行给定函数名时使用的版本。你需要意识到由此可能引发的复杂情况。

10-2.从源代码安装模块

问题

您需要将一个模块从源代码安装到一个标准库目录中。

解决办法

当从源代码安装时,许多模块被编写为使用 Python 中包含的一个名为distutils的系统。

它是如何工作的

Distutils是 Python 中包含的一个非常基本的系统。对于更简单的模块,处理安装过程已经足够了。该模块包括一个名为setup.p y 的特殊文件,用于处理细节。当您使用distutils安装一个包时,首先要将源文件解压到一个临时目录中。然后,您需要运行清单 10-6 中给出的命令。

python setup.py install
Listing 10-6.Installation with Distutils

这将运行模块所需的所有步骤,以便将它安装在 Python 库目录之一中。

10-3.从 Pypi 安装模块

问题

您想要安装 Pypi 存储库中的一个可用模块。

解决办法

工具 pip 提供了直接从 Pypi 存储库中轻松安装模块和包的能力。

它是如何工作的

在早期版本的 Python 中,pip 工具不可用。它从版本开始提供

    • Python 2 >= 2.7.9
    • Python 3 >= 3.4

对于早期版本,您首先需要安装它。如果您使用的是 Linux,大多数发行版都包含一个 pip 包。对于其他操作系统,或者如果您想要安装最新版本,您可以从 Pypi 网站( https://pip.pypa.io/en/latest/installing/ )下载所需的软件包,并按照附带的说明进行操作。然后,您可以使用清单 10-7 中的命令安装感兴趣的 Python 模块。

pip install numpy
Listing 10-7.Installing a Module with pip

使用 pip 真正强大的部分是它将处理依赖性检查。这样,您就可以专注于安装您需要的部件。如果您试图在一个您没有权限的系统上安装模块,您总是可以使用清单 10-8 中所示的命令将其安装在您的主目录中。

pip install --user numpy

Listing 10-8.Installing a Module in Your Home Directory

10-4.使用 pip 升级模块

问题

您需要更新系统上已经安装的软件包。

解决办法

pip 工具包括一个升级选项,该选项将检查 Pypi 存储库以查看是否有更新的版本。

它是如何工作的

要检查新版本,使用清单 10-9 中给出的命令。

pip install --upgrade numpy
Listing 10-9.Upgrading Packages

这种形式的更新选项对所有依赖项进行积极的更新。相反,您可以使用清单 10-10 中的命令进行“必要时升级”更新。

pip install --upgrade --no-deps numpy
pip install numpy
Listing 10-10.Doing a Selective Upgrade

如您所见,这实际上是一个两步过程,应该只更新那些需要更新的依赖项。

十一、数字和数值

Python 越来越多的应用领域之一是在科学界。一个问题,也一直是一个问题,就是 Python 在做数值计算的时候效率不是很高。幸运的是,Python 的设计旨在使扩展其功能变得相对容易。有助于科学计算的核心模块是Numpy模块。Numpy 将处理数字计算的最低效部分外包给用 c 编写的外部库,它使用的标准开源库与其他专门编写的应用中使用的相同,用于处理大量的数字计算。

Numpy 功能的核心是由一个叫做 array 的新对象提供的。数组是包含一种数据类型元素的多维对象。这意味着 Numpy 模块中的函数可以自由地假设可以对数据做什么,而不必在访问数据时检查每个元素。

11-1.创建数组

问题

您希望创建数组用于其他 Numpy 函数。

解决办法

创建数组最简单的方法是使用提供的creation函数获取列表中的现有数据,并将其转换成一个新的数组对象。也可以使用 empty 函数创建一个新的空数组对象。

它是如何工作的

最简单的形式的array函数简单地接受一个值列表并返回一个新的数组对象,如清单 11-1 所示。

>>> import numpy as np
>>> list1 = [1, 2, 3.0, 4]
>>> array1 = np.array(list1)
>>> array1
array([1., 2., 3., 4.])
Listing 11-1.Basic Array Creation

这将返回一个一维数组,其中每个元素都是一个实数。array函数的默认行为是选择保存原始列表中每个元素的最小数据类型。您可以专门选择想要在代码中使用的数据类型,比如清单 11-2 。

>>> complex1 = np.array(list1, dtype=complex)
>>> complex1
array([1.+0.j, 2.+0.j, 3.+0.j, 4.+0.j])
Listing 11-2.Creating an Array of Complex Numbers

如果您有一个需要处理的数据矩阵,您可以简单地提交一个列表列表,其中每个列表都是您的矩阵的一行,如清单 11-3 所示。

>>> matrix1 = np.array([[1, 2], [3, 4]])
>>> matrix1
array([[1, 2],
       [3, 4]])
Listing 11-3.Creating a Matrix

如果您还没有准备好数据,但是想要一个存储数据的地方,有一个函数可以创建一个固定大小和特定数据类型的空数组。例如,清单 11-4 展示了如何创建一个空的二维整数数组。

>>> empty1 = np.empty([2, 2], dtype=int)
>>> empty1
array([[-1073741821, -1067949133],
       [  496041986,    19249760]])
Listing 11-4.Creating an Empty Array of Integers

这个函数的问题是,它可能无法以任何方式初始化这些值,这取决于它所运行的操作系统。你最后得到的只是那些内存位置中存在的数据。虽然这稍微快了一点,但这确实意味着您需要意识到新数组中的初始值是垃圾数据。如果需要从一些初始值开始,可以从 0 或 1 开始,如清单 11-5 所示。

>>> zero1 = np.zeros((2, 3), dtype=float)
>>> zero1
array([[0., 0., 0.],
        [0., 0., 0.]])
>>> ones1 = np.ones((3, 2), dtype=int)
>>> ones1
array([[1, 1],
       [1, 1],
       [1, 1]])
Listing 11-5.Creating Arrays of Zeroes and Ones

请注意,对于新创建的数组的维度,这两个函数采用一系列值,而不是一个列表。

11-2.复制数组

问题

您需要复制一个数组以供进一步处理。

解决办法

在程序的不同部分共享数据有三种方式:无拷贝访问、浅层拷贝和深层拷贝。

它是如何工作的

通过一次使用多个变量,可以使程序的不同部分可以访问数组。在清单 11-6 中,您可以看到如何将同一个数组赋给两个不同的变量。

>>> a = np.ones((6,), dtype=int)
>>> a
array([1, 1, 1, 1, 1, 1])
>>> b = a
Listing 11-6.Using No-Copy Sharing

和 Python 的其他部分一样,这两个变量指向内存中的同一个实际对象。您可以使用其中任何一个来影响实际对象。

第二种类型的访问共享是通过浅层拷贝,其中不拷贝数据本身,只拷贝关于数据的信息。这是可能的,因为数组对象由两部分组成。第一个是存储在数组中的数据,而第二个包含关于数组的元数据,例如数组的形状。清单 11-7 展示了如何通过创建一个视图来创建一个浅层副本。

>>> view1 = ones1.view()
>>> # Do these variables point to the same object?
>>> view1 is ones1
False
>>> view1.base is ones1

True
Listing 11-7.
Shallow Copies

您可以通过使用新视图的base属性来访问原始对象。您可以通过视图更改元数据,如清单 11-8 所示。

>>> view1.shape = 2,3
>>> ones1
array([[1, 1],
       [1, 1],
       [1, 1]])
>>> view1
array([[1, 1, 1],
       [1, 1, 1]])
Listing 11-8.Changing the Shape of a View

这将改变存储数据的矩阵的形状(列数和行数)。第三种复制形式是深度复制,即复制数组的所有部分。这由copy方法处理,如清单 11-9 所示。

>>> copy1 = a.copy()
>>> a is copy1
False
>>> a is copy1.base
False
Listing 11-9.
Deep Copy
of an Array

11-3.访问数组数据

问题

您需要访问数组的单个元素或子部分。

解决办法

您可以使用多维列表索引来访问单个元素,并且可以使用切片来访问子部分。

它是如何工作的

对于一维数组,可以使用与列表相同的索引来访问单个元素。清单 11-10 显示了一个简单的例子。

>>> a[1] = 2
>>> a
array([1, 2, 1, 1, 1])
Listing 11-10.Changing the Value of an Array Element

切片也以同样的方式工作,如清单 11-11 所示。

>>> a[1:3]
array([2, 1])
Listing 11-11.Getting a Slice of an Array

需要注意的一点是,slice 实际上返回的是原始数组的一个浅层副本,因此不会复制原始数据。

当处理多维数组时,您只需通过为每个额外的维度添加一个额外的值来扩展索引。例如,清单 11-12 展示了如何从矩阵中获取单个元素。

>>> ones1[1, 1] = 2
>>> ones1
array([[1, 1],
       [1, 2],
       [1, 1]])
Listing 11-12.Accessing One Element from a Matrix

如果您对获取单个行感兴趣,您可以使用清单 11-13 中的例子。

>>> ones1[1, : ]
array([1, 2])
Listing 11-13.Selecting a Row from a Matrix

11-4.操作矩阵

问题

你需要操纵一个给定的矩阵。这包括反演、转置和计算范数。

解决办法

包含一整套线性代数工具来处理矩阵操作。

它是如何工作的

如果你从一个简单的 2 乘 2 矩阵开始,你可以用清单 11-14 中的代码转置它。

>>> a = np.array([[1.0, 2.0], [3.0, 4.0]])
>>> np.linalg.inv(a)
array([[-2., 1.],
       [1.5, -0.5]])
Listing 11-14.Inverting a Matrix

linalg子模块也提供了一个计算定额的函数,如清单 11-15 所示。

>>> np.linalg.norm(a)
5.4772255750516612
Listing 11-15.Finding a Norm

如果你想寻找一个矩阵的轨迹,这实际上是一个数组对象的方法,如清单 11-16

>>> a.trace()
5.0
Listing 11-16.Finding the Trace of a Matrix

矩阵的转置也是数组的一种方法,如清单 11-17 所示。

>>> a.transpose()
array([[1., 3.],
   [2., 4.]])
Listing 11-17.Finding the Transpose of a Matrix

11-5.计算快速傅立叶变换

问题

你需要计算一个快速傅立叶变换来观察一些数据集合的频谱。

解决办法

Numpy提供了一套不同类型的 FFT(快速傅立叶变换)函数。

它是如何工作的

离散 FFT 可用于一维、二维或 n 维数据。然而,每一种情况的数学方法都非常不同。所以Numpy为每种情况提供了独立的函数,如清单 11-18 所示。

# a is a 1-dimensional array
np.fft.fft(a)
# b is a 2-dimensional array
np.fft.fft2(b)
# c is a 3-dimensional array
np.fft.fftn(c)

Listing 11-18.Discrete FFTs

如你所见,所有的 FFT 功能实际上都被安排在一个名为fftNumpy子模块中。如果您使用的数据集大于所选 FFT 函数的适用范围,则使用最后 x 个轴。例如,如果您在一维 FFT 中使用数组c,它将使用最后一个轴作为计算的输入。如果你愿意,你可以用axis参数指定一个不同的轴,如清单 11-19 所示。

np.fft.fft(c, axis=1)
Listing 11-19.FFT Over Other Axes

11-6.将文件数据加载到数组中

问题

您希望将数据从文件直接加载到数组中。

解决办法

Numpy可以读写纯文本文件,以及自己特殊的二进制格式。

它是如何工作的

要从纯文本文件中读取数据,可以使用函数loadtxt(),如清单 11-20 所示。

>>> txt1 = np.loadtxt('mydata.txt')
Listing 11-20.Reading in a Text File

此函数假设您的数据以列和行的形式排列,其中每行就是一行。定义列是通过用其他字符分隔各个值来完成的。默认情况下,这是通过空白来完成的。科学数据的常用格式是逗号分隔值(CSV)。如果是这种情况,您可以用清单 11-21 中给出的代码加载您的数据。

>>> txt2 = np.loadtxt('mydata.txt', delimiter=',')
Listing 11-21.Loading a CSV File

如果你有以Numpy的特殊二进制格式保存的数据,你可以使用一个简单的load命令将其加载回内存,如清单 11-22 所示。

>>> data = np.load('mydata.npy')
Listing 11-22.Loading Binary Data

11-7.保存数组

问题

您希望将数组中的数据保存到磁盘。

解决办法

与加载数据一样,保存数据时有几个选项。您可以将其保存为Numpy的二进制格式,也可以保存为某种原始文本格式。

它是如何工作的

要使用Numpy的二进制格式保存数据,您可以简单地使用save函数,如清单 11-23 所示。

>>> np.save('mydata.npy', data)

Listing 11-23.Saving Data Using Numpy’s Binary Format

如果你在上面的调用中给它的文件名没有一个.npy文件扩展名,一个将被添加到它里面。相反,如果您想将数据保存到一个纯文本文件中,以便其他程序可以使用,您可以使用savetxt函数调用,如清单 11-24 所示。

>>> np.savetxt('mydata.csv', data, delimiter=',')
Listing 11-24.Saving a CSV File

在这种情况下,您显式地将分隔符设置为逗号,得到一个 CSV 文件。如果不设置分隔符,默认为单个空格字符。

11-8.生成随机数

问题

你需要生成高质量的随机数。

解决办法

Numpy提供一个 Mersenne Twister 伪随机数发生器,提供非常优质的随机数。它可以提供基于几种分布的随机数,如二项式分布、卡方分布、伽玛分布和指数分布。

它是如何工作的

如果您需要使用特定分布的随机数,您可以使用RandomState提供的方法来生成它们。清单 11-25 展示了如何从几何分布中生成一个随机值。

>>> rand1 = np.random.geometric(p=0.5)
Listing 11-25.Generating Random Numbers from a Geometric Distribution

大多数发生器都包含控制每个发行版细节的参数。它们通常还包含一个size参数,您可以用它来请求一组随机值,而不仅仅是一个。

如果您想要一个可重复的随机数序列(例如,如果您正在测试代码),您可以用清单 11-26 中的代码显式地设置一个种子。

>>> np.random.seed(42)

Listing 11-26.Setting a Seed for Random Number Generation

RandomState被创建时,这个种子也被初始化。如果你不提交一个,那么RandomState要么尝试从操作系统随机数生成器(例如,Linux 上的/dev/urandom)读取一个值,要么根据时钟设置种子。

在大多数情况下,您可以获得与清单 11-27 中的代码一起使用的随机数类型。

>>> rand2 = np.random.random()
Listing 11-27.Generating Random Numbers

11-9.计算基本统计数据

问题

您需要对存储在数组中的数据进行基本的统计。

解决办法

Numpy提供了一系列统计函数,可以对不同维度的数组进行操作。你可以做你可能需要的所有标准的简单统计分析。

它是如何工作的

给定一组存储在一维数组中的数据,您可以用清单 11-28 中的代码找到平均值、中值、方差和标准差。

>>> a = np.array([1, 2, 3, 4, 5])
>>> np.mean(a)
3.0
>>> np.median(a)
3.0
>>> np.var(a)
2.0
>>> np.std(a)
1.4142135623730951
Listing 11-28.Basic Statistics

如果您有多维数据,您可以选择沿着哪个轴计算这些统计数据。

11-10.计算直方图

问题

您有一系列的数据,您需要将这些数据分组并计算直方图。

解决办法

Numpy包含一些相关的函数来处理直方图,包括一维直方图和多维直方图。

它是如何工作的

假设您已经将数据序列存储在变量b中,您可以使用清单 11-29 中的代码生成一个直方图。

>>> b = np.array([1,2,1,2,3,1,2,3,3,2,1])
>>> np.histogram(b)
(array([4, 0, 0, 0, 0, 4, 0, 0, 0, 3], dtype=int64),
 array([ 1\. ,  1.2,  1.4,  1.6,  1.8,  2\. ,  2.2,  2.4,  2.6,  2.8,  3\. ]))
Listing 11-29.Generating a Simple Histogram

默认情况下,Numpy会尝试将您的数据分组到 10 个箱中。第一个数组告诉您每个容器中有多少个值,第二个数组告诉您每个容器的边界。您可以通过添加第二个参数来设置箱子的数量,如清单 11-30 所示。

>>> np.histogram(b, 3)
(array([4, 4, 3], dtype=int64),
 array([ 1\. , 1.66666667, 2.33333333, 3\. ]))
Listing 11-30.Histograms with a Set Bin Count

十二、并发

几十年来,计算机的速度越来越快,但我们开始遇到一些物理学的限制。这意味着为了完成更多的工作,我们需要并行使用多个过程。Python 中有几种技术可以支持代码的并发执行。

第一种技术是使用线程来分解工作。这种方法的主要问题是,它受到由 GIL(全局解释器锁)引起的瓶颈的影响。执行 I/O 或使用特定模块(如 numpy)的线程可以绕过这个瓶颈。如果你需要做更多的计算工作,你可以使用进程来代替。在本章中,你将看到 Python 中的几个可用选项。

12-1.创建线程

问题

你想创建一个线程来完成一些任务。

解决办法

Python 标准库包含一个名为threading的模块,该模块包含一个Thread类。

它是如何工作的

主类Thread支持并行运行多个函数。清单 12-1 展示了如何创建和运行一个基本线程。

import threading
def print_sum():
    print('The sum of 1 and 2 is 3')
my_thread = threading.Thread(target=print_sum)
my_thread.start()
Listing 12-1.Creating a Thread

您应该注意到,您创建的线程在您调用start()方法之前不会开始执行目标函数。如果这个函数是一个运行时间更长的函数,您可以通过使用is_alive()方法来检查它是否还在运行。它将返回一个布尔值,告诉你它是否还在运行。如果没有给定线程的结果就无法继续,可以调用join()方法强制等待,直到线程全部完成。

12-2.使用锁

问题

您需要控制线程对特定资源的访问。

解决办法

threading模块包括一个Lock类来控制线程访问。

它是如何工作的

当线程需要安全地访问全局资源时,使用锁。清单 12-2 展示了如何创建和使用一个锁对象。

import threading
sum = 0
my_lock = threading.Lock()
def adder():
    global sum, my_lock
    my_lock.acquire()
    sum = sum + 1
    my_lock.release()
my_thread = threading.thread(target=adder)
my_thread.start()
Listing 12-2.Creating a Lock Object

默认情况下,如果锁已经被另一个线程获取,那么锁对象的acquire()方法就会阻塞。相反,如果你想在等待锁的时候做些别的事情,你可以使用acquire()方法中的参数blocking=False。它将立即返回,给出一个布尔值,表明获取尝试是否成功。

12-3.设置障碍

问题

您需要通过设置一个公共停止点来同步线程活动。

解决办法

threading模块包括一个可用于设置公共停止点的障碍对象。

它是如何工作的

在许多语言中,使用屏障涉及简单的函数调用,而在 Python 中,屏障是用对象来管理的。清单 12-3 展示了如何为五个线程创建一个屏障。

import threading
b = threading.Barrier(5, timeout=10)
Listing 12-3.Creating a Barrier Object

正如您所看到的,您必须明确地告诉 barrier 对象有多少线程将使用它。您还可以设置一个超时值,该值是允许线程等待屏障得到满足的最长时间。为了实际使用 barrier 对象,每个线程都需要调用wait()方法。

12-4.创建流程

问题

您需要为多重处理创建多个进程。

解决办法

Python 标准库包括一个名为multiprocessing的模块,该模块包含一个Process类。

它是如何工作的

如果您的代码受到 GIL 的影响,一种解决方法是使用类Process在主 Python 进程之外生成其他任务。该接口与线程的接口非常相似。例如,清单 12-4 展示了如何创建一个新流程并开始运行。

import multiprocessing
def adder(a, b):
    return a+b
proc1 = multiprocessing.Process(target=adder, args=(2,2))
proc1.start()
proc1.join()
Listing 12-4.Creating a New Process

正如您所看到的,您新创建的流程对象是用start()方法执行的,您可以用join()方法强制代码的主要部分等待结果。

12-5.进程间的通信

问题

您需要将信息从一个流程对象发送到另一个流程对象。

解决办法

multiprocessing模块有两个可用于进程间通信的类:pipequeue类。

它是如何工作的

因为流程对象在 Python 解释器的主要部分之外执行,所以与它们或它们之间的通信需要更多的工作。最基本的通信形式是管道。清单 12-5 展示了如何创建一个新的管道对象。

import multiprocessing
def func(pipe_end):
    pipe_end.send(['hello', 'world'])
    pipe_end.close()
parent_end, child_end = multiprocessing.Pipe()
proc1 = multiprocessing.Process(target=func, args=(child_end,))
proc1.start()
print(parent_end.recv())
proc1.join()
Listing 12-5.Creating a Pipe

如您所见,管道是一个简单的通信通道,有两端,进程可以从中读取和写入。管道是全双工的,所以消息可以从两端发送。然而,管道的主要问题是末端一次只能被一个进程使用。如果两个进程试图同时从同一端读取或写入,数据可能会损坏。

另一种不同的技术是使用队列对象与。队列是一个 FIFO(先进先出)对象。它可以接受来自多个进程的数据,并且多个进程可以从队列中取出数据。清单 12-6 展示了如何创建和使用队列对象。

import multiprocessing
def func(queue1):
   queue1.put(['hello', 'world'])
my_queue = multiprocessing.Queue()
proc1 = multiprocessing.Process(target=func, args=(my_queue,))
proc1.start()
print(my_queue.get())
proc1.join()
Listing 12-6.Creating a Queue

12-6.创造一批工人

问题

您需要启动并运行一个进程池。

解决办法

Python 标准库模块multiprocessing包含一个Pool类来管理任务队列。

它是如何工作的

当您有一系列需要处理的任务时,您可以创建一个进程池来完成这些任务。清单 12-7 展示了如何创建一个由四个工作进程组成的池,并让它们处理一堆任务。

import multiprocessing
def adder(a):
    return a+a
pool1 = multiprocessing.Pool(processes=4)
Listing 12-7.Creating a Pool of Processes

这个新创建的池对象有几种不同的方法来在进程之间划分任务。这些方法有阻塞版本和非阻塞版本。例如,清单 12-8 展示了如何使用map方法。

# This method blocks untill all processes return
pool1.map(adder, range(10))
# This method returns a result object
results = pool1.map_async(adder, range(10))
results.wait()
Listing 12-8.Mapping a Function to a Pool of Processes

清单 12-8 中的最后一行用于阻塞和等待,直到返回所有结果。当您准备使用外包任务的所有结果时,可以使用它。

12-7.创建子流程

问题

您需要生成一个子流程来处理外部任务。

解决办法

Python 标准库包含一个名为subprocess的模块,可以生成外部进程。

它是如何工作的

subprocess模块旨在取代运行外部流程的旧的os.systemos.spawn方法。该模块包含一个名为run()的方法,这是使用子流程的常用方法。例如,清单 12-9 展示了如何在 Linux 机器或 Mac OS 机器上获得当前目录下的文件列表。

import subprocess
results = subprocess.run(['ls', '-l'], stdout=subprocess.PIPE)
print(results.stdout)
Listing 12-9.Spawning a Subprocess

返回的 results 对象包含许多关于外部进程如何运行的信息,包括退出代码。

12-8.安排事件

问题

您需要为以后的某个时间安排任务。

解决办法

Python 标准库包含一个名为sched的模块,其中有几个对象和方法,在不同时间调度工作时非常有用。

它是如何工作的

为了安排未来的任务,您可以创建一个Scheduler对象来获取事件队列并管理它们。您可以使用enter()方法将事件添加到队列中,如清单 12-10 所示。

import sched, time
def print_time():
    print(time.time())
my_sched = sched.scheduler()
my_sched.enter(10, 1, print_time)
Listing 12-10.Creating a Scheduler

当事件触发时,enter()方法接受一个延迟、一个优先级和一个要执行的函数。如果您想在特定时间触发事件,可以使用enterabs()方法。一旦您有了一个更大的事件列表,您就可以使用调度器的queue属性来检查当前队列。如果您到达了程序的末尾,您可以使用run()方法强制代码停止并等待,直到所有事件都结束。

十三、工具

所有编程语言都使用外部工具,以使程序员的开发和执行更容易。Python 在这方面也没有什么不同。在这一章中,你将会看到一些为 Python 编程语言编写代码时最常用的外部工具。这些包括设置环境的工具、更好的解释器、完整的编码环境,甚至是外壳替换。

13-1.创建虚拟环境

问题

您希望为特定的 Python 程序创建和使用虚拟环境。

解决办法

创建和管理虚拟环境的常用方法是通过virtualenv工具。

它是如何工作的

Python 的优势之一,它对第三方模块非常健康的选择,也是它的问题之一。你很快就会得到一个大规模的模块库,这些模块只在某些时候使用。最小化这个问题的一个方法是为每个项目建立一个单独的环境,这样你就可以在这个环境中只安装你所需要的模块。第一步,安装virtualenv。清单 13-1 展示了如何用 pip 来做这件事。

pip install virtualenv
Listing 13-1.Installing virtualenv

安装后,您可以创建新的虚拟环境。清单 13-2 展示了如何为一个新项目创建一个新的空白环境。

virtualenv project1
Listing 13-2.Creating a New Virtual Environment

这个命令创建一个名为project1的新目录,并安装运行 Python 程序所需的一切,以及安装和管理模块。为了使用这个新环境,您将目录更改为project1子目录,并获取 shell 脚本./bin/activate。许多最常用的 Linux shells 都有不同的版本,还有 Windows cmd shell 和 Windows powershell。这个脚本设置了几个环境变量来使用包含的解释器和模块库。

当您使用完有问题的环境后,您可以运行脚本deactivate来撤销环境更改。如果您决定不再需要某个特定的虚拟环境,只需删除整个相关目录及其所有内容。完整文档可在 https://virtualenv.pypa.io/en/stable/ 获得。

13-2.使用 Ipython Shell

问题

您希望使用更好的解释器外壳,而不是 Python 解释器使用的默认外壳。

解决办法

IPython 解释器外壳在默认的 Python 解释器外壳上提供了许多增强。

它是如何工作的

您可以使用清单 13-3 中所示的命令安装最新版本的 IPython。

pip install ipython
Listing 13-3.Installing IPython

要使用这个解释器,您只需要在命令提示符下执行命令ipython。它与许多 Linux shells 非常相似,因为它提供了制表符补全、命令历史和更复杂的命令编辑等功能。

IPython 解释器中更强大的功能之一是称为 magics 的命令集。这些特殊的命令以一个%符号开始,后面跟着一些关键字。例如,清单 13-4 展示了如何为一个给定的操作计时。

%timeit x = range(1000000)
Listing 13-4.Using the timeit Magic Function

有些魔术可以运行外部脚本,加载或保存命令,甚至影响 IPython 解释器的颜色。清单 13-5 展示了如何将之前的一系列命令保存到一个文件中。

# Saving a series of commands to a file
%save myfile.py 2-5 10
Listing 13-5.Saving to a File with Ipython Magics

这样,您可以从一次实验中保存有用的行。load魔法函数将文件的内容读入 IPython 前端,就好像您刚刚输入了它们一样。清单 13-6 展示了如果你加载一个只有一条print语句的文件会发生什么。

In [7]: %load myfile.py

In [8]: print("Hello world")

Listing 13-6.Loading a File with IPython Magic

您还可以创建自己的神奇函数,进一步扩展 IPython 的功能。主要文档可在 http://ipython.readthedocs.io/en/stable/index.html 找到。

13-3.使用 Jupyter 环境

问题

您希望使用更完整的环境,以便于交互式开发。

解决办法

你可以使用 Jupyter,它是 IPython 的 web 接口的一个分支。它提供了一个类似于 Maple、Mathematica 或 Matlab 等商业应用的接口。

它是如何工作的

第一步是在您的系统上安装 Jupyter。清单 13-7 展示了如何使用 pip 来完成这项工作。

pip install jupyter
Listing 13-7.Installing Jupyter

在命令提示符下,执行命令jupyter notebook将启动 web 服务器和 Python 引擎,然后启动连接到新启动的服务器的 web 浏览器。Jupyter 正迅速成为用 Python 进行科学研究的事实平台。除了继承自 IPython 的增强功能,Jupyter 还包括绘制内嵌 matplotlib 图形等功能。作为 Jupyter 的一部分,实际上还有一套非常完整的其他工具,例如笔记本查看器和笔记本评分工具,供您在课堂环境中使用 Jupyter 时使用。主文档位于 http://jupyter.readthedocs.io/en/latest/index.html

13-4.使用 xonsh 作为替换外壳

问题

Linux 或 Mac OS 上的命令 shell 是非常个人化的选择。它是您用来与机器上的一切进行交互的环境。您可能希望使用 Python 作为在计算机上工作的命令 shell。

解决办法

您可以使用名为xonsh的新项目作为替换命令 shell。

它是如何工作的

可以用 pip 安装xonsh,如清单 13-8 所示。

pip install xonsh
Listing 13-8.Installing xonsh

一旦安装完毕,xonsh就可以像其他命令 shell 一样使用。xonsh至少部分兼容 bash。这是指运行其他程序、拥有和使用命令历史记录以及处理后台作业的能力。除了这种常见的功能,您还拥有 Python 引擎的所有功能。这意味着您可以使用 Python 模块在命令 shell 中添加功能。清单 13-9 展示了如何从命令行直接获得随机数。

jbernard@DESKTOP-QPKN2QC ∼ <branch-timeout> $ import numpy as np
jbernard@DESKTOP-QPKN2QC ∼ <branch-timeout> $ np.random.random()
0.48053753953641054
Listing 13-9.Using Python Modules within xonsh

主文档站点位于 http://xon.sh/index.html