Basics of Python programming (with Pycharm and development environment)

1. PyCharm integrated development environment

1、Python HelloWorld

python source program:

  • python is a text file that stores python code;
  • The extension must end with .py;

Walkthrough steps:

  • Find a directory c:\file in your computer;
  • Created a new text file hello.py in the file directory;
  • Open this file with Notepad, if the following content;
print("hello world")
print("hello python")
  • Save and exit;
  • open cmd;

  • Enter python hello.py on the command line;

 

2. Integrated development environment

The integrated development environment (IDE, Integrated Development Environment) integrates all the tools needed to develop software, generally including the following tools:

  • GUI;
  • Code editor (supports code completion/auto indentation);
  • compiler/interpreter;
  • Debugger (breakpoint/stepping;

3. Introduction to PyCharm

PyCharm is a very good integrated development environment for Python.

In addition to the necessary functions of general IDE, PyCharm can also be used under Windows, Linux, and macos.

PyCharm is suitable for developing large-scale projects. A project usually contains many source files. The number of lines of code in each source file is limited, usually within a few hundred lines. Each source file performs its own duties and completes complex business functions together. .

4. Run the python program in PyCharm

There are two ways to run python:

  • The command line runs through python python source program;
  • Run directly in pycharm;

1. The first pycharm program

Start pycharm:

Select create new project in the startup interface.

You can create a new project through the welcome interface or the menu File/New Project. 

It is recommended to use only lowercase letters, numbers and underscores when naming the file name, and the file name cannot start with a number.

2.  PyCharm program debugging

The file navigation area can browse/locate/open project files;

The file editing area can edit the currently opened file;

The console area can: output program execution content;

Trace the execution of debug code;

The toolbar in the upper right corner can execute (SHIFT+F10)/debug (SHIFT+F9) code;

You can step through the code with the step button (F8) above the console.

3. Set the interpreter version used by the project

If the opened directory is not the project directory created by PyCharm, sometimes the interpreter version used is Python 2x, and the interpreter version needs to be set separately.

The settings window can be opened through File/Settings..., as shown in the figure below.

4. Precautions for running multiple py files in pycharm

A development project is to develop a software that specifically solves a complex business function.

Usually each project has an independent and exclusive directory for saving all project-related files, and a project usually contains many source files.

1. Create a new file world.py in the 3-code project

2. Add a print("hello world") to the world.py file

3. Right click and select Run'world'

hint:

In PyCharm, if you want to make any Python program executable, you must first execute it through the right mouse button.

For commercial projects, usually in a project, there is only one Python source file that can be directly executed.

5. Install the packages required for the course in pycharm

2. Basics of Python

1. Indentation and code block construction

Unlike most other programming languages, Python uses whitespace and indentation to identify code blocks. In other words, the composition of the loop body and the else conditional clause are all determined by blank characters.

Most programming languages ​​use some kind of curly braces to denote blocks of code. The following C code will calculate the factorial of 9 and store the result in the variable r:

/* C语言代码  */
int n, r;
n = 9;
r = 1;
while (n > 0) {
    r *= n;
    n--;
}

The body of the while loop here is surrounded by curly braces, which is the code that will be executed each time the loop is executed. As shown in the above code, in order to clearly express the purpose, the code is generally indented to some extent.

But writing in the following format is also allowed:

/* 随意缩进的C语言代码 */
    int n, r;
        n = 9;
        r = 1;
    while (n > 0) {
r *=  n;
n--;
}

Although the above code is very difficult to read, it still works correctly.

Here is the equivalent implementation in Python:

# Python代码(赞!)
n = 9
r = 1
while n > 0:
    r = r * n     ⇽---  Python还支持C风格的写法r * = n
    n = n – 1     ⇽---  Python还支持C风格的写法n - = 1

Python does not use curly braces to identify code structure, but indentation itself. The last two lines of code above are the body of the while loop because they follow the while statement and are indented one level below the while statement. If these two lines of code are not indented, the body of the while loop will not be formed.

Using indentation instead of curly braces to denote code structure may take some getting used to, but there are clear benefits.

  • It is no longer possible to have missing or redundant braces. No more digging through your code over and over just to find the closing parenthesis at the bottom that matches the previous opening parenthesis.
  • The appearance of the code structure intuitively reflects its actual structure, and it is easy to understand the structure of the code at a glance.
  • The coding style of Python can be roughly unified. In other words, it's less likely to drive yourself mad trying to understand someone else's wacky code. Everyone's code looks like they wrote it themselves.

Maybe everyone's code already insists on indentation, so this is not a big improvement. If IDLE is used, each line is automatically indented. If you want to go back to the indentation level, just press the Backspace key. Most editors and IDEs for programming (such as Emacs, VIM, and Eclipse) provide automatic indentation. If a command is entered at the prompt with one or more spaces preceding it, the Python interpreter returns an error message. This one might take a mistake or two to get used to.

2. Identify annotations

In most cases, anything after the symbol # in a Python file is a comment and will be ignored by the compiler. With one notable exception, the # in the string is just an ordinary character:

# 将5赋给x
x = 5
x = 3         # 现在x成了3
x = "# This is not a comment"

Comments are often added to Python code.

3. Variables and assignments

Assignment is the most commonly used Python command, and its usage is similar to other programming languages. The following uses Python code to create a new variable x and assign it a value of 5:

x = 5

Unlike many other computer languages, Python does not need to declare variable types, nor does it need to add terminators after each line of code. A new line of code means the end, and the variable is automatically created when it is assigned a value for the first time.

Variables in Python: Is it a bucket or a label?

The name "variable" in Python may be a bit misleading, it should be more accurate to call it "name" or "label". However, it seems that everyone is used to calling it a "variable". Regardless of the name, you should know how variables in Python work.

A common interpretation of a variable is a container for storing values, sort of like a bucket, although that's not precise. For many programming languages ​​(such as C language), this interpretation is reasonable.

However, variables in Python are not containers, but labels that point to Python objects, which reside in the interpreter's namespace. Any number of labels (or variables) can refer to the same object. When an object changes, the values ​​of all variables pointing to it change.

After reading the following simple code, you can understand the meaning of the above:

>>> a = [1, 2, 3]
>>> b = a
>>> c = b
>>> b[1] = 5
>>> print(a, b, c)
[1, 5, 3] [1, 5, 3] [1, 5, 3]

If you think of variables as containers, the above results don't make sense. Changing the contents of one container should not change the other two containers at the same time. But it makes sense if the variable is just a label pointing to an object. All three tags point to the same object, and if the object changes, all three tags will reflect it.

If the variable refers to a constant or immutable value, the above distinction is not so obvious:

>>> a = 1
>>> b = a
>>> c = b
>>> b = 5
>>> print(a, b, c)
1 5 1

Because the object the variable points to cannot be changed, the behavior of the variable is consistent with both interpretations. In fact, after the third line of code is executed, a, b, and c all point to the same unchangeable integer object with a value of 1. The next line of code b = 5 makes b point to the integer object 5, but the points of a and c remain unchanged.

Python variables can be set to any object, whereas in C and many other languages, variables can only store values ​​of the declared type. The following Python code is perfectly legal:

>>> x = "Hello"
>>> print(x)
Hello
>>> x = 5
>>> print(x)
5

 At first x points to the string object "Hello", and then points to the integer object 5. Of course, this feature can be abused, because arbitrarily letting the same variable name point to different data types one after another may make the code difficult to understand.

The new assignment operation will overwrite all previous assignments, and the del statement will delete the variable. If you try to output the contents of a variable after deleting it, an error will be thrown, as if the variable was never created:

>>> x = 5
>>> print(x)
5
>>> del x
>>> print(x)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
>>>

Here first there is the traceback, which is printed when an error (called an exception) is detected. The last line of code shows that an exception was detected, in this case a NameError for x. After being deleted, x is no longer a valid variable name. Because only one line of code is output in interactive mode, only trace information for "line 1, in <module>" is returned in the above example. Usually when an error occurs, the complete dynamic call hierarchy information of the existing function is returned. If IDLE is used, the information returned is similar, and the code shown below may be obtained:

Traceback (most recent call last):
  File "<pyshell#3>", line 1, in <module>
    print(x)
NameError: name 'x' is not defined

In the Python standard library documentation, there is a list of all possible exceptions and their causes. Please use the index to find information about an exception received (such as a NameError).

Python variable names are case-sensitive and can contain letters, numbers, and underscores, but must begin with a letter or underscore.

4. Expression

Everyone is familiar with the arithmetic expressions supported by Python. The following code will calculate the average of 3 and 5 and store the result in the variable z:

x = 3
y = 5
z = (x + y) / 2

Note that arithmetic operators involving only integers do not necessarily return integers. Even if all values ​​are integers, division (as of Python 3) returns floats, so decimals are not truncated. If traditional integer divisions that return truncated integers are desired, the operator // can be used instead.

Python uses standard arithmetic precedence rules. If the last line of code above had omitted the parentheses, it would have been calculated as x + (y / 2).

Expressions don't have to just contain numbers; strings, booleans, and many other types of objects can be used in expressions in a variety of ways.

5. String

Like most other programming languages, Python identifies strings with double quotes. The following code assigns the string "Hello, World" to the variable x:

x ="Hello, World"

Backslashes can be used to escape characters, giving them special meaning. \n represents a newline character, \t represents a tab character, and \\ represents the backslash character itself. And \" is the double quote itself, not the end of the string:

x = "\tThis string starts with a \"tab\"."
x = "This string contains a single backslash(\\)."

Single quotes can be used instead of double quotes. The following two lines of code have the same effect:

x = "Hello, World"
x = 'Hello, World'

The only difference between them is that in a single-quoted string, there is no need to add a backslash to the double-quote character; in a double-quoted string, there is no need to add a backslash to the single-quote character:

x = "Don't need a backslash"
x = 'Can\'t get by without a backslash'
x = "Backslash your \" character!"
x = 'You can leave the " alone'

Ordinary strings do not allow spanning lines.

The following code will be invalid:

# 以下Python代码将引发错误——不能让1个字符串跨越2行
x = "This is a misguided attempt to
put a newline into a string without using backslash-n"

But Python supports the use of triple double quotes to identify strings, so that the string can contain single quotes and double quotes without backslashes:

x = """Starting and ending a string with triple " characters 
permits embedded newlines、and the use of " and ' without 
backslashes"""

Now x contains all characters between two """. You can use triple single quotes ''' instead of double quotes to achieve the same effect.

6. Value

Python provides 4 kinds of values: integers, floating point numbers, complex numbers and Boolean values. Integer constants are integer values ​​such as 0, -11, +33, 123456, and the range is unlimited, and the size is only limited by machine resources. Floating-point numbers can be expressed with a decimal point or scientific notation: 3.14, -2E-8, 2.718281828. The precision of floating-point numbers is determined by the underlying hardware, but is generally equivalent to the double-precision (64-bit) type in C. Complex numbers probably don't get as much attention, and booleans are True or False, which have the same effect as 1 and 0, except in string form.

Arithmetic operations in Python are very similar to those in C. Computational operations on two integers produce an integer, except for division (/), which results in a floating-point number. If the division sign // is used, the result will be a truncated integer. Floating-point operations always return floating-point numbers.

Here are some examples:

>>> 5 + 2 - 3 * 2
1
>>> 5 / 2          # 普通除法将返回浮点数
2.5
>>> 5 / 2.0        # 结果还是浮点数
2.5
>>> 5 // 2         # 用'//'整除将返回截断后的整数值
2
>>> 30000000000    # 在很多编程语言中,整型是放不下的
30000000000
>>> 30000000000 * 3
90000000000
>>> 30000000000 * 3.0
90000000000.0
>>> 2.0e-8         # 科学计数法将返回浮点数
2e-08
>>> 3000000 * 3000000 
9000000000000
>>> int(200.2)     ⇽---  ❶
200
>>> int(2e2)       ⇽---  ❶
200
>>> float(200)     ⇽---  ❶
200.0

These few lines of code explicitly convert between multiple types, and the int function will truncate the floating point number.

Python's numeric values ​​have two advantages over C or Java: integers can be of any size, and the result of dividing two integers is a floating-point number.

1. Built-in numerical processing functions

Python provides the following numeric manipulation functions as part of its core:

abs、divmod、float、hex、int、max、min、oct、pow、round

2. Advanced Numerical Processing Functions

Python does not have built-in functions for more advanced numerical processing, such as trigonometric functions, hyperbolic trigonometric functions, and some useful constants, but they are all provided in the standard module math.

For now it is sufficient to know that the following statements must be executed in a Python program or in an interactive session to use mathematical functions.

from math import *

The math module provides the following functions and constants:

acos、asin、atan、atan2、ceil、cos、cosh、e、exp、fabs、floor、fmod、frexp、hypot、ldexp、
log、log10、mod、pi、pow、sin、sinh、sqrt、tan、tanh

3. Numerical calculation

Due to limited computing speed, the basic installation of Python is not suitable for performing intensive numerical calculations. But the powerful Python extension NumPy efficiently implements many advanced numerical processing operations. NumPy focuses on array operations, including multidimensional matrices, and more advanced functions such as fast Fourier transform.

You should be able to find NumPy or a link to it on the official SciPy website.

4. Plural

Complex numbers are automatically created whenever an expression has the form nj: n is the same as Python's integer and floating-point form, and j is of course standard imaginary notation, equal to the square root of -1.

For example:

>>> (3+2j)
(3+2j)

Note that when the calculation result is a complex number, Python will add parentheses, indicating that an object value is displayed:

>>> 3 + 2j - (4+4j)
(-1-2j)
>>> (1+2j) * (3+4j)
(-5+10j)
>>> 1j * 1j
(-1+0j)

Computing j * j returns -1 as expected, but the result is still a Python complex object. Complex numbers are never automatically converted to equivalent real numbers or integer objects. But you can easily access the real and imaginary parts of complex numbers with the real and imag attributes.

>>> z = (3+5j)
>>> z.real
3.0
>>> z.imag
5.0

Note that the real and imaginary parts of complex numbers are always returned as floating point numbers.

5. Advanced Complex Number Functions

Most users will think that the calculation of the square root of -1 should not have a result, but should report an error, so the functions in the math module are not suitable for complex numbers. Similar complex number functions are provided by the cmath module:

acos、acosh、asin、asinh、atan、atanh、cos、cosh、e、exp、log、log10、pi、sin、sinh、sqrt、
tan、tanh

In order to clearly identify these special-purpose plural functions in the code and avoid conflicts with ordinary functions of the same name, it is best to import the cmath module first:

import cmath

Then when using the complex number function, explicitly reference the cmath package:

>>> import cmath
>>> cmath.sqrt(-1)
1j

Use less <module> import *

The above example is a good example of why the use of from <module> import * in import statements should be minimized. If you use the from form to import the math module first, and then import the cmath module, then the functions in the cmath module will overwrite the functions with the same name in the math module. It also takes more effort for someone reading the code to figure out where a certain function comes from. There are some modules that are explicitly designed to have to use the import form in the example above.

The important thing to remember is that after importing the cmath module, you can do almost anything with other numeric types.

7. None value

In addition to standard types such as strings and numbers, Python also has a special basic data type that defines a special data object called None. As the name suggests, None is used to represent a null value. In Python, None exists in various ways. For example, a "procedure" in Python is just a function without an explicit return value, which means that it returns None by default.

In everyday Python programming, None is often used as a placeholder to indicate that data at a certain position in a data structure will be meaningful, even if the data has not yet been calculated. It is very simple to detect whether None exists, because there is only one instance of None in the entire Python system, all references to None point to the same object, and None is only equivalent to itself.

8. Get user input

Use the input() function to get user input. The input() function can take a string parameter as a prompt message displayed to the user:

>>> name = input("Name? ")
Name? Jane
>>> print(name)
Jane
>>> age = int(input("Age? "))     ⇽---  将输入的字符串转换为整数
Age? 28
>>> print(age)
28
>>>

This method of getting user input is fairly simple. User input is obtained as a string, so to be used as a number, it must be converted with the int() or float() functions. This can be regarded as a small trap.

9. Built-in operators

Python provides a variety of built-in operators. The standard operators include +, *, etc., and the more advanced ones include shift and bitwise logical operation functions, etc. Most operators are not unique to Python, other programming languages ​​also provide, a complete list of Python built-in operators, can be found in the official documentation.

10. Basic Python coding style

Python places relatively few restrictions on coding style, aside from explicitly requiring indentation to identify blocks of code. Even so, the amount of indentation and the type of indentation (tabs vs. spaces) are not enforced. However, in "Python Enhancement Proposal 8" (Python Enhancement Proposal 8, PEP 8), it contains recommended coding style guidelines

Some Pythonic style specifications are listed in the table below, but in order to fully understand the Pythonic style, please read PEP 8 repeatedly.

Python-style coding style guidelines:

Scenes suggestion example
module/package name Short, all lowercase, without underscores if not necessary imp、sys
Function name All lowercase, with underscores for readability foo()、my_func()
variable name All lowercase, with underscores for readability my_var
class name word capitalization MyClass
constant name All caps, separated by underscores PI、TAX_RATE
indentation Each level has a difference of 4 spaces, no Tab key
comparison operation Do not compare with True or False values if my_var:if not my_var:

It is strongly recommended to follow the PEP 8 specification. Because each specification is carefully selected and has passed the test of time, it can make the code easier for Python programmers to understand.

3. Python data types

Python sequence types: list (list) and tuple (tuple). At the beginning, you may compare lists with arrays in other languages, but please don't be confused, the functions of lists are more flexible and powerful than ordinary arrays.

Tuples are like unmodifiable lists and can be thought of as restricted list types or record types. Collections are useful when an object's membership in the collection (rather than its position) is important.

1. String

1. Understand a string as a sequence of characters

If you want to extract characters or substrings, you can think of a string as a sequence of characters, which means you can use the indexing and slicing syntax:

>>> x = "Hello"
>>> x[0]
'H'
>>> x[-1]
'o'
>>> x[1:]
'ello'

There is a string slicing usage that strips off newlines at the end of the string, usually the line of text just read from the file:

>>> x = "Goodbye\n"
>>> x = x[:-1]
>>> x
'Goodbye'

The above code is just an example. You should know that Python has other better string methods for removing unwanted characters. But this example illustrates the usefulness of slices.

You can also use the len function to determine the number of characters in a string, similar to getting the number of elements in a list:

>>> len("Goodbye")
7

But strings are not lists of characters. The most obvious difference between strings and lists is that strings cannot be modified. If you try something like string.append('c') or string[0] = 'H', an error will be raised. In the above example of removing the newline character in the string, a new slice of the original string is created instead of directly modifying the original string. This is a fundamental limitation of Python, intended for efficiency.

2. Basic string manipulation

The easiest (and probably most common) way to concatenate Python strings is to use the string concatenation operator "+":

>>> x = "Hello " + "World"
>>> x
'Hello World'

Python also has a similar operator for string multiplication, which can be useful sometimes, but not often:

>>> 8 * "x"
'xxxxxxxx'

3. Special characters and escape sequences

When using strings above, there have been some character sequences treated specially by Python: \n represents a newline character, and \t represents a tab character. A sequence of characters that begins with a backslash and is used to represent other characters is called an escape sequence.

Escape sequences are often used to represent special characters that do not have a standard single-character printable format (such as tabs and newlines).

1) Basic escape sequences

Python gives a list of two-character escape sequences used in strings, as shown in the following table.

escape sequence Represented characters
\' apostrophe
\" Double quotes
\a ringing symbol
\b backspace
\f Form feed
\n line break
\r Carriage return (different from \n)
\t Tab (Tab)
\v vertical tab

These escape sequences also apply to bytes objects.

The ASCII character set is the character set used by Python, and it is also the standard character set used by almost all computers. A considerable number of special characters are defined in the ASCII character set, and these special characters can be obtained through the escape sequence in the number format.

2) Numeric format (octal, hexadecimal) and Unicode-encoded escape sequences

Within a string, any ASCII character can be included with the octal or hexadecimal escape sequence corresponding to the ASCII character. An octal escape sequence is a backslash followed by a 3-digit octal number, and the ASCII character corresponding to this octal number will be replaced by the octal escape sequence. Hexadecimal escape sequences are not prefixed with "\", but with "\x", followed by any number of hexadecimal digits. If a character that is not a hexadecimal digit is encountered, it is considered the end of the escape sequence.

For example, in the ASCII character table, the character "m" converts to a decimal value of 109, converts to an octal value of 155, and converts to a hexadecimal value of 6D, so:

>>> 'm'
'm'
>>> '\155'
'm'
>>> '\x6D'
'm'

The above expressions all evaluate the string containing the single character "m". This form can also be used to represent characters that have no printable format.

For example, for the newline character "\n", the octal value is 012 and the hexadecimal value is 0A:

>>> '\n'
'\n'
>>> '\012'
'\n'
>>> '\x0A'
'\n'

Strings in Python 3 are Unicode strings, so they can contain almost all characters in all languages. Any Unicode character can be escaped, either in numeric form (as shown before) or by Unicode name:

>>> unicode_a ='\N{LATIN SMALL LETTER A}'     ⇽---  用Unicode名称转义
>>> unicode_a
'a'                              ⇽---  ❶
>>> unicode_a_with_acute = '\N{LATIN SMALL LETTER A WITH ACUTE}'
>>> unicode_a_with_acute
'á'
>>> "\u00E1"              ⇽---  用数字格式转义,前缀为\u
'á'
>>>

The Unicode character set includes ordinary ASCII characters ❶.

3) Differences in printing and evaluating strings with special characters

As discussed earlier, there is a difference between evaluating a Python expression in an interactive environment and using the print function to print the result of the same expression. Although the contained strings are the same, the two operations may produce seemingly different screen output. If a string is evaluated at the top level of an interactive Python session, all special characters of the string will be displayed as octal escape sequences, which can interpret the contents of the string clearly. The print function passes the string directly to the terminal program, and the screen program may interpret special characters in a special way. The following demonstrates this process, the string is composed of character a, newline, tab and character b:

>>> 'a\n\tb'
'a\n\tb'
>>> print('a\n\tb')
a
    b

In the first case, newlines and tabs appear explicitly in the string; in the second case, they are used as newlines and tabs.

The print function also usually adds a newline at the end of the string. Sometimes (lines of text read from a file are already terminated by a newline) this may not be necessary. Set the end parameter of the print function to "", so that the print function no longer adds a newline:

>>> print("abc\n")
abc

>>> print("abc\n", end="")
abc
>>>

4. String methods

Python string methods are mostly built into the standard Python string class, so all string objects automatically have these methods. In the standard String module, some useful constants are also included.

Most string methods are attached to the string object they operate through the dot operator ".", such as the x.upper() method. That is, string methods follow the dot operator and are appended to the string object. Because strings are immutable, string methods can only be used to obtain return values, and cannot modify their attached string objects in any way.

1) String split and join methods

Anyone who has worked with strings will almost certainly know that the methods split and join are useful. The functions of these two methods are opposite.

The split method returns a list of substrings in a string. The join method takes a string list as a parameter, joins the strings together to form a new string, and inserts the caller string between each element. Usually the split method uses whitespace as the delimiter to split the string, but optional parameters can be used to replace the delimiter.

Concatenating strings with "+" is useful, but it is not efficient for concatenating a large number of strings into one string. Because every time "+" is applied, a new string object is created. For the "Hello World" example above, 3 string objects are generated, 2 of which are immediately discarded.

A better option is to use the join function:

>>> " ".join(["join", "puts", "spaces", "between", "elements"])
'join puts spaces between elements'

Change the string that calls the join method to insert anything you want between the joined strings:

>>> "::".join(["Separated", "with", "colons"])
'Separated::with::colons'

It is even possible to concatenate the elements of a list of strings with the empty string "":

>>> "".join(["Separated", "by", "nothing"])
'Separatedbynothing'

The most common use of the split method is as a simple parsing tool for parsing string delimited data records stored in text files.

By default, the split method will split on all whitespace characters, not just one. But it is also possible to split based on a specific sequence by passing in an optional parameter:

>>> x = "You\t\t can have tabs\t\n \t and newlines \n\n " \
               "mixed in"
>>> x.split()
['You', 'can', 'have', 'tabs', 'and', 'newlines', 'mixed', 'in']
>>> x = "Mississippi"
>>> x.split("ss")
['Mi', 'i', 'ippi']

Sometimes it is useful to allow the last part of the string to be split to contain arbitrary text, perhaps including substrings that were matched by split. To achieve this, specify the number of splits to perform when generating the result by passing an optional second parameter to the split method.

Assuming that it is specified to split n times, the split method will split the input string from the beginning, or stop splitting after executing n times (at this time, the generated list contains n+1 substrings), or read the entire character Stop after string.

Examples are as follows:

>>> x = 'a b c d'
>>> x.split(' ', 1)
['a', 'b c d']
>>> x.split(' ', 2)
['a', 'b', 'c d']
>>> x.split(' ', 9)
['a', 'b', 'c', 'd']

If the second optional parameter is passed in when calling the split method, the first parameter must also be given at the same time. If you want to use the second parameter and split by whitespace, please set the first parameter to None.

Generally, when dealing with text files generated by other programs, the split and join methods are widely used. If your self-written program needs to generate output files in more standard formats, it is good to use the csv and json modules in the Python standard library.

2) Convert the string to a numeric value

Using the functions int and float, you can convert a string to an integer or float, respectively. These two functions will raise a ValueError exception if the string cannot be converted to a numeric value of the specified type.

Here, the int function can also accept a second optional parameter, which is used to specify the numerical base used when converting the input string.

>>> float('123.456')
123.456
>>> float('xxyy')
Traceback (innermost last):
File "<stdin>", line 1, in ?
ValueError: could not convert string to float: 'xxyy'
>>> int('3333')
3333
>>> int('123.456')           ⇽---  整数不能带有小数点
Traceback (innermost last):
File "<stdin>", line 1, in ?
ValueError: invalid literal for int() with base 10: '123.456'
>>> int('10000', 8)                                      ⇽---  将10000视为八进制数
4096
>>> int('101', 2)
5
>>> int('ff', 16)
255
>>> int('123456', 6)          ⇽---  无法将123456解释为六进制数
Traceback (innermost last):
File "<stdin>", line 1, in ?
ValueError: invalid literal for int() with base 6: '123456'

Any idea what's causing the last error?

Here it is required to interpret the string as a hexadecimal number, but the number 6 cannot appear in a hexadecimal number. Did not expect it!

3) Remove extra whitespace

Strings have three amazingly simple and useful methods, the functions strip, lstrip, and rstrip. The Strip function will return a new string identical to the original string, except that leading and trailing whitespace characters will be removed. The functions of lstrip and rstrip are similar, except that the blank characters on the left or right of the original string are removed respectively:

>>> x = "  Hello,    World\t\t "
>>> x.strip()
'Hello,    World'
>>> x.lstrip()
'Hello,    World\t\t '
>>> x.rstrip()
'  Hello,    World'

In the above example, tabs are also treated as whitespace. The exact definition of whitespace may vary on different operating systems, see the string.whitespace constant for Python's definition of whitespace.

On my Windows system, Python returns the following:

>>> import string
>>> string.whitespace
' \t\n\r\x0b\x0c'
>>> " \t\n\r\v\f"
' \t\n\r\x0b\x0c'

The vertical tab and form feed character here are expressed in the form of backslash and hexadecimal number (\xnn). A space character is represented as a space itself. If you're trying to affect something like strip by changing the value of string.whitespace, it's a good idea, but please don't. Because such operations are not guaranteed to give you the desired result.

But the strip, rstrip, and lstrip methods can take a parameter that contains the characters to be stripped.

>>> x = "www.python.org"
>>> x.strip("w")               ⇽---  删除所有的w字符
'.python.org'
>>> x.strip("gor")               ⇽---  ❶ 删除所有的g、o、r字符
'www.python.'
>>> x.strip(".gorw")           ⇽---  删除所有的.、g、o、r、w字符
'python'

Note that the strip method will remove all characters in the parameter string, and the order of the characters can be arbitrary.

Because Python reads in a whole line of text each time, including the trailing newline (if any). When processing these read-in lines, the usual trailing newline is not needed, and rstrip is a convenient way to strip the newline.

4) String search

String objects provide many simple search methods. Before introducing these methods, let's take a look at another module re of Python.

Another string search method: re module The re module is also used for string search, but it uses a much more flexible way of regular expressions. re does not search for a specified single substring, but searches according to a string pattern. For example, you can find substrings that are all numbers.

Why is this mentioned before a full introduction to re? In my experience, many times the basic string search methods are not suitable. People should have benefited from a more powerful search mechanism, but stopped pursuing a better solution because they didn't know it existed. If the basic string search method is enough, everyone is happy. Note, however, that there are more powerful options available.

There are four basic string search methods, namely find, rfind, index and rindex, which are similar. There is also a related count method that counts the number of times a substring occurs within another string. The following first introduces the find method in detail, and then introduces the differences of other methods. The find method has one required parameter, the substring to search for. The find method will return the position of the first character of the first instance of the substring in the calling string object, or -1 if the substring is not found:

>>> x = "Mississippi"
>>> x.find("ss")
2
>>> x.find("zz")
-1

The find method can also take one or two optional parameters. The first optional argument (if present) start is an integer that causes find to ignore all characters in the string up to the position start when searching for a substring. The second optional parameter (if present) end is also an integer, which will cause find to ignore characters after and after position end:

>>> x = "Mississippi"
>>> x.find("ss", 3)
5
>>> x.find("ss", 0, 3)
-1

The function of the rfind method is almost identical to that of the find method, but it searches from the end of the string and returns the position of the first character of the last occurrence of the substring in the string:

>>> x = "Mississippi"
>>> x.rfind("ss")
5

The rfind method also has one or two optional parameters, and the usage is the same as the find method. The index and rindex methods have exactly the same function as find and rfind respectively, but there is one difference: when the index or rindex method cannot find a substring in the string, instead of returning -1, it will raise ValueError.

The usage of the count method is exactly the same as the above 4 functions, but returns the number of non-overlapping occurrences of the given substring in the given string:

>>> x = "Mississippi"
>>> x.count("ss")
2

In addition, there are two other string methods for string searches, startswith and endswith. These two methods will return True or False if the caller's string begins or ends with one of the substrings of the given argument string. Otherwise return False:

>>> x = "Mississippi"
>>> x.startswith("Miss")
True
>>> x.startswith("Mist")
False
>>> x.endswith("pi")
True
>>> x.endswith("p")
False

The startswith and endswith methods can search for multiple substrings at once. If the argument is a tuple of strings, then these two methods will check all the strings in the tuple and return True as long as a string matches:

>>> x.endswith(("i", "u"))
True

The startswith and endswith methods are useful for simple searches to see if the string being checked is at the beginning or end of the line.

5) String modification

Strings are immutable, but String objects have several methods that perform operations on the string and return a new string, which is a modified version of the original string. In most cases, this approach achieves the same effect as direct modification. A full description of these methods can be found in the official documentation.

Use the replace method to replace all substrings in a string (the first parameter) with another string (the second parameter). This method also takes an optional third parameter (see the official documentation for details):

>>> x = "Mississippi"
>>> x.replace("ss", "+++")
'Mi+++i+++ippi'

While similar to the string search functions, the re module's substring replacement methods are much more powerful.

The functions string.maketrans and string.translate can be used together to convert multiple characters in a string into other characters. Although rarely used, these two functions can simplify programming when necessary.

Suppose you want to develop a translation program to convert string expressions in one computer language into expressions in another language. The first language uses the character "~" to indicate logical NOT, and the second language uses "!". The first language uses "^" to represent logical AND, and the second uses "&". The first language uses "(" and ")", while the second language uses "[" and "]". Now in a given string, to change all instances of "~" in the string to "!", all instances of "^" to "&", and all instances of "(" to "[" , changing all instances of "]" to ")".

Of course, replace can be called multiple times to complete the task, but a simpler and more effective way is:

>>> x = "~x ^ (y % z)"
>>> table = x.maketrans("~^()", "!&[]")
>>> x.translate(table)
'!x & [y % z]'

The second line of code uses maketrans to build a translation comparison table, and the data comes from its two string parameters. Both parameters must have the same number of characters. The generated lookup table can be used to find the nth character of the first parameter and return the nth character of the second parameter.

Then the comparison table generated by maketrans is passed to the translate method. The translate method will traverse each character in the calling string object and check whether it can be found in the comparison table given by the second parameter [1]. If any character can be found in the lookup table, then

The translate method will replace it with the control characters found in the comparison table to generate a translated string. The translate method can also accept an optional parameter specifying the characters to remove from the string.

Other functions in the string module can perform more specialized tasks. The string.lower method converts all alphabetic characters in the string to lowercase, while the upper method does the reverse. The capitalize method will convert the first character of the string to uppercase letters, and the title method will convert the first characters of all words in the string to uppercase letters. The swapcase method converts lowercase letters in a string to uppercase, and converts uppercase letters to lowercase. The expandtabs method replaces all tab characters with the specified number of spaces, so that tab characters are completely removed. The ljust, rjust, and center methods pad spaces in a string so that left and right spaces are adjusted to the specified width. zfill will pad the numeric string with "0" on the left.

6) Use the list to modify the string

Since strings are immutable objects, they cannot be manipulated directly like lists. Sometimes it is useful to be able to process a string as a list of characters, although the process of generating a new string (leaving the original string unchanged) is useful in many cases. At this point, the string can be converted to a list of characters, and after processing as needed, the result of the list of characters can be converted back to a string:

>>> text = "Hello, World"
>>> wordList = list(text)
>>> wordList[6:] = []       ⇽---  移除逗号之后的所有字符
>>> wordList.reverse()
>>> text = "".join(wordList)               
>>> print(text)                  ⇽---  无缝拼接
,olleH

You can also convert a string to a character tuple with the built-in tuple function. If you want to convert the list back to a string, use "".join() to complete it.

This approach should not be overused, as new string objects are created and destroyed, which is relatively expensive. Handling hundreds or thousands of strings in this way probably won't have much of an impact on how your program works, but it might when dealing with millions of strings.

7) Other useful string methods and constants

The string object also has a number of useful methods that report various characteristics of the string. For example, whether the string contains numbers or letters, or whether it is all uppercase or lowercase:

>>> x = "123"
>>> x.isdigit()
True
>>> x.isalpha()
False
>>> x = "M"
>>> x.islower()
False
>>> x.isupper()
True

There are also some useful constants defined in the string module. String.whitespace has been introduced above, this string contains characters that Python uses as whitespace in the current system. string.digits is the string '0123456789'. string.hexdigits not only contains all the characters in string.digits, but also contains 'abcdefABCDEF', which is the characters used for hexadecimal digits. string.octdigits contains '01234567', the only digits used for octal digits. string.ascii_lowercase contains all lowercase ASCII alphabet characters, string.ascii_uppercase contains all uppercase ASCII alphabet characters, and string.ascii_letters contains all characters in string.ascii_lowercase and string.ascii_uppercase. One might think that assigning these constants to other values ​​should change the behavior of Python. Python will tolerate this change, but it's probably not a good idea.

Remember, strings are sequences of characters, so you can easily test whether a character belongs to a string with Python's in operator. Of course, it is usually more concise and easier to use the string method.

The following table lists the most commonly used string operations:

string manipulation illustrate example
+ concatenates two strings together x = "hello " + "world"
* copy string x = " " * 20
upper Convert a string to uppercase x.upper()
lower Convert a string to lowercase x.lower()
title Capitalize the first letter of each word in a string x.title()
find 、 index search for a substring in a string

x.find(y)

x.index(y)

rfind 、 rindex Search for a substring starting from the end of the string

x.rfind(y)

x.rindex(y)

startswith 、 endswith Checks whether the beginning or end of a string matches a given substring

x.startswith(y)

x.endswith(y)

replace Replace target substring in string with new substring x.replace(y, z)
strip 、 rstrip 、 lstrip Remove whitespace or other characters from both ends of a string x.strip()
encode Convert a Unicode string to a bytes object x.encode("utf_8")

Note that none of the above methods will modify the original string, but return the position in the string or a new string.

5. Convert object to string

In Python, almost everything can be converted to some kind of string form with the built-in repr function. Of the Python types that have been introduced so far, only lists are a familiar complex data type.

So here's how to convert a list to a string:

>>> repr([1, 2, 3])
'[1, 2, 3]'
>>> x = [1]
>>> x.append(2)
>>> x.append([3, 4])
>>> 'the list x is ' + repr(x)
'the list x is [1, 2, [3, 4]]'

In the above example, the repr function is used to convert the list x to string form, and then the result is concatenated with another string to form the final string. Without repr, the code will not work. For an expression like "string" + [1,2] + 3, do you want to add strings, lists, or just numbers? Python doesn't know what to do in this situation, so it handles it safely (throws an error) without making any assumptions. In the above example, all elements of the list must be converted to a string representation before string concatenation can be performed.

Lists are the only Python complex objects introduced so far, and repr can be used to obtain some sort of string form for almost any Python object. To verify, let's try to call repr on the built-in complex object (actually a Python function):

>>> repr(len)
'<built-in function len>'

The string generated by Python does not contain the implementation code of the len function, but at least returns a string <built-in function len> to describe the function of the function. All Python data types (dictionary, tuple, class, etc.) call the repr function once, so no matter what type of Python object it is, it can return a string describing information about the object.

For program debugging, this makes sense. If you are not sure about the variable information during the running of the program, you can use the repr function to print out the variable information.

以上就是Python如何将任何对象转换为描述该对象的字符串的过程,其实Python有两种方式来转换。repr函数返回的结果,也许可以不太精确地被称为Python对象的正式字符串表示(formal string representation)。说得具体一点,由repr函数返回的Python对象的字符串形式,可以重建原来的Python对象。对于大型的、复杂的对象,可能不希望在调试时的输出或者状态报告中看到这种结果。

Python还提供了内置的str函数。与repr相比,str旨在生成可打印(printable)的字符串形式,并且可用于任何Python对象。str返回的结果,也许可被称为对象的非正式字符串表示(informal string representation)。由str返回的字符串,不需要完整定义对象,只要能供人类阅读即可,而不用供Python代码读取。

一开始使用函数repr和str时,不会注意到它们之间有什么区别。因为在开始用到Python面向对象的特性之前,两者没有区别。对任何内置Python对象的str调用,总是会调用repr来得出结果。只有开始定义自己的类时,str和repr之间的差别才会变得重要起来。

那么,为什么现在要提及呢?这是为了说明一点,在运行repr函数时,幕后进行的操作会比简单地用print函数调试要多一些。为了维持良好的编程风格,请渐渐习惯用str而不是repr来创建用于显示的字符串信息。

6. 使用format方法

在Python 3中格式化字符串的途径有两种,用字符串类的format方法比较新一些。format方法用了两个参数,同时给出了包含被替换字段的格式字符串,以及替换后的值。这里的被替换字段是用{}标识的。如果要在字符串中包含字符“{”或“}”,请用“{ {”或“}}”来表示。

format方法是一种强大的字符串格式化脚本,称得上是一种微型语言,几乎为操纵字符串格式提供了无限可能。

1)format方法和位置参数

format方法的一种比较简单的用法,就是用被替换字段的编号,分别对应传入的参数:

>>> "{0} is the {1} of {2}".format("Ambrosia", "food", "the gods")     ⇽---  ❶
'Ambrosia is the food of the gods'
>>> "{
   
   {Ambrosia}} is the {0} of {1}".format("food", "the gods")     ⇽---  ❷
'{Ambrosia} is the food of the gods'

注意format方法是由格式字符串调用的,格式字符串也可以是字符串变量❶。这里有两个“{”或“}”字符表示发生了转义,因此不表示被替换字段。❷

以上示例带有3个被替换字段,即{0}、{1}和{2},将会依次由第一个、第二个和第三个参数填充。无论{0}在格式字符串中位于什么位置,它总是被第一个参数取代,依此类推。

还可以用命名参数的形式来使用format。

2)format方法和命名参数

format方法还能够识别命名参数和被替换字段:

>>> "{food} is the food of {user}".format(food="Ambrosia", user="the gods") 
'Ambrosia is the food of the gods'

上述情况下,在选择被替换参数时会按照被替换字段名称和参数名称进行匹配。

同时使用位置参数和命名参数也是允许的,甚至可以访问参数中的属性和元素:

>>> "{0} is the food of {user[1]}".format("Ambrosia", 
...          user=["men", "the gods", "others"]) 
'Ambrosia is the food of the gods'

这时,第一个参数是位置参数,第二个参数user[1]指向的是命名参数user的第二个元素。

3)格式描述符

格式描述符(format specifier)用于设定格式化输出的结果,控制功能甚至超过了旧版的字符串格式化样式。在填充被替换字段时,格式描述符能够控制填充字符、对齐方式、正负号、宽度、精度和数据类型。

如前所述,格式描述符的语法本身就是一种微型的语言,因为比较复杂所以无法在此全部介绍。但以下示例可以略微展示一下格式描述符的用途:

>>> "{0:10} is the food of gods".format("Ambrosia")     ⇽---  ❶
'Ambrosia   is the food of gods'
>>> "{0:{1}} is the food of gods".format("Ambrosia", 10)     ⇽---  ❷
'Ambrosia   is the food of gods'
>>> "{food:{width}} is the food of gods".format(food="Ambrosia", width=10)
'Ambrosia   is the food of gods'
>>> "{0:>10} is the food of gods".format("Ambrosia")     ⇽---  ❸
'  Ambrosia is the food of gods'
>>> "{0:&>10} is the food of gods".format("Ambrosia")     ⇽---  ❹
'&&Ambrosia is the food of gods'

描述符“:10”设置该字段宽度为10个字符,不足部分用空格填充❶。描述符“:{1}”表示字段宽度由第二个参数定义❷。描述符“:>10”强制字段右对齐,不足部分用空格填充❸。描述符“:&>10”强制右对齐,不足部分不用空格而是用“&”字符填充❹。

7. 用%格式化字符串

用字符串取模(string modulus)操作符%来格式化字符串,该操作符用于将Python数值并入格式化字符串(Formatting String)中,以用于打印或其他用途。

C语言用户会注意到,这种古怪的用法类似于printf家族函数。用%作为格式化符号是旧版的字符串格式化风格,但是因为这是Python早期版本中的标准,所以这里才会介绍一下。如果代码是从早期版本的Python移植过来的,或者是由熟悉早期版本的程序员编写的,那就可能会看到它。但这种风格的格式不应该再出现在新编写的代码中了,因为它将被废弃,Python语言以后不会再提供支持了。

示例如下:

>>> "%s is the %s of %s" % ("Ambrosia", "food", "the gods")
'Ambrosia is the food of the gods'

字符串取模操作符(指中间的那个黑体%,不是左面的3个%s)由两部分组成:左侧是字符串,右侧是元组。字符串取模操作符将会扫描左侧的字符串,查找特定的格式化序列[2](formatting sequence),并按顺序将其替换为右侧的值而生成新的字符串。在以上例子中,左侧的格式化序列就是3个%s,意思就是“在此处粘贴一个字符串”。

只要在右侧传入不同的值,就可以生成不同的字符串:

>>> "%s is the %s of %s" % ("Nectar", "drink", "gods")
'Nectar is the drink of gods'
>>> "%s is the %s of the %s" % ("Brussels Sprouts", "food", "foolish")
'Brussels Sprouts is the food of the foolish'

右侧元组中的成员不一定非得是字符串,因为用了%s就会自动对其调用str函数:

>>> x = [1, 2, "three"]
>>> "The %s contains: %s" % ("list", x)
"The list contains: [1, 2, 'three']"

1)使用格式化序列

所有的格式化序列都是包含在%操作符左侧字符串中的子字符串。每个格式化序列都以一个百分号开始,后面跟随一个或多个字符代表要被替换为格式化序列的位置以及替换方式。上面用到的%s是最简单的格式化序列,表示要用%操作符右侧元组中相应的字符串替换%s。

其他的格式化序列可能会稍显复杂些。以下格式化序列将数字的输出宽度(字符总数)设定为6,将小数点后面的字符数设定为2,并将数字左对齐。这里把格式化序列放在一对尖括号中,以便于观察在格式化后的字符串中插入了额外的空格:

>>> "Pi is <%-6.2f>" % 3.14159 # 格式化序列为 %–6.2f
'Pi is <3.14  >'

Python的官方文档中给出了格式化序列中所有允许的字符选项。选项有点多,但没有特别难以使用的。请记住,在Python交互环境下可以不断地试用这些格式化序列,检验一下是否符合预期的需求。

2)命名参数和格式化序列

%操作符还提供了一种额外特性,在某些场合可能会比较有用。但是,为了介绍这种特性,必须用到尚未详细介绍的Python字典(dictionary)特性。在其他编程语言中,字典常被称为散列表(hash table)或关联数组(associative array)。

格式化序列可以用名称而不是位置来指定要替换的内容。这时,每个格式化序列在前缀“%”之后紧跟着一个用圆括号括起来的名称,如下所示:

"%(pi).2f"         ⇽---  在圆括号中标注名称

同时,%操作符右侧的参数也不再以单个值或值的元组形式,而是以输出值组成的字典给出,每个已命名的格式化序列在字典中都有对应名称的键。用之前的格式化序列和字符串模操作符,代码可能如下所示:

>>> num_dict = {'e': 2.718, 'pi': 3.14159}
>>> print("%(pi).2f - %(pi).4f - %(e).2f" % num_dict)
3.14 - 3.1416 - 2.72

如果要对格式字符串执行大量替换时,上述代码特别有用,因为不需要再保持格式字符串中的格式化序列与右侧元组元素的位置对应关系。字典参数中的元素定义顺序无关紧要,并且格式字符串中可以多次引用字典中的值(就像以上代码的'pi'一样)。

Python内置的print函数也有一些选项,可以比较容易地处理一些简单的字符串格式输出。如果只提供一个参数,print函数将会打印参数值和一个换行符,这样每次调用print函数都会在单独一行中打印每个结果:

>>> print("a")
a
>>> print("b")
b

不过print函数还有更多用法。可以给print函数传入多个参数,这些参数值将会打印在同一行中,中间用空格分隔,最后以换行符结尾:

>>> print("a", "b", "c")
a b c

如果这样还不能满足需求,可以给print函数多带几个参数,用来控制分隔符和每行的结束符:

>>> print("a", "b", "c", sep="|")
a|b|c
>>> print("a", "b", "c", end="\n\n")
a b c

>>>

print函数还可以将结果输出到文件,也可以输出到控制台。

>>> print("a", "b", "c", file=open("testfile.txt", "w"))

对简单的文本输出而言,print函数的可选参数就足够用了,但是更复杂的场景最好还是采用format方法。

8. 字符串内插

从Python 3.6开始,新提供了一种创建字符串常量的途径,字符串常量中可包含任意值,被称为字符串内插(string interpolation)。字符串内插可以在字符串内包含Python表达式的值。因为内插字符串的前缀是个“f”,所以被称为f字符串(f-string),其包含表达式的语法类似于format方法,但开销较小一些。

以下例子大致演示了f字符串的用法:

>>> value = 42
>>> message = f"The answer is {value}"
>>> print(message)
The answer is 42

正如format方法一样,这里同样可以加入格式描述符:

>>> pi = 3.1415
>>> print(f"pi is {pi:{10}.{2}}")
pi is        3.1

因为字符串内插是新特性,所以用途还不是十分明确。关于f字符串和格式描述符的完整文档,请参考Python的PEP-498文档。

9. bytes对象

bytes对象与string对象比较类似,但有一个重要区别:string对象是Unicode字符组成的不可变序列,而bytes对象是值从0到256的整数序列。如果需要处理二进制数据,例如,从二进制数据文件中读取数据时,bytes对象是必需的。

bytes对象看起来像string,但不能像string对象那样使用,也不能与string对象拼接。这点非常重要,请务必牢记。

>>> unicode_a_with_acute = '\N{LATIN SMALL LETTER A WITH ACUTE}'
>>> unicode_a_with_acute
'á'
>>> xb = unicode_a_with_acute.encode()     ⇽---  ❶
>>> xb
b'\xc3\xa1'     ⇽---  ❷
>>> xb += 'A'     ⇽---  ❸
Traceback (most recent call last):
  File "<pyshell#35>", line 1, in <module>
    xb += 'A'
TypeError: can't concat str to bytes
>>> xb.decode()     ⇽---  ❹
'á'

上述例子中,首先从普通(Unicode)字符串转换为bytes对象,需要调用字符串的encode方法❶。在编码为bytes对象后,字符成了两个字节,打印输出的方式不再和字符串一样了❷。因为两种类型不再兼容,这时再想把bytes对象和字符串对象相加,就会报类型错误❸。最后,将bytes对象转换回字符串,需要调用bytes对象的decode方法❹。

多数时候,根本无须考虑该用Unicode字符串还是bytes。但如果要处理国际字符集(日益普遍),则必须了解普通字符串和bytes对象之间的区别。

2、列表

1. 列表类似于数组

Python的列表与Java、C等其他语言的数组非常相似,是对象的有序集合。创建列表的方法是,在方括号中列出以逗号分隔的元素,如下所示:

# 将包含3个元素的列表赋给x
x = [1, 2, 3]

注意,列表不必提前声明,也不用提前就将大小固定下来。以上在一行代码中就完成了列表的创建和赋值,列表的大小会根据需要自动增减。

Python提供了强类型的array模块,支持基于C语言数据类型的数组。有关数组的用法,建议仅在确实需要提升性能时才考虑使用。

与很多其他语言的列表不同,Python的列表可以包含不同类型的元素,列表元素可以是任意的Python对象。

下面就是包含各种元素的列表示例:

# 第一个元素是数字,第二个元素是字符串,第三个元素是另一个列表
x = [2, "two", [1, 2, 3]]

最基本的内置列表函数或许就是len函数了,它返回列表的元素数量:

>>> x = [2, "two", [1, 2, 3]]
>>> len(x)
3

注意,len函数不会对内部嵌套的列表中的数据项进行计数。

2. 列表的索引机制

理解了列表的索引机制,将能使Python更有用。

使用类似C语言数组索引的语法,就可以从Python列表中提取元素。像C和许多其他语言一样,Python从0开始计数,索引为0将返回列表的第一个元素,索引为1则返回第二个元素,依此类推。

下面给出一些示例:

>>> x = ["first", "second", "third", "fourth"]
>>> x[0]
'first'
>>> x[2]
'third'

但是Python的索引用法比C语言更加灵活。如果索引为负数,表示从列表末尾开始计数的位置,其中-1是列表的最后位置,-2是倒数第二位,依此类推。

继续沿用以上列表x,可以执行以下操作:

>>> a = x[-1]
>>> a
'fourth'
>>> x[-2]
'third'

对于只涉及单个列表索引的操作,通常可以认为索引指向了列表中的特定元素。对于更高级的索引操作,更为正确的理解是将索引视为元素之间的位置标识。

提取单个元素时,对索引的理解不会有问题。但是Python支持一次提取或赋值一整个子列表,也就是切片(slice)操作。不是用list[index]提取紧跟着index的数据项,而是用list[index1:index2]提取index1(含)和上限至index2(不含)之间的所有数据项,并放入一个新列表中。

下面给出一些示例:

>>> x = ["first", "second", "third", "fourth"]
>>> x[1:-1]
['second', 'third']
>>> x[0:3]
['first', 'second', 'third']
>>> x[-2:-1]
['third']

如果第二个索引给出一个第一个索引之前的位置,似乎应该按逆序返回两个索引之间的元素。但其实不会如此,而是会返回空列表:

>>> x[-1:2]
[]

在对列表进行切片时,还可以省略index1或index2。省略index1表示“从列表头开始”,而省略index2则表示“直到列表末尾为止”:

>>> x[:3]
['first', 'second', 'third']
>>> x[2:]
['third', 'fourth']

如果两个索引都省略了,将会由原列表从头至尾创建一个新列表,即列表复制。如果需要修改列表,但又不想影响原列表,就需要创建列表的副本,这时就能用列表复制技术了:

>>> y = x[:]
>>> y[0] = '1 st'
>>> y
['1 st', 'second', 'third', 'fourth']
>>> x
['first', 'second', 'third', 'fourth']

3. 修改列表

除了提取列表元素,使用列表索引语法还可以修改列表。只要将索引放在赋值操作符左侧 即可:

>>> x = [1, 2, 3, 4]
>>> x[1] = "two"
>>> x
[1, 'two', 3, 4]

切片语法也可以这样使用。类似lista[index1:index2]=listb的写法,会导致lista在index1和index2之间的所有元素都被listb的元素替换掉。listb的元素数量可以多于或少于lista中被移除的元素数,这时lista的长度会自动做出调整。

利用切片赋值操作,可以实现很多功能,例如:

>>> x = [1, 2, 3, 4]
>>> x[len(x):] = [5, 6, 7]            ⇽---  在列表末尾追加列表
>>> x
[1, 2, 3, 4, 5, 6, 7]
>>> x[:0] = [-1, 0]                   ⇽---  在列表开头插入列表
>>> x
[-1, 0, 1, 2, 3, 4, 5, 6, 7]
>>> x[1:-1] = []                      ⇽---  移除列表元素
>>> x
[-1, 7]

向列表添加单个元素是常见操作,所以专门为此提供了append方法。

>>> x = [1, 2, 3]
>>> x.append("four")
>>> x
[1, 2, 3, 'four']

如果用append方法把列表添加到另一个列表中去,是会出问题的。添加进去的列表会成为主列表中的单个元素:

>>> x = [1, 2, 3, 4]
>>> y = [5, 6, 7]
>>> x.append(y)  
>>> x
[1, 2, 3, 4, [5, 6, 7]]

extend方法和append方法类似,但是它能够将列表追加到另一个列表之后:

>>> x = [1, 2, 3, 4]
>>> y = [5, 6, 7]
>>> x.extend(y)  
>>> x
[1, 2, 3, 4, 5, 6, 7]

列表还有一个特殊的insert方法,可以在两个现有元素之间或列表之前插入新的元素。insert是列表的方法,带有两个参数。

第一个参数是新元素被插入列表的索引位置,第二个参数是新元素本身:

>>> x = [1, 2, 3]
>>> x.insert(2, "hello")
>>> print(x)
[1, 2, 'hello', 3]
>>> x.insert(0, "start")
>>> print(x)
['start', 1, 2, 'hello', 3]

多数情况下,可将list.insert(n,elem)简单地理解为,在列表的第n个元素之前插入elem。insert只是一个便利的方法。任何可用insert完成的操作,通过切片赋值也可以完成。

也就是说,当n是非负值时,list.insert(n, elem)与list[n:n] = [elem]的效果是一样的。使用insert有利于提高代码的可读性,insert甚至还可以处理负数索引:

>>> x = [1, 2, 3]
>>> x.insert(-1, "hello")
>>> print(x)
[1, 2, 'hello', 3]

删除列表数据项或切片的推荐方法是使用del语句。del语句能完成的功能,并没有超过切片赋值操作,但通常它更容易被记住,也更易于阅读:

>>> x = ['a', 2, 'c', 7, 9, 11]
>>> del x[1]
>>> x
['a', 'c', 7, 9, 11]
>>> del x[:2]
>>> x
[7, 9, 11]

通常,del list[n]的功能与list[n:n+1] = []是一样的,而del list[m:n]的功能则与list[m:n] = []相同。

列表的remove方法并不是insert方法的逆操作。insert方法会在指定位置插入列表元素,remove则会先在列表中查找给定值的第一个实例,然后将该值从列表中删除:

>>> x = [1, 2, 3, 4, 3, 5]
>>> x.remove(3)
>>> x
[1, 2, 4, 3, 5]
>>> x.remove(3)
>>> x
[1, 2, 4, 5]
>>> x.remove(3)
Traceback (innermost last):
 File "<stdin>", line 1, in ?
ValueError: list.remove(x): x not in list

如果remove找不到要删除的的值,就会引发错误。可以用Python的异常处理机制来捕获错误,也可以在做remove之前,先用in检查一下要删除的值是否存在,以避免错误的发生。

列表的reverse方法是一种较为专业的列表修改方法,可以高效地将列表逆序:

>>> x = [1, 3, 5, 6, 7]
>>> x.reverse()
>>> x
[7, 6, 5, 3, 1]

4. 对列表排序

利用Python内置的sort方法,可以对列表进行排序:

>>> x = [3, 8, 4, 0, 2, 1]
>>> x.sort()
>>> x
[0, 1, 2, 3, 4, 8]

sort方法是原地排序的,也就是说会按排序修改列表。如果排序时不想修改原列表,可以有两种做法:一种是使用内置的sorted()函数;另一种是先建立列表的副本,再对副本进行排序:

>>> x = [2, 4, 1, 3]
>>> y = x[:]
>>> y.sort()
>>> y
[1, 2, 3, 4]
>>> x
[2, 4, 1, 3]

 sort方法对字符串也是有效的:

>>> x = ["Life", "Is", "Enchanting"]
>>> x.sort()
>>> x
['Enchanting', 'Is', 'Life']

sort方法可以对任何对象进行排序,因为Python几乎可以对任何对象进行比较。

但是在排序时有一点需要注意:sort用到的默认键方法要求,列表中所有数据项均为可比较的类型。

这意味着,如果列表同时包含数字和字符串,那么使用sort方法将会引发异常:

>>> x = [1, 2, 'hello', 3]
>>> x.sort()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'str' and 'int'

然而,对列表的列表却是可以进行排序的:

>>> x = [[3, 5], [2, 9], [2, 3], [4, 1], [3, 2]]
>>> x.sort()
>>> x
[[2, 3], [2, 9], [3, 2], [3, 5], [4, 1]]

根据Python对复杂对象的内部比较规则,子列表的排序规则是:先升序比较第一个元素,再升序比较第二个元素。

sort的用法还可以更加灵活,它可以带有可选的reverse参数,当reverse=True时可以实现逆向排序,可以用自定义的键函数来决定列表元素的顺序。

1)自定义排序

为了使用自定义排序,先要定义函数,这是还未介绍的内容。

在默认情况下,sort使用内置的Python比较函数来确定顺序,这也适用于大多数情况。但有时候,不想以与默认排序相应的方式对列表进行排序。下面假设需要按照每个单词的字符数对单词列表进行排序,而不是Python通常的词典顺序。

为此需要编写一个函数,用于返回需要排序的值或键,并将该函数与sort方法一起使用。该函数接受一个参数,并返回供sort函数使用的键或值。对于按字符数排序的排序要求,也许采用以下的键函数比较合适:

def compare_num_of_chars(string1):
    return len(string1)

上述键函数没有什么特别之处,就是向sort方法回传每个字符串的长度,而不是字符串本身。

定义了键函数之后,就可以通过关键字key将键函数传递给sort方法。因为函数也是Python对象,所以可以像其他任何Python对象一样来传递。下面是一个小程序,演示了默认排序和自定义排序之间的区别:

>>> def compare_num_of_chars(string1):
        return len(string1)
>>> word_list = ['Python', 'is', 'better', 'than', 'C']
>>> word_list.sort()
>>> print(word_list)
['C', 'Python', 'better', 'is', 'than']
>>> word_list = ['Python', 'is', 'better', 'than', 'C']
>>> word_list.sort(key=compare_num_of_chars)
>>> print(word_list)
['C', 'is', 'than', 'Python', 'better']

第一个列表按照词典序排序,大写字母在小写字母之前,第二个列表按照字符数升序排序。

自定义排序非常有用,但如果性能是关键需求,可能会比不上默认排序速度。通常对性能影响很小,但如果键函数特别复杂,则影响可能会超出预期,尤其当涉及数十万或数百万个元素排序时。

有一种特别的场合应该避免采用自定义排序,这就是要按降序而非升序对列表进行排序时。在这种情况下,将reverse参数设置为True并调用sort方法即可。如果由于某种原因不想如此,就最好仍然先对列表进行正常排序,然后用reverse方法对结果列表逆转顺序。标准排序和逆转排序加在一起,仍然会比自定义排序快很多。

2)sorted()函数

列表内置了排序方法,但Python的其他可迭代对象(如字典的键),就没有sort方法。Python还内置有sorted()函数,能够从任何可迭代对象返回有序列表。

和列表的sort方法一样,sorted()函数同样也用到了参数key和reverse:

>>> x = (4, 3, 1, 2)
>>> y = sorted(x)
>>> y
[1, 2, 3, 4] 
>>> z = sorted(x, reverse=True)
>>> z
[4, 3, 2, 1]

5. 其他常用的列表操作

还有其他几种列表方法也很常用,但是无法归入任何类别。

1)用in操作符判断列表成员

用in操作符来测试某值是否在列表中,这十分简单,in返回的是布尔值。还可以用not in操作符来做反向判断。

>>> 3 in [1, 3, 4, 5]
True
>>> 3 not in [1, 3, 4, 5]
False
>>> 3 in ["one", "two", "three"]
False
>>> 3 not in ["one", "two", "three"]
True

2)用+操作符拼接列表

如果要将两个现有的列表拼接起来创建新的列表,可以使用+(拼接列表)操作符,而作为参数的列表不会发生变化:

>>> z = [1, 2, 3] + [4, 5]
>>> z
[1, 2, 3, 4, 5]

3)用*操作符初始化列表

用*操作符可以生成指定大小的列表,列表初始化为给定值。当大型列表的大小可以预见时,这是很常见的处理方式。虽然用append方法可以添加元素,也可根据需要自动扩展列表,但利用*在程序一开始就精确设置好列表的大小,能够获得更高的运行效率。如果列表的大小不发生变化,就不会产生重新分配内存的开销。

>>> z = [None] * 4
>>> z
[None, None, None, None]

当与列表一起使用时,*(在此被称为列表乘法操作符)将按指定的次数复制给定列表,并把所有的列表副本拼接起来构成一个新的列表。这是提前定义指定大小列表的标准Python方法。

列表乘法中经常会用到内含单个None实例的列表,但其实列表可以包含任何内容:

>>> z = [3, 1] * 2
>>> z
[3, 1, 3, 1]

4)用min和max方法求列表的最小值和最大值

用min和max方法可以查找列表中的最小元素和最大元素。min和max可能主要会用于数值型列表,但实际上它们可以用于包含任何类型元素的列表。如果不同类型之间的比较没有意义,那么尽力在类型不同的对象集中查找最大或最小对象,将会引发错误:

>>> min([3, 7, 0, -2, 11])
-2
>>> max([4, "Hello", [1, 2]])
Traceback (most recent call last):   
  File "<pyshell#58>", line 1, in <module>     
    max([4, "Hello",[1, 2]])
TypeError: '>' not supported between instances of 'str' and 'int'

5)用index方法搜索列表

如果要查找某值在列表中的位置,而不是只想知道该值是否在列表中,使用index方法即可。index方法会遍历列表,查找与给定值相等的列表元素,并返回该列表元素的位置:

>>> x = [1, 3, "five", 7, -2]
>>> x.index(7)
3
>>> x.index(5)
Traceback (innermost last):
 File "<stdin>", line 1, in ? 
ValueError: 5 is not in list

如上所示,如果要查找位置的元素在列表中不存在,就会引发错误。处理这个错误的方式,可以与处理remove方法中发生的类似错误相同。也就是说,在调用index方法之前,用in对列表进行测试。

6)用count方法对匹配项计数

count也会遍历列表并查找给定值,但返回的是在列表中找到该值的次数,而不是位置信息:

>>> x = [1, 2, 2, 3, 5, 2, 5]
>>> x.count(2)
3
>>> x.count(5) 
2
>>> x.count(4)
0

7)列表操作小结

列表显然是非常强大的数据结构,其可能的用途远超普通数组。对Python编程来说,列表操作非常重要,所以特地将列表操作在此列出,以便参考,如下表所示。

列表操作 说明 示例
[] 创建空列表 x = []
len 返回列表长度 len(x)
append 在列表末尾添加一个元素 x.append('y')
extend 在列表末尾添加另一个列表 x.extend(['a', 'b'])
insert 在列表的指定位置插入一个新元素 x.insert(0, 'y')
del 删除一个列表元素或切片 del(x[0])
remove 检索列表并移除给定值 x.remove('y')
reverse 原地将列表逆序 x.reverse()
sort 原地对列表排序 x.sort()
+ 将两个列表拼接在一起 x1 + x2
* 将列表复制多份 x = ['y'] * 3
min 返回列表中的最小元素 min(x)
max 返回列表中的最大元素 max(x)
index 返回某值在列表中的位置 x.index['y']
count 对某值在列表中出现的次数计数 x.count('y')
sum 对列表数据项计算合计值(如果可以合计的话) sum(x)
in 返回某数据项是否为列表的元素 'y' in x

熟悉上述这些列表操作,将让Python编程变得更为容易。

6. 嵌套列表和深复制

列表可以嵌套。嵌套列表的一种用途是表示二维矩阵。矩阵的成员可以通过二维索引来引用,这些矩阵的索引的用法如下:

>>> m = [[0, 1, 2], [10, 11, 12], [20, 21, 22]]
>>> m[0]
[0, 1, 2]
>>> m[0][1]
1
>>> m[2]
[20, 21, 22]
>>> m[2][2]
22

上述索引机制可以按需扩展到更多维度。

大多数时候,对嵌套列表只需要考虑这么多了。但在使用嵌套列表时可能还是会碰到其他问题,特别是变量如何引用对象,列表之类的对象可能会被修改(可变对象)。下面最好还是举个例子进行说明:

>>> nested = [0]
>>> original = [nested, 1]
>>> original
[[0], 1]

在这种情况下,嵌套列表中的值既可以通过变量nested进行修改,也可以通过变量original进行修改:

>>> nested[0] = 'zero'
>>> original
[['zero'], 1] 
>>> original[0][0] = 0 
>>> nested
[0]
>>> original
[[0], 1]

但是,如果nested被赋为另一个列表,则变量original和nested之间的关联就断开了:

>>> nested = [2]
>>> original
[[0], 1]

通过全切片(即x[:])可以得到列表的副本,用+或*操作符(如x+[]或x*1)也可以得到列表的副本。但它们的效率略低于使用切片的方法。这3种方法都会创建所谓的浅副本(shallow copy),大多数情况下这也能够满足需求了。

但如果列表中有嵌套列表,那就可能需要深副本(deep copy)。深副本可以通过copy模块的deepcopy函数来得到:

>>> original = [[0], 1]
>>> shallow = original[:]
>>> import copy
>>> deep = copy.deepcopy(original)

变量original和shallow所指的列表关联在了一起。修改其中一个变量所指的嵌套列表的值,会影响另一个变量所指的嵌套列表:

>>> shallow[1] = 2
>>> shallow
[[0], 2]
>>> original
[[0], 1]
>>> shallow[0][0] = 'zero'
>>> original
[['zero'], 1]

深副本则完全与原变量无关,它的变化对原列表没有任何影响:

>>> deep[0][0] = 5
>>> deep
[[5], 1]
>>> original
[['zero'], 1]

对于列表中其他嵌套的可修改对象(例如字典),上述规则同样适用。

3、元组

元组是与列表非常相似的数据结构。但是元组只能创建,不能修改。元组与列表非常像,或许大家很想知道,Python为什么要不厌其烦地设置两种类型。原因就是元组具有列表无法实现的重要作用,如用作字典的键。

1. 元组的基础知识

元组的创建方式类似于列表,只要把一系列值赋给变量即可。列表是由“[”和“]”括起来的序列,元组则是由“(”和“)”括起来的序列:

>>> x = ('a', 'b', 'c')

以上代码创建了一个包含3个元素的元组。

元组创建完成后,就能像列表一样使用了。由于元组和列表的使用方法太相像了,以至于很容易就会忘记它们其实是不同的数据类型:

>>> x[2]
'c'
>>> x[1:]
('b', 'c')
>>> len(x)
3
>>> max(x)
'c'
>>> min(x)
'a'
>>> 5 in x
False
>>> 5 not in x
True

元组和列表之间的主要区别就是,元组是不可变的。如果尝试对元组进行修改,将会收到一个令人困惑的错误信息。以Python的口气就是,不知道如何给元组中的数据项赋值:

>>> x[2] = 'd'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

利用+和*操作符,可以由现有元组创建新的元组:

>>> x + x
('a', 'b', 'c', 'a', 'b', 'c')
>>> 2 * x
('a', 'b', 'c', 'a', 'b', 'c')

元组副本的创建方式,与列表完全相同:

>>> x[:]
('a', 'b', 'c')
>>> x * 1
('a', 'b', 'c')
>>> x + ()
('a', 'b', 'c')

元组本身不能被修改,但假如包含了可变对象(如列表或字典),而且这些对象被各自赋值了变量,就可以对可变对象实现修改。包含可变对象的元组,是不允许作为字典的键使用的。

2. 单个元素的元组应加上逗号

元组在使用句法上,有一点需要注意。因为用于包围列表元素的方括号在Python其他地方没有再用到,所以可以很清楚地用[]表示空列表,[1]则表示包含一个元素的列表。但是用于包含元组的圆括号,就并非如此了。圆括号还可以用来对表达式的内容进行分组,以便能强制按照指定顺序对表达式求值。

例如,Python程序中出现了(x + y),那么到底是先求x与y的和再放入单个元素的元组,还是在任意一侧的表达式起作用之前用圆括号先强制对x和y求和呢?

仅当元组只包含一个元素时,才会出现上述问题。因为元组内的多个元素之间会用逗号分隔,Python编译器看到逗号,就会认为这里的圆括号是用来标识元组的,而不是要将表达式内容分组。当元组只包含一个元素时,Python会要求在元素后面跟一个逗号,以便消除歧义。当元组不包含元素(空元组)时,不会有问题。一对空的圆括号肯定是一个元组,不然就毫无意义了:

>>> x = 3
>>> y = 4
>>> (x + y)   # 此行代码将把x和y相加
7
>>> (x + y,)  # 跟了逗号就意味着,圆括号是用来标识元组的
(7,)
>>> ()        # 用成对的空的圆括号创建一个空元组
()

3. 元组的打包和拆包

方便起见,Python允许元组出现在赋值操作符的左侧,这时元组中的变量会依次被赋予赋值操作符右侧元组的元素值。示例如下:

>>> (one, two, three, four) =  (1, 2, 3, 4)
>>> one
1
>>> two
2

以上示例还可以写得更加简洁一些,因为在赋值时,即使没有圆括号,Python也可以识别出元组。等号右侧的值会被打包(pack)为元组,然后拆包(unpack)到左侧的变量中去:

one, two, three, four = 1, 2, 3, 4

上面用一行代码就替换掉了以下4行:

one = 1
two = 2
three = 3
four = 4

采用这种技巧,交换两个变量的值就变得十分简便了。不必写成:

temp = var1
var1 = var2
var2 = temp

只要简单一行代码即可:

var1, var2 = var2, var1

为了进一步方便使用,Python 3还支持扩展的拆包特性,允许带*的元素接收任意数量的未匹配元素。以下示例可以清楚地说明这种特性:

>>> x = (1, 2, 3, 4)
>>> a, b, *c = x
>>> a, b, c
(1, 2, [3, 4])
>>> a, *b, c = x
>>> a, b, c
(1, [2, 3], 4)
>>> *a, b, c = x
>>> a, b, c
([1, 2], 3, 4)
>>> a, b, c, d, *e = x
>>> a, b, c, d, e
(1, 2, 3, 4, [])

注意,带星号的元素会把多余的所有数据项接收为列表。如果没有多余的元素,则带星号的元素会收到空列表。

当遇到列表分隔符时,也会执行打包和拆包操作:

>>> [a, b] = [1, 2]
>>> [c, d] = 3, 4
>>> [e, f] = (5, 6)
>>> (g, h) = 7, 8
>>> i, j = [9, 10]
>>> k, l = (11, 12)
>>> a
1
>>> [b, c, d]
[2, 3, 4]
>>> (e, f, g)
(5, 6, 7)
>>> h, i, j, k, l
(8, 9, 10, 11, 12)

4. 列表和元组的相互转换

利用list函数,元组很容易就能转换为列表,参数是任意序列,生成的是由构成原始序列的元素构成的新列表。

类似地,列表也可以通过tuple函数转换为元组,但是生成的不是列表而是元组:

>>> list((1, 2, 3, 4))
[1, 2, 3, 4]
>>> tuple([1, 2, 3, 4])
(1, 2, 3, 4)

利用list,很容易就能将字符串拆分为字符。这很有意思,有必要说明一下。

>>> list("Hello")
['H', 'e', 'l', 'l', 'o']

上述机制之所以能够生效,是因为list(和tuple)适用于任何Python序列。而字符串正好就是字符序列。

4、集合

Python的集合(set)是一组对象的无序集。如果主要关心的是成员是否属于集合、是否唯一,那么集合就比较有用了。与字典键类似,集合中的项必须是不可变的、可散列的。这就表示,整数、浮点数、字符串和元组可以作为集合的成员,但列表、字典和集合本身不可以。

1. 集合的操作

除in、len、for循环遍历这些常见的操作之外,集合还支持很多特有的操作:

>>> x = set([1, 2, 3, 1, 3, 5])     ⇽---  ❶
>>> x
{1, 2, 3, 5}     ⇽---  ❷
>>> x.add(6)     ⇽---  ❸
>>> x
{1, 2, 3, 5, 6}    
>>> x.remove(5)     ⇽---  ❹
>>> x
{1, 2, 3, 6}
>>> 1 in x     ⇽---  ❺
True
>>> 4 in x     ⇽---  ❺
False
>>> y = set([1, 7, 8, 9])
>>> x | y     ⇽---  ❻
{1, 2, 3, 6, 7, 8, 9}
>>> x & y     ⇽---  ❼
{1}
>>> x ^ y     ⇽---  ❽
{2, 3, 6, 7, 8, 9}
>>>

通过对序列(如列表)调用set函数,可以创建集合❶。在由序列生成集合时,重复的元素将会被移除❷。用set函数创建集合后,可以用add❸和remove❹修改集合中的元素。关键字in可用于检查对象是否为集合的成员❺。用操作符“|”可获得两个集合的并集❻,用操作符“&”可获得交集❼,用操作符“^”则可以求得对称差(symmetric difference)。对称差是指,属于其中一个但不同时属于两个集合的元素。

以上代码并没有把所有的集合操作都列全,但足以说明集合的工作方式了。更多信息参见Python的官方文档。

2. 不可变集合

因为集合是可变的且不可哈希的,所以不能用作其他集合的成员。为了让集合本身也能够成为集合的成员,Python提供了另一种集合类型frozenset,它与集合很相像,但是创建之后就不能更改了。

frozenset是不可变的、可散列的,因此可以作为其他集合的成员:

>>> x = set([1, 2, 3, 1, 3, 5]) 
>>> z = frozenset(x)
>>> z 
frozenset({1, 2, 3, 5})
>>> z.add(6)
Traceback (most recent call last):
  File "<pyshell#79>", line 1, in <module>
   z.add(6)
AttributeError: 'frozenset' object has no attribute 'add'
>>> x.add(z)
>>> x
{1, 2, 3, 5, frozenset({1, 2, 3, 5})}

3. 列表与元祖和集合对比

表和元组是容纳了一系列元素的数据结构,就像字符串是字符序列一样。

列表很像其他编程语言中的数组,但列表能自动调整大小,支持切片表示法,拥有很多使用方便的函数。

元组类似于列表,但不可修改。因此元组占用的内存较少,可以作为字典键。

集合是可迭代的数据集,但其元素没有顺序,不存在重复元素。

5、字典

1. 字典简介

如果在其他编程语言中从未用过关联数组和散列表,那么不妨将字典与列表进行比较,以便能理解其用法。

  • 列表中的值可以通过整数索引进行访问,索引表示了给定值在列表中的位置。
  • 字典中的“值”通过“键”进行访问,键可以是整数、字符串或其他Python对象,同样表示了给定值在字典中的位置。换句话说,列表和字典都支持用索引来访问任意值,但是字典“索引”可用的数据类型比列表的索引要多得多。而且字典提供的索引访问机制与列表的完全不同。
  • 列表和字典都可以存放任何类型的对象。
  • 列表中存储的值隐含了按照在列表中的位置排序,因为访问这些值的索引是连续的整数。这种顺序可能会被忽略,但需要时就可以用到。存储在字典中的值相互之间没有隐含的顺序关系,因为字典的键不只是数字。注意,如果用字典的时候同时还需要考虑条目的顺序(指加入字典的顺序),那么可以使用有序字典。有序字典是字典类的子类,可从collections模块中导入。还可以用其他数据结构(通常是列表)来定义字典条目的顺序,显式地将顺序保存起来,但这不会改变普通字典没有隐式(内置)排序的事实。

尽管存在差异,但字典和列表的用法往往比较类似。首先,空字典的创建就很像空列表,但是用花括号代替了方括号:

>>> x = []
>>> y = {}

上面第一行新建了一个空列表并赋给了x。第二行新建了一个空字典并赋给了y。

字典创建完毕后,就可以像使用列表一样在里面存储数据值了:

>>> y[0] = 'Hello'
>>> y[1] = 'Goodbye'

仅是以上赋值操作,词典和列表就已经存在明显差异。如果对列表做同样的操作,就会引发错误。因为在Python中,对列表中不存在的位置赋值是非法的。

例如,对列表x的第0个元素赋值,就会收到错误消息:

>>> x[0] = 'Hello'
Traceback (innermost last):
  File "<stdin>", line 1, in ?
IndexError: list assignment index out of range

而对于字典来说,就不会有问题,新的位置会按需创建。

现在字典中已经有值了,可以访问和使用了:

>>> print(y[0])
Hello
>>> y[1] + ", Friend."
'Goodbye, Friend.'

总之,上述用法让字典与列表看起来非常相像。但是下面的用法就能看出两者的巨大差异了,字典可通过非整数键来存储并使用数据值:

>>> y["two"] = 2
>>> y["pi"] = 3.14
>>> y["two"] * y["pi"]
6.28

上述操作用列表是绝对不可能完成的!列表的索引必须是整数,而字典的键则没有什么限制,可以是数字、字符串或其他很多种Python对象。因此有很多列表无法完成的任务,用字典就能很自然地完成。例如,用字典实现电话簿程序,就比用列表更为合理一些,因为某人的电话号码可以按照这个人的姓氏对电话号码进行索引存储。

字典提供了一种映射手段,可从一组任意对象映射到有关联的另一组任意对象。现实世界中的字典、辞典和翻译书,就是字典的很好类比。为了说明这种类比的自然程度,下面给出了一段将颜色从英文转换为法文的定义:

>>> english_to_french = {}           ⇽---  创建空字典
>>> english_to_french['red'] = 'rouge'       ⇽---  存入3个单词
>>> english_to_french['blue'] = 'bleu'
>>> english_to_french['green'] = 'vert'
>>> print("red is", english_to_french['red'])  C     ⇽---  获取“red”对应的值
red is rouge

2. 字典的其他操作

除基础的元素赋值和访问操作之外,字典还支持很多其他操作。

可以显式地用逗号分隔的键/值对来定义字典:

>> english_to_french = {'red': 'rouge', 'blue': 'bleu', 'green': 'vert'}

len函数可以返回字典的条目数量:

可以用字典的keys方法获取字典中的所有键,这在用Python的for循环遍历字典内容时,常会用到。

>>> list(english_to_french.keys())
['green', 'blue', 'red']

在Python 3.5及以前的版本中,keys方法返回列表中的键是无序的。这些键没有经过排序,也不一定按照创建顺序排列。Python代码每次运行都可能打印出不同的键顺序。如果需要键有序排列,可以存入列表变量并对列表进行排序。但从Python 3.6开始,字典会维持键的创建顺序,并按创建顺序返回。

用values方法还能够获取到存储在字典中的所有值:

>>> list(english_to_french.values())
['vert', 'bleu', 'rouge']

values方法并没有keys方法那么常用。

用items方法可以将所有键及其关联值以元组序列的形式返回:

>>> list(english_to_french.items())
[('green', 'vert'), ('blue', 'bleu'), ('red', 'rouge')]

与keys方法类似,items方法通常与for循环结合使用,用于遍历字典中的内容。

del语句可用于移除字典中的条目,即键值对:

>>> list(english_to_french.items())
[('green', 'vert'), ('blue', 'bleu'), ('red', 'rouge')]
>>> del english_to_french['green']
>>> list(english_to_french.items())
[('blue', 'bleu'), ('red', 'rouge')]

keys、values和items方法的返回结果都不是列表,而是视图(view)。视图的表现与序列类似,但在字典内容变化时会动态更新。这就是为什么在以上示例中要用list函数将结果转换为列表。除此之外,它们的表现与序列相似,允许用for循环迭代,可用in来检查成员的资格,等等。

由keys方法(有时候是items方法)返回的视图还与集合有点类似,可进行并集、差集和交集操作。

如果要访问的键在字典中不存在,则会被Python视为出错。这时可以用in关键字先检测一下字典中是否存在该键。如果字典中该键对应有存储值,则返回True;否则返回False:

>>> 'red' in english_to_french
True
>>> 'orange' in english_to_french
False

或者还可以用get函数进行检测。如果字典中包含该键,则get函数返回与键关联的值。如果不包含则返回函数的第二个参数:

>>> print(english_to_french.get('blue', 'No translation'))
bleu
>>> print(english_to_french.get('chartreuse', 'No translation'))
No translation

第二个参数是可选的。如果未给出,则get函数会在字典键不存在时返回None。

类似的,如果要安全获取键值,确保能在值不存在时设为默认值,可以用setdefault方法:

>>> print(english_to_french.setdefault('chartreuse', 'No translation'))
No translation

get和setdefault方法的区别在于,以上setdefault调用完毕后,会在字典中生成键'chartreuse'和对应的值'No translation'。

用copy方法可以获得字典的副本:

>>> x = {0: 'zero', 1: 'one'}
>>> y = x.copy()
>>> y
{0: 'zero', 1: 'one'}

copy方法会生成字典的浅副本,大多数情况下应该能满足需要了。如果字典值中包含了可修改对象,如列表或其他字典,那就可能需要用到copy.deepcopy函数生成深副本。

字典的update方法会用第二个字典(即参数)的所有键/值对更新第一个字典(即调用者)。如果键在两个字典中都存在,则第二个字典中的值会覆盖第一个字典的值:

>>> z = {1: 'One', 2: 'Two'}
>>> x = {0: 'zero', 1: 'one'}
>>> x.update(z)
>>> x
{0: 'zero', 1: 'One', 2: 'Two'}

 字典的各个方法提供了一整套操作和使用字典的工具。

下表列出了主要的字典方法,以供快速参考。

字典操作 说明 示例
{} 新建空字典 x = {}
len 返回字典的条目数量 len(x)
keys 返回字典所有键的视图 x.keys()
values 返回字典所有值的视图 x.values()
items 返回字典所有条目的视图 x.items()
del 从字典中移除一个条目 del(x[key])
in 测试键是否存在于字典中 'y' in x
get 返回键的值或自定义的默认值 x.get('y', None)
setdefault 如果键在字典中存在则返回其对应值,否则在字典中设置该键为默认值并返回该值 x.setdefault('y', None)
copy 生成字典的浅副本 y = x.copy()
update 将两个字典的条目合并 x.update(z)

完整的列表参见Python标准库文档。

3. 单词计数

假定有一个文件中存放着一个单词列表,每个单词占一行。

如何才能知道每个单词在文件中出现的次数呢?

利用字典可以轻松完成这一任务:

>>> sample_string = "To be or not to be"
>>> occurrences = {}
>>> for word in sample_string.split():
...     occurrences[word] = occurrences.get(word, 0) + 1   ⇽---  ❶
...
>>> for word in occurrences:
...     print("The word", word, "occurs", occurrences[word], \
...            "times in the string")
...
The word To occurs 1 times in the string
The word be occurs 2 times in the string
The word or occurs 1 times in the string
The word not occurs 1 times in the string
The word to occurs 1 times in the string

在字典occurrences中,会对每个单词出现的次数进行累加❶。

以上例子很好演示了字典的强大威力。代码比较简单,由于在Python中对字典操作进行了高度优化,运行速度也会相当快。上述这种处理模式十分便捷,事实上已经被标准化为Counter类,内置于标准库的collections模块中。

4. 可用作字典键的对象

上面的例子用了字符串作为字典键。但是不仅是字符串,任何不可变(immutable)且可散列(hashable)的Python对象,都可被用作字典的键。

如前所述,Python中可修改的对象均被称为可变(mutable)对象。列表就是可变对象,因为列表的元素可被添加、更改和移除。同理,字典也是可变对象。数值类型是不可变对象。假如变量x原来指向数值3,将x赋为4之后,其实是让x指向了另一个数字4,但是数字3本身没有改动,3仍然是3。字符串也是不可变对象。list[n]返回list的第n个元素,string[n]返回string的第n个字符。list[n] = value将会改变list的第n个元素,但string[n] = character在Python中是非法操作,将会引发错误,因为Python中的字符串是不可变的。

然而,字典的键必须是不可变且可散列的,这意味着不能将列表用作字典键。但在许多时候,像列表一样的键用起来会很方便。例如,以姓和名作为键来保存人员信息,就能便于使用。如果用包含两个元素的列表作为字典键,就可以轻松完成这一任务。

Python提供了元组来解决上述问题,元组基本上可被视为不可变的列表。除一旦建立就不能修改之外,元组的创建和使用都类似于列表。此外,字典还有一个限制,即键还必须是可散列的,这比不可变的要求还要高。为了实现可散列,对象必须带有散列值(由__hash__方法提供),并且在值的整个生命周期内保持不变。这就意味着,虽然元组本身在技术上是不可变的,但是包含可变值的元组是不可散列的。只有不包含任何可变嵌套对象的元组才是可散列的,可有效地用作字典的键。

下表列出了哪些Python内置类型是不可变的、可散列的、有资格用作字典键的。

Python对象 不可变 可散列 可作为字典键
int
float
boolean
complex
str
bytes
bytearray
list
tuple 有时 有时
set
frozenset
dictionary

5. 稀疏矩阵

在数学术语中,矩阵(matrix)是指数字的二维网格,通常在教科书中会在两边加上方括号表示,如下所示:

 这种矩阵的标准表示方法就是列表的列表。在Python中,矩阵如下所示:

matrix = [[3, 0, -2, 11], [0, 9, 0, 0], [0, 7, 0, 0], [0, 0, 0, -5]]

矩阵中的元素可以通过行号和列号访问:

element = matrix[rownum][colnum]

在天气预报之类的应用中,矩阵往往十分庞大,每条边有数千个元素,这就意味着矩阵总共有数百万个元素,而且这种矩阵中很多元素常常都为0。在某些应用中,除少量元素之外,其他矩阵元素都可能为0。为了节省内存,往往会采用某种形式,实际只存储其中的非0元素。这种矩阵被称为稀疏矩阵。

用字典和索引的元组来实现稀疏矩阵,还是比较简单的。例如,上面的矩阵就可以表示如下:

matrix = {(0, 0): 3, (0, 2): -2, (0, 3): 11,
          (1, 1): 9, (2, 1): 7, (3, 3): -5}

然后就可以通过行号和列号访问各个矩阵元素了,代码如下:

if (rownum, colnum) in matrix:
    element = matrix[(rownum, colnum)]
else:
    element = 0

另一种稍难理解但效率更高的方案是使用字典的get方法。当get方法在字典中找不到键时会返回0,否则返回与该键关联的值,这样可以减少一次字典搜索操作:

element = matrix.get((rownum, colnum), 0)

如果要完成大量的矩阵操作,可能要深入研究一下专门的数值计算包NumPy。

6. 将字典用作缓存

将字典用作缓存(cache),也就是保存计算结果的数据结构,以免重复计算。假设要定义名为sole的函数,参数为3个整数,并将返回结果。

代码可能会如下所示:

def sole(m, n, t):
    # 执行某些相当耗时的计算
    return(result)

如果函数耗时过长,并且会被调用数万次,则程序运行就会十分缓慢。

现在假定无论程序运行多少次,调用sole的参数组合大约有200种。也就是说,程序运行时sole(12, 20, 6)可能会被调用50次或以上,其他很多参数组合也类似。因此通过消除参数相同时的重复计算,就可以节省大量时间。

这里可以用字典,并用元组作为字典键,例如:

sole_cache = {}
def sole(m, n, t):
    if (m, n, t) in sole_cache:
        return sole_cache[(m, n, t)]
    else:
        # 执行较为耗时的计算任务
        sole_cache[(m, n, t)] = result
        return result

 在以上经过重写的sole函数中,用一个全局变量来存储以前的结果。该全局变量是个字典,并且字典的键是作为参数组合已传给sole函数的元组。然后,只要传给sole函数的参数组合是曾经计算过结果的,就不会再次计算,而是直接返回保存过的结果。

7. 字典的效率

如果具有传统编译型语言的经验,大家可能会对是否使用字典而犹豫不决,担心字典的效率比列表或数组低。事实上Python字典的执行速度已经相当快了。Python语言的许多内部特性都依赖于字典,为提高字典的效率已经投入了大量的心血。

Python的所有数据结构都经过了高度优化,因此不应该花太多时间去考虑哪个更快,哪个效率更高。如果用字典可以比用列表更容易更清晰地解决问题,那就尽管用吧。仅当确认是字典导致了不可接受的速度下降时,再来考虑替代方案。

四、流程控制

1、while循环

之前已经多次用到了基本的while循环。

完整的while循环结构如下:

while condition:
    body
else:
    post-code

condition是一个布尔表达式,也就是运算结果为值True或False。只要condition为True,body部分的代码就会重复执行下去。如果condition的计算结果为False,则while循环将会执行post-code部分的代码,然后停止执行。如果condition一开始就为False,那么body部分代码就根本不会被执行,只会执行post-code部分的代码。body和post-code部分的代码都是由换行符分隔的一条或多条Python语句,并且代码缩进的级别也相同。Python解释器根据代码缩进的级别来识别这两个部分。这里不需要使用其他分隔符,如大括号或方括号。

注意,while循环的else部分是可选的,且不常用到。因为只要body部分没有包含break语句,那么循环:

while condition:
    body
else:
    post-code

和循环:

while condition:
    body 
post-code

的效果是一样的,而且第二种写法更简单,也更容易理解。当然,在某些场合else子句还是有点用的。

在while循环的body部分,可以使用两种特殊语句,也就是语句break和continue。如果执行了break语句,那么while循环就会立即终止,甚至都不会再执行post-code部分(当存在else子句时)。如果执行了continue语句,则会导致body部分的剩余语句被跳过,进行下一次condition计算,循环继续进行。

2、if-elif-else语句

在Python中,最通用的if-elif-else结构的形式如下:

if condition1:
    body1
elif condition2:
    body2
elif condition3:
    body3
.
.
.
elif condition(n-1):
    body(n-1)

else:
    body(n)

如果condition1为True,则执行body1;否则,如果condition2为True,则执行body2;否则……依此类推,直至遇到判断为True的条件或者else子句并执行body(n)。与while循环一样,body部分也由一条或多条Python语句构成,由换行符分隔并处于相同的缩进级别。

当然,并不是每个条件从句都必须存在。elif、else部分是可以省略的,两者可以都省略。如果没有符合条件的语句可执行(没有条件为True,也没有else部分),那就什么都不做。

if语句之后的body部分是必须提供的。但是这里可以使用pass语句,pass语句也可在Python中需要语句的其他任何地方使用。

pass语句用作语句的占位符,但是它不执行任何操作:

if x < 5:
    pass
else:
    x = 5

Python没有提供case(或switch)语句。

Python的case语句在哪里?

如前所述,Python中没有提供case语句。在大多数其他语言采用case或switch语句的场合,Python可以用串联的if...elif...elif...else结构来很好地应对。

如果遇到极少数棘手的场合,通常可用函数字典来解决,如下所示:

def do_a_stuff():
    #process a
def do_b_stuff():
    #process b
def do_c_stiff():
    #process c
func_dict = {'a' : do_a_stuff,
             'b' : do_b_stuff,
             'c' : do_c_stuff }
x = 'a'
func_dict[x]()                     ⇽---  运行字典中的函数

确实有人提出过要在Python中加入case语句的建议(参见PEP 275和PEP 3103),但总体的一致意见还是认为没有必要或不值得增加麻烦。

3、for循环

Python的for循环与某些编程语言的不大一样。传统模式是在每次迭代时递增并检测某个变量,C语言的for循环通常就是如此。在Python中,for循环遍历的是任何可迭代对象的返回值,也就是任何可以生成值序列的对象。

例如,for循环可以挨个遍历列表、元组或字符串的元素。这里的可迭代对象还可以是特殊的range函数,或者被称为生成器(generator)或生成器表达式的特殊类型函数。

这种生成器函数的功能非常强大,常见的形式为:

for item in sequence:
    body
else:
    post-code

对sequence的每个元素都会执行一次body部分的语句。一开始item会被设为sequence的第一个元素,并执行body部分;然后item会被设为sequence的第二个元素,并执行body部分,等等,对sequence的其余元素都会逐个依此处理。

else部分是可选的。像while循环的else部分一样,很少被用到。break和continue在for循环中的作用,和在while循环中是一样的。

以下循环将会打印出x中每个数字的倒数:

x = [1.0, 2.0, 3.0]
for n in x:
    print(1 / n)

1. range函数

有时候循环中需要显式的索引(如值在列表中出现的位置)。这时可以将range函数和len函数结合起来使用,生成供for循环使用的索引序列。以下代码将会打印列表中所有出现负数的位置:

x = [1, 3, -7, 4, 9, -5, 4]
for i in range(len(x)):
    if x[i] < 0:
        print("Found a negative number at index ", i)

对于给出的数字n,range(n)会返回0、1、2、……、n-2、n-1。因此,将列表长度(调用len函数)传入就会产生列表元素索引的序列。虽然看起来很类似,但range函数并不会构建一个整数值的列表,而是创建一个能够按需生成整数值的range对象。

如果要用显式循环遍历大型列表,这种设计就很有用。例如,不必建立包含1000万个元素的列表,这会占用相当多的内存,而是使用range(10000000),只会占用少量内存,一样能生成整数序列,从0开始,直至(不含)for循环需要用到的10000000。

2. 用初值和步进值控制range函数

range函数有两个变体,可以对生成的值序列施以更多的控制。

如果range函数带有两个数值参数,则第一个参数是结果序列的起始值,第二个参数是结果序列的结束值(不含)。

以下给出一些例子:

>>> list(range(3, 7))     ⇽---  ❶
[3, 4, 5, 6]
>>> list(range(2, 10))     ⇽---  ❶
[2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(5, 3))
[]

这里list()函数的作用,只是为了把range函数生成的值强制转换为列表。通常在实际的代码中不会这么用❶。

以上代码还不能实现倒计数,因此list(range(5, 3))的值会是空列表。要实现倒计数或1以外的步进计算,需要用到range函数的第三个可选参数,以便给出计数时的步进值:

>>> list(range(0, 10, 2))
[0, 2, 4, 6, 8]
>>> list(range(5, 0, -1))
[5, 4, 3, 2, 1]

range函数的返回序列中,始终会包含第一个参数给出的起始值,但不会包含第二个参数给出的结束值。

3. 在for循环中使用break和continue语句

在for循环体中,也可以使用break和continue这两种特殊语句。如果执行了break,就会立即终止for循环,甚至不会执行post-code部分(存在else子句时)。如果在for循环中执行了continue,则会导致body的其余部分被跳过,循环正常进入下一次迭代。

4. for循环和元组拆包

元组拆包操作可以让某些for循环变得简洁一些。以下代码将读取元组列表,每个元组中包含两个元素,计算每个元组中两个数的乘积并累计求和。

在某些领域这是一种常见的数学运算:

somelist = [(1, 2), (3, 7), (9, 5)]
result = 0
for t in somelist:
    result = result + (t[0] * t[1])

以下代码的效果相同,但更为简洁:

somelist = [(1, 2), (3, 7), (9, 5)]
result = 0

for x, y in somelist:
    result = result + (x * y)

上述代码在for关键字之后紧跟着用到了元组x,y,而不是平常的单个变量。在for循环的每次迭代时,x中包含的是list中当前元组的元素0,y包含了list中当前元组的元素1。

元组的这种用法是Python提供的一种便利手段,表示列表的每个元素都应该是大小一致的元组,以便能拆包到for关键字后面跟的元组变量中。

5. enumerate函数

通过组合使用元组拆包和enumerate函数,可以实现同时对数据项及其索引进行循环遍历。用法与range函数类似,优点是代码更清晰、更易理解。

与之前的例子类似,以下代码也将打印列表中所有出现负数的位置:

x = [1, 3, -7, 4, 9, -5, 4]
for i, n in enumerate(x):     ⇽---  ❶
    if n < 0:                 ⇽---  ❷
        print("Found a negative number at index ", i)     ⇽---  ❸

enumerate函数返回的是元组(索引,数据项)❶。这样可以不通过索引来访问列表的数据项❷。索引是可用的❸。

6. zip函数

在循环遍历之前将两个或以上的可迭代对象合并在一起,有时候会很有用。zip函数可以从一个或多个可迭代对象中逐一读取对应元素,并合并为元组,直至长度最短的那个可迭代对象读取完毕:

>>> x = [1, 2, 3, 4]      ⇽---  x包含4个元素
>>> y = ['a', 'b', 'c']     ⇽---  y包含3个元素
>>> z = zip(x, y)
>>> list(z)
[(1, 'a'), (2, 'b'), (3, 'c')]     ⇽---  z只包含3个元素

4、列表和字典推导式

利用for循环遍历列表、修改或选中某个元素、新建列表或字典,这些都是十分常见的用法。这时的循环往往如下所示:

>>> x = [1, 2, 3, 4]
>>> x_squared = []
>>> for item in x:
...     x_squared.append(item * item)
...
>>> x_squared
[1, 4, 9, 16]

这种用法太过普遍了,因此Python为这种操作提供了特殊的快捷写法,称为推导式(comprehension)。不妨将列表或字典推导式视作一条for循环语句,在一行代码中完成了由一个序列创建新列表或字典的操作。

列表推导式的用法如下:

new_list = [expression1 for variable in old_list if expression2]

字典推导式的用法如下:

new_dict = {expression1:expression2 for variable in list if expression3}

在这两种情况下,表达式的主体部分类似于for循环的开头for variable in list,再加上某些用于新建键或值的变量的表达式,以及可选的用于根据变量值决定是否要放入新列表或字典中的条件表达式。

以下代码和上面的示例功能相同,但是用到了列表推导式:

>>> x = [1, 2, 3, 4]
>>> x_squared = [item * item for item in x]
>>> x_squared
[1, 4, 9, 16]

这里甚至可以用if语句来筛选列表的项:

>>> x = [1, 2, 3, 4]
>>> x_squared = [item * item for item in x if item > 2]

>>> x_squared
[9, 16]

字典推导式也类似,但是需要同时给出键和值。如果想实现类似上面的例子,但要把数字作为字典键,数字的平方作为字典值,就可以用字典推导式来实现。

如下所示:

>>> x = [1, 2, 3, 4]
>>> x_squared_dict = {item: item * item for item in x}
>>> x_squared_dict
{1: 1, 2: 4, 3: 9, 4: 16}

列表和字典推导式非常灵活,也十分强大。用习惯后会让处理列表的操作简化许多。建议进行一些尝试,只要是编写for循环处理一系列项时,都可以尝试用推导式来实现。

生成器表达式(generator expression)类似于列表推导式,看起来与列表推导式十分相像,只是用圆括号代替了方括号而已。

以下示例是上述列表推导式的生成器表达式版本:

>>> x = [1, 2, 3, 4]
>>> x_squared = (item * item for item in x)
>>> x_squared
<generator object <genexpr> at 0x102176708>
>>> for square in x_squared:
...     print(square,)
...
1 4 9 16

注意,除方括号的变化之外,生成器表达式还不返回列表。如上所示,生成器表达式返回的是生成器对象,可用作for循环中的迭代器,这与range()函数的用途非常类似。使用生成器表达式的优点是,不会在内存中生成整个列表。因此可生成任意大小的值序列,内存开销也很小。

5、语句、代码块和缩进

在介绍流程控制结构时,首次用到了代码块和缩进,下面再来介绍一下这部分内容。

Python用语句缩进来确定流程控制结构不同代码块(即语句体)的边界。一个代码块由一条或多条语句组成,语句之间通常由换行符分隔。赋值语句、函数调用、print函数、占位用的pass语句和del语句都是Python语句。

代码流程控制结构(if-elif-else、while和for循环)则属于复合语句:

compound statement clause:
    block
compound statement clause:
    block

复合语句由一条或多条子句组成,每条子句后面跟着缩进代码块。复合语句可以像任何其他语句一样出现在代码块中,这样就创建了嵌套代码块。

大家也许还会碰到一些特殊的用法。如果语句之间用分号分隔,则可以在同一行放置多条语句。只包含单行语句的代码块,可以紧挨着复合语句子句的冒号,放在同一行中:

>>> x = 1; y = 0; z = 0
>>> if x > 0: y = 1; z = 10
... else: y = -1 
...
>>> print(x, y, z)
1 1 10

缩进有误的代码会引发异常,可能会遇到两种异常,第一种是:

>>>
>>>   x = 1
File "<stdin>", line 1
    x = 1
    ^
    IndentationError: unexpected indent
>>>

以上代码将不该缩进的行进行了缩进。在基本模式下,插入符“^”标出了出现问题的位置。

在IDLE Python shell中,无效缩进会高亮显示。如果代码没有按要求缩进(即复合语句子句之后的第一行),也会显示相同的消息。

这里有一种情况可能会引起困惑。如果编辑器将制表符显示为4个空格(或者是在Windows命令行交互模式下,提示符后面的第一个制表符只会缩进4个空格),有一行缩进4个空格,下一行用制表符缩进,那么这两行看起来就像处于同一缩进级别。

但是,仍然会收到以上异常,因为Python会将制表符映射为8个空格。避免上述问题的最好方法就是,在Python代码中只使用空格进行缩进。如果一定要用制表符缩进,或者正在处理采用了制表符的代码,请务必不要将制表符与空格混合使用。

在基础命令行交互模式和IDLE Python shell中,最外层的缩进块后面需多加一行空行: 

>>> x = 1
>>> if x == 1: 
...    y = 2
...    if v > 0:
...        z = 2
...        v = 0
...
>>> x = 2

在z = 2所在行后面,不需要添加空行。但是v = 0所在行之后则需要加上一行空行,如果是保存到文件中的模块代码,则不需要加入这个空行。

如果代码块中的语句缩进的字符数不足,将会引发第二种缩进异常:

>>> x = 1
>>> if x == 1:    
           y = 2
        z = 2
File "<stdin>", line 3
       z = 2
       ^
    IndentationError: unindent does not match any outer indentation level

在上述例子中,z = 2所在行没有正确对齐y = 2所在行的下方。这种格式很少出现,但还要再提一下。因为这种情况可能会引起困惑。

Python对缩进量没有限制,也不会报错。只要保持同一个代码块中的缩进量一致,缩进多少字符都没有关系。请勿滥用这种灵活性,推荐标准是每级缩进使用4个空格。

缩进级别越多,跨行情况当然就越普遍。可以显式地用反斜杠符将一行代码拆分开。在一对()、{}或[]之内,也可以隐式地在单词之间随意拆分。也就是在输入列表、元组、字典中的值、函数调用的参数、方括号中的任意表达式时。

一条语句的后续行,可以缩进任意级别:

>>> print('string1', 'string2', 'string3' \
...    ,  'string4', 'string5')    
string1 string2 string3 string4 string5
>>> x = 100 + 200 + 300 \
...    + 400 + 500
>>> x
1500
>>> v = [100, 300, 500, 700, 900,
...    1100, 1300]
>>> v
[100, 300, 500, 700, 900, 1100, 1300]
>>> max(1000, 300, 500,
...        800, 1200)
1200
>>> x = (100 + 200 + 300
...          + 400 + 500)
>>> x
1500

字符串也可以用“\”拆分成多行,但用于表示缩进的制表符或空格符将会成为字符串的一部分,而且“\”必须是行末最后一个字符。

为了避免这种情况,请记住任何由空白字符分隔的字符串都会被Python解释器自动拼接起来:

>>> "strings separated by whitespace " \
...    """are automatically""" ' concatenated'
'strings separated by whitespace are automatically concatenated'
>>> x = 1
>>> if x > 0:
...        string1 = "this string broken by a backslash will end up \
...                with the indentation tabs in it"
...
>>> string1
'this string broken by a backslash will end up \t\t\twith
     the indentation tabs in it'
>>> if x > 0:
...        string1 = "this can be easily avoided by splitting the " \
...            "string in this way"
...
>>> string1
'this can be easily avoided by splitting the string in this way'

6、布尔值和布尔表达式

在上述代码流程控制的示例中,显然用到了条件判断,但并没有真正解释清楚什么是Python中的True和False,以及哪些表达式可用于条件判断。

Python有一种布尔对象类型,可以被赋为True或False。所有布尔操作表达式都返回True或False。

1. 大多数Python对象都能用作布尔类型

C语言用整数0表示False,其他整数均表示True,Python对待布尔值的方式与C语言类似。Python对此做了推广,用0或空值表示False,其他任何值都是True。在实际应用中,这意味着:

  • 数字0、0.0和0+0j都是False,其他数字均为True;
  • 空串""是False,其他字符串均为True;
  • 空列表[]是False,其他列表均为True;
  • 空字典{}是False,其他字典均为True;
  • 空集合()是False,其他集合均为True;
  • Python的特殊值None始终为False;

通常情况下,Python的其他数据结构同样适用于上述规则。如果数据结构为空或0,则在布尔上下文中将被视为False,否则被视为True。某些对象(如文件对象和编码对象)对0或空元素没有明确的定义,这些对象也不应该在布尔上下文中使用。

2. 比较操作符和布尔操作符

用普通的<、<=、>、>=等操作符,就可以进行对象之间的比较。==是判断相等操作符,!=是判断不等操作符。in和not in操作符用于判断是否属于序列(列表、元组、字符串和字典)的成员,操作符is和is not用于判断两个对象是否为同一个对象。

通过and、or、not操作符,可以将返回布尔值的多个表达式组合成更为复杂的表达式。以下代码将判断变量是否位于指定的大小区间内:

if 0 < x and x < 10:
    ...

Python为以上这种特殊类型的复合语句提供了很棒的简写形式,可以像在数学论文中一样写为:

if 0 < x < 10:
    ...

优先级的规则有很多,如果不太确定,则可以用圆括号来确保Python按要求解释表达式。如果表达式比较复杂,无论是否必要,用圆括号可能都是比较好的做法,因为这可以让以后维护代码的人确切地理解代码的意图。有关优先级的更多详细信息,参见Python文档。

接下来将会介绍一些更为高级的内容。如果是Python的初学者,或许现在可以先跳过以下内容。

操作符and和or将会返回对象。and操作符要么返回第一个为False的对象(表达式的计算结果),要么返回最后一个对象。同理,or操作符要么返回第一个为True的对象,要么返回最后一个对象。这看起来有点令人困惑,但合乎常理。只要带有and操作的表达式中有一个元素为False,就会导致整个表达式的计算结果为False,并返回False。

如果所有元素均为True,则整个表达式为True,返回的最后一个元素肯定也为True。反过来说,对于or操作也是合理的。只要有一个元素为True,整条语句就为True,并返回第一个为True的元素。如果没有元素为True,则返回最后一个肯定为False的元素。

换句话说,就像很多其他编程语言一样,只要or操作符遇到计算结果为True的表达式,或者and操作符遇到计算结果为False的表达式,那么对整个表达式的计算过程就会终止:

>>> [2] and [3, 4]
[3, 4]
>>> [] and 5
[]
>>> [2] or [3, 4]
[2]
>>> [] or 5
5
>>>

操作符==和!=将会检查操作对象是否包含同样的值。大多数情况下,用到的都是==和!=,而不是is和is not。

is和is not用来判断操作对象是否为同一个对象:

>>> x = [0]
>>> y = [x, 1]
>>> x is y[0]        ⇽---  x和y指向同一个对象
True
>>> x = [0]          ⇽---  x被赋予另一个对象
>>> x is y[0]
False
>>> x == y[0]
True

7、编写简单的文本文件分析程序

为了更好地了解Python程序的运行原理,这里介绍一个简单的示例,大致重现了UNIX的wc工具程序,可将文件的行数、单词数和字符数显示出来。

下面代码所示的程序是特意为Python新手编写的,尽量进行了简化。

word_count.py:

#!/usr/bin/env python3

""" Reads a file and returns the number of lines, words, 
    and characters - similar to the UNIX wc utility
"""
infile = open('word_count.tst')       ⇽---  打开文件

lines = infile.read().split("\n")     ⇽---  读取文件,按行拆分

line_count = len(lines)     ⇽---  用len()获取行数
word_count = 0              ⇽---  初始化计数器
char_count = 0

for line in lines:     ⇽---  遍历每一行

    words = line.split()     ⇽---  拆分为单词
    word_count += len(words)

    char_count += len(line)     ⇽---  获取字符数

print("File has {0} lines, {1} words, {2} characters".format  ⇽---  打印结果
                             (line_count, word_count, char_count))

如果要测试以上程序,可以针对以下示例文件运行一下。

word_count.tst:

Python provides a complete set of control flow elements, 
including while and for loops, and conditionals. 
Python uses the level of indentation to group blocks 
of code with control elements.

运行word_count.py之后,输出结果将会如下:

naomi@mac:~/quickpythonbook/code $ python3.1 word_count.py 
File has 4 lines, 30 words, 189 characters

以上代码可以对Python程序有个大致概念。代码不长,大部分工作都是由for循环中的3行代码完成的。

事实上,该程序还可以写得更简短流畅一些。大多数Python高手,都将简洁视为Python的强大优势之一。

五、函数

1、基本的函数定义

Python函数定义的基本语法如下:

def name(parameter1, parameter2, . . .):
    body

与代码流程控制结构一样,Python用缩进来界定函数体。以下示例将之前计算阶乘的代码放入函数体中,这样只需调用fact函数即可得到阶乘值了:

>>> def fact(n):
...     """ Return the factorial of the given number. """    ⇽---  ❶
...     r = 1
...     while n > 0:
...         r = r * n
...         n = n - 1
...     return r           ⇽---  ❶
...

第二行❶是可选的文档字符串(docstring),可通过fact.__doc__读取其值。文档字符串用于描述函数对外表现出来的功能及所需的参数,而注释(comment)则是记录代码工作原理的内部信息。文档字符串紧随在函数定义的第一行后面,通常用3重引号包围起来,以便能跨越多行。代码助手只会提取文档字符串的第一行。标准的多行文档字符串写法,是在第一行中给出函数的概述,第二行是空行,然后是其余的详细信息。return语句之后的值将会返回给函数的调用者❷。

在某些编程语言中,无返回值的函数被称为“过程”。虽然Python允许编写不含return语句的函数,但这些函数还不是真正的过程。所有的Python过程都是函数。如果过程体没有显式地执行return语句,则会返回特殊值None。如果执行了return arg语句,则值arg会被立即返回。return语句执行之后,函数体中的其余语句都不会执行。因为Python没有真正的过程,所以均被称为“函数”。

虽然Python函数都带有返回值,但是否使用这个返回值则由写代码的人决定:

>>> fact(4)     ⇽---  ❶
24     ⇽---  ❷
>>> x = fact(4)     ⇽---  ❸
>>> x
24
>>>

一开始返回值没有与任何变量关联❶,fact函数的值只是被解释器打印出来而已❷。然后返回值与变量x关联❸。

2、多种函数参数

大多数函数都需要形参,每种编程语言都有各自的函数形参定义规则。Python非常灵活,提供了3种函数形参的定义方式。

1. 按位置给出形参

在Python中,最简单的函数传形参方式就是按位置给出。在函数定义的第一行中,可以为每个形参指定变量名称。当调用函数时,调用代码中给出的形参将按顺序与函数的形参变量逐一匹配。

以下函数计算x的y次幂:

>>> def power(x, y):
...     r = 1
...     while y > 0:
...         r = r * x
...         y = y - 1
...     return r

>>> power(3, 3)
27

上述用法要求,调用代码使用的形参数量与函数定义时的形参数量应完全匹配,否则会引发TypeError:

>>> power(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: power() missing 1 required positional argument: 'y'
>>>

函数的形参可以有默认值,可以在函数定义的第一行中给出该默认值,如下所示: 

def fun(arg1, arg2=default2, arg3=default3, ...)

可以为任何数量的形参给出默认值。带默认值的形参必须位于形参列表的末尾,因为与大多数编程语言一样,Python也是根据位置来把实参与形参匹配起来的。给函数的实参数量必须足够多,以便让形参列表中最后一个不带默认值的形参能获取到实参。

以下函数同样也会计算x的y次幂。但如果在函数调用时没有给出y,则会用默认值2,于是就成了计算平方的函数:

>>> def power(x, y=2):
...     r = 1
...     while y > 0:
...         r = r * x
...         y = y - 1
...     return r

以下交互式会话演示了默认实参的效果:

>>> power(3, 3)
27
>>> power(3)
9

2. 按形参名称传递实参

也可以使用对应的函数形参的名称将实参传给函数,而不是按照形参的位置给出。继续上面的交互示例,可以键入:

>>> power(2, 3) 
8
>>> power(3, 2)
9
>>> power(y=2, x=3)
9

最后提交给power函数的实参带了名称,因此与顺序无关。实参与power函数定义中的同名形参关联起来,得到的是3^2的结果。这种实参传递方式被称为关键字传递(keyword passing)。

如果函数需要带有大量实参,并且大多数实参都有默认值,那么联合使用关键字传递和默认实参功能可能就非常有用了。

例如,有个生成当前目录下文件信息清单的函数,可用布尔型实参指定清单中是否要包含每个文件的大小、最后修改日期等信息。

函数定义如下所示:

def list_file_info(size=False, create_date=False, mod_date=False, ... ):
    ...获取文件名...  
    if size:
        # 获取文件大小
    if create_date:
        # 获取文件的创建日期
    # 其他功能
    return fileinfostructure

然后用关键字传递方式调用,指明需要包含的文件信息(在本例中为文件大小和修改日期,但不是创建日期):

fileinfo = list_file_info( size = True,mod_date = True)

这种参数处理方式特别适用于非常复杂的函数,图形用户界面(GUI)中常会用到。如果用过Tkinter包建立Python的GUI程序,就会发现这种可选的关键字命名实参是非常有用的。

3. 变长实参

Python函数也可以定义为实参数量可变的形式,定义方式有两种。一种用于处理实参预期相对明了的情况,实参列表尾部数量不定的实参将会被放入一个列表中。另一种方式可将任意数量的关键字传递实参放入一个字典中,这些实参均是在函数形参列表中不存在同名形参的。

下面将介绍这两种机制。

1)位置实参数量不定时的处理

当函数的最后一个形参名称带有“*”前缀时,在一个函数调用中所有多出来的非关键字传递实参(即这些按位置给出的实参未能赋给合适的形参)将会合并为一个元组赋给该形参。下面用这种简单方式来实现一个求数字列表中最大值的函数。

首先,实现函数:

>>> def maximum(*numbers):
...     if len(numbers) == 0:
...         return None
...     else:
...         maxnum = numbers[0]
...         for n in numbers[1:]:
...             if n > maxnum:
...                 maxnum = n
...         return maxnum
...

接下来,测试该函数的功能:

>>> maximum(3, 2, 8)
8
>>> maximum(1, 5, 9, -2, 2)
9

2)关键字传递实参数量不定时的处理

按关键字传递的实参数量不定时,也能进行处理。如果形参列表的最后一个形参前缀为“**”,那么所有多余的关键字传递实参将会被收入一个字典对象中。字典的键为多余实参的关键字(形参名称),字典的值为实参本身。这里的“多余”是指,传递实参的关键字匹配不到函数定义中的形参名称。

例如:

>>> def example_fun(x, y, **other):
...     print("x: {0}, y: {1}, keys in 'other': {2}".format(x, 
...           y, list(other.keys())))
...     other_total = 0
...     for k in other.keys():
...         other_total = other_total + other[k]
...     print("The total of values in 'other' is {0}".format(other_total))

在交互会话中测试一下,以上函数可以处理用关键字foo和bar传入的实参,即便foo和bar不属于函数定义中给出的形参名也没问题:

>>> example_fun(2, y="1", foo=3, bar=4)
x: 2, y: 1, keys in 'other': ['foo', 'bar']
The total of values in 'other' is 7

4. 多种参数传递方式的混用

Python函数的所有实参传递方式可以同时使用,但一不小心就可能会引起混乱。混合使用多种实参传递方式的一般规则是,先按位置传递实参,接着是命名实参,然后是带单个“*”的数量不定的位置传递实参,最后是带“**”的数量不定的关键字传递实参。详细信息参见官方文档。

3、将可变对象用作函数实参

函数的实参传递的是对象的引用,形参则成为指向对象的新引用。对于不可变对象(如元组、字符串和数值),对形参的操作不会影响函数外部的代码。但如果传入的是可变对象(如列表、字典或类的实例),则对该对象做出的任何改动都会改变该实参在函数外引用的值。函数内部对形参的重新赋值不会影响实参:

>>> def f(n, list1, list2):
...     list1.append(3)
...     list2 = [4, 5, 6]
...     n = n + 1
...
>>> x = 5
>>> y = [1, 2]
>>> z = [4, 5]
>>> f(x, y, z)
>>> x, y, z
(5, [1, 2, 3], [4, 5])

在函数f()开始执行时,各初始变量和函数形参分别都指向同一个对象。

函数f()执行完毕后,y(函数内的list1)引用的值已经发生了变化,而n和list2则指向了不同的对象。变量x没有变化,因为x是不可变的。而函数形参n则被指向了新的值6。同理,变量z没有变化,因为在函数f内,对应的形参list2被指向了新的对象[4,5,6]。只有y发生了变化,因为其指向的实际列表发生了变化。

4、局部变量、非局部变量和全局变量

下面回顾一下fact函数的定义:

>>> def fact(n):
        """ 返回给定值的阶乘 """
        r = 1
        while n > 0:
            r = r * n
            n = n - 1
        return r

变量r和n对于fact函数的任何调用都是局部(local)的,在函数执行期间,它们的变化对函数外部的任何变量都没有影响。函数形参列表中的所有变量,以及通过赋值(如fact函数中的r = 1)在函数内部创建的所有变量,都是该函数的局部变量。

在使用变量之前,用global语句对其进行声明,可以显式地使其成为全局(global)变量。函数可以访问和修改全局变量。全局变量存在于函数之外,所有将其声明为全局变量的其他函数,以及函数之外的代码,也可以对其进行访问和修改。

以下示例演示了局部变量和全局变量的差异:

>>> def fun():
...     global a
...     a = 1
...     b = 2
...

以上示例中定义的函数,将a视为全局变量,而视b为局部变量,并对a和b进行了修改。

下面测试一下上述函数:

>>> a = "one"
>>> b = "two"

>>> fun()
>>> a
1
>>> b
'two'

在fun函数内对a的赋值,同时也是对fun函数外部现存的全局变量a进行操作。因为a在fun函数中被指定为global,所以赋值会将该全局变量从"one"修改为1。对b来说则不一样,fun函数内部名为b的局部变量一开始指向fun函数外部的变量b的相同值,但赋值操作让b指向了函数fun内的新值。

函数闭包,即内部函数可以不经声明直接引用外部函数的变量。对于文中的顶级变量b,则不适用,会报“UnboundLocalError”错误。

nonlocal语句与global语句类似,它会让标识符引用最近的闭合作用域(enclosing scope)中已绑定的变量。global语句是对顶级变量使用的,而nonlocal语句则可引用闭合作用域中的全部变量,如下代码所示。

nonlocal.py文件:

g_var = 0          ⇽---  inner_test函数中的g_var绑定为同名的顶级变量
nl_var = 0
print("top level-> g_var: {0} nl_var: {1}".format(g_var, nl_var))
def test(): 
    nl_var = 2     ⇽---  inner_test函数中的nl_var绑定为test函数中的同名变量
    print("in test-> g_var: {0} nl_var: {1}".format(g_var, nl_var))
    def inner_test():
        global g_var   ⇽---  inner_test函数中的g_var绑定为同名的顶级变量
        nonlocal nl_var   ⇽---  inner_test函数中的nl_var绑定为test函数中的同名变量
        g_var = 1
        nl_var = 4
        print("in inner_test-> g_var: {0} nl_var: {1}".format(g_var,
            nl_var))

    inner_test()
    print("in test-> g_var: {0} nl_var: {1}".format(g_var, nl_var))

test()
print("top level-> g_var: {0} nl_var: {1}".format(g_var, nl_var))

上述代码运行后会打印出以下结果:

top level-> g_var: 0 nl_var: 0
in test-> g_var: 0 nl_var: 2
in inner_test-> g_var: 1 nl_var: 4
in test-> g_var: 1 nl_var: 4
top level-> g_var: 1 nl_var: 0

注意,顶级变量nl_var的值没有受到影响。如果inner_test函数中包含global nl_var语句,那么nl_var的值就会受影响了。

最起码的一点就是,如果想对函数之外的变量赋值,就必须将其显式声明为nonlocal或global。但如果只是要访问函数外的变量,则不需要将其声明为nonlocal或global。如果Python在函数本地作用域中找不到某变量名,就会尝试在全局作用域中查找。

因此,对全局变量的访问会自动发送给相应的全局变量。个人不建议使用这种便捷方式。如果所有全局变量都被显式地声明为global,阅读代码的人就会看得更清楚。以后,则还可能有机会将全局变量的使用限制在函数内部,仅限极少数情况下才会用到。

5、将函数赋给变量

与其他Python对象一样,函数也可以被赋值,如下所示:

>>> def f_to_kelvin(degrees_f):     ⇽---  定义f_to_kelvin函数
...     return 273.15 + (degrees_f - 32) * 5 / 9
...
>>> def c_to_kelvin(degrees_c):     ⇽---  定义c_to_kelvin函数
...     return 273.15 + degrees_c
...
>>> abs_temperature = f_to_kelvin     ⇽---  将f_to_kelvin函数赋给变量
>>> abs_temperature(32)
273.15
>>> abs_temperature = c_to_kelvin     ⇽---  将c_to_kelvin函数赋给变量
>>> abs_temperature(0)
273.15

函数可以被放入列表、元组或字典中:

>>> t = {'FtoK': f_to_kelvin, 'CtoK': c_to_kelvin}     ⇽---  ❶
>>> t['FtoK'](32)       ⇽---  访问字典中的f_to_kelvin函数
273.15
>>> t['CtoK'](0)      ⇽---  访问字典中的c_to_kelvin函数
273.15

引用函数的变量,用起来与函数完全相同❶。

最后一个例子演示了如何使用字典调用各个函数,只要通过用作字符串键的值即可。在需要根据字符串值选择不同函数的情况下,这种模式就很常用。

很多时候,这种用法代替了C和Java等语言中的switch结构。

6、lambda表达式

上面那种简短的函数,还可以用lambda表达式来定义:

lambda parameter1, parameter2, . . .: expression

lambda表达式是匿名的小型函数,可以快速地在行内完成定义。通常小型函数是要被传给另一个函数的,例如,列表的排序方法用到的键函数。这种情况下,通常没有必要定义一个大型函数,而且在使用的地方以外定义也会显得很别扭。

典就可以在一处完成全部定义:

>>> t2 = {'FtoK': lambda deg_f: 273.15 + (deg_f - 32) * 5 / 9,
...       'CtoK': lambda deg_c: 273.15 + deg_c}     ⇽---  ❶
>>> t2['FtoK'](32)
273.15

以上示例将lambda表达式定义为字典值❶。

注意,lambda表达式没有return语句,因为表达式的值将自动返回。

7、生成器函数

生成器(generator)函数是一种特殊的函数,可用于定义自己的迭代器(iterator)。在定义生成器函数时,用关键字yield返回每一个迭代值。当没有可迭代值,或者遇到空的return语句或函数结束时,生成器函数将停止返回值。

与普通的函数不同,生成器函数中的局部变量值会保存下来,从本次调用保留至下一次调用:

>>> def four():
...     x = 0     ⇽---  将x的初始值设为0
...     while x < 4:
...         print("in generator, x =", x)
...         yield x     ⇽---  返回x的当前值
...         x += 1     ⇽---  x递增1
...
>>> for i in four():
...       print(i)
...
in generator, x = 0
0
in generator, x = 1
1
in generator, x = 2
2
in generator, x = 3
3

注意,以上生成器函数包含一个while循环,限定了生成器执行的次数。根据使用方式的不同,调用无停止条件的生成器函数可能会导致无限循环。

从Python 3.3开始,除yield之外,为生成器函数新增了关键字yield from。从本质上说,yield from使将生成器函数串联在一起成为可能。yield from的执行方式与yield相同,但是会将当前生成器委托(delegate)给子生成器。简单一点的话,可以如下使用:

>>> def subgen(x):
...      for i in range(x):
...          yield i
...
>>> def gen(y):
...      yield from subgen(y)
...
>>> for q in gen(6):
...      print(q)
...
0
1
2
3
4
5

以上示例允许将yield表达式移出主生成器,方便了代码重构。

还可以对生成器函数使用in,以便检查某值是否属于生成器生成的一系列值:

>>> 2 in four()
in generator, x = 0
in generator, x = 1
in generator, x = 2
True
>>> 5 in four()
in generator, x = 0
in generator, x = 1
in generator, x = 2
in generator, x = 3
False

8、装饰器

如上所述,因为函数是Python的一级对象(first-class),所以能被赋给变量。函数也可以作为实参传递给其他函数,还可作为其他函数的返回值回传。

例如,可以编写一个Python函数,它把其他函数作为形参,并将这个形参包入另一个执行相关操作的新函数中,然后返回这个新函数。

这个新的函数组合可用于替换原来的函数:

>>> def decorate(func):
...     print("in decorate function, decorating", func.__name__)
...     def wrapper_func(*args):
...         print("Executing", func.__name__)
...         return func(*args)
...     return wrapper_func
...   
>>> def myfunction(parameter):
...     print(parameter)
...   
>>> myfunction = decorate(myfunction)
in decorate function, decorating myfunction
>>> myfunction("hello")
Executing myfunction
hello

装饰器(decorator)就是上述过程的语法糖(syntactic sugar),只增加一行代码就可以将一个函数包装到另一个函数中去。效果与上述代码完全相同,不过最终的代码则更加清晰易懂。

装饰器用起来十分简单,由两部分组成:先定义用于包装或“装饰”其他函数的装饰器函数;然后立即在被包装函数的定义前面,加上“@”和装饰器函数名。

这里的装饰器函数应该是以一个函数为形参,返回值也是一个函数,如下所示:

>>> def decorate(func):
...     print("in decorate function, decorating", func.__name__)     ⇽---  ❶
...     def wrapper_func(*args):
...         print("Executing", func.__name__)
...         return func(*args)
...     return wrapper_func     ⇽---  ❷
...   
>>> @decorate     ⇽---  ❸
... def myfunction(parameter):
...     print(parameter)
...   
in decorate function, decorating myfunction           
>>> myfunction("hello")     ⇽---  ❹
Executing myfunction
hello

当定义要包装的函数时,上面的decorate函数会把该函数的名称打印出来❶。装饰器函数最后将会返回包装后的函数❷。通过使用@decorate,myfunction就被装饰了起来❸。被包装的函数将会在装饰器函数执行完毕后调用❹。

装饰器可将一个函数封装到另一个函数中,这样就可以方便地实现很多目标了。在Django之类的Web框架中,装饰器用于确保用户在执行函数之前已经处于登录状态了。在图形库中,装饰器可用来向图形框架中注册函数。

总结:

在函数内部,可以使用global语句访问外部变量。

实参的传递可以根据位置,也可以根据形参的名称。

函数形参可以有默认值。

函数可以把多个实参归入元组,以便能定义实参数量不定的函数。

函数可以把多个实参归入字典,以便能定义实参数量不定的函数,其中实参按照形参的名称传入。

函数是Python的一级对象,也就是说函数可以被赋给变量,可以通过变量来访问,可以被装饰。

六、类与面向对象编程

1、类

1. 定义类

Python中的类(class)实际上就是数据类型。Python的所有内置数据类型都是类,Python提供了强大的工具,用于对类的所有行为进行控制。

类可用class语句进行定义:

class MyClass:
    body

body部分由一系列Python语句组成,通常包含变量赋值和函数定义语句。不过赋值和函数定义语句都不是必需的,body可以只包含一条pass语句。

为了能让类标识符足够醒目,按惯例每个单词首字母应该大写。类定义完之后,只要将类名称作为函数进行调用,就可以创建该类的对象,即类的实例:

instance = MyClass()

类的实例可被当作结构(structure)或记录(record)使用。与C结构或Java类不同,类实例的数据字段不需要提前声明,可以在运行时再创建。

下面的小例子定义了一个名为Circle的类,创建了Circle的实例,并给其radius字段赋值,然后用该字段计算出圆的周长:

>>> class Circle:
...     pass
...
>>> my_circle = Circle()
>>> my_circle.radius = 5
>>> print(2 * 3.14 * my_circle.radius)
31.4

与Java及许多其他语言一样,实例/结构的数据字段通过句点表示法来访问和赋值。

通过在类的定义中包含初始化方法__init__,可以实现对实例的字段进行自动初始化。每次创建类的新实例时,该函数都会运行,新建实例本身将作为函数的第一个参数self代入。__init__方法类似于Java中的构造器,但其实什么都没有构造,只是可以用于初始化类的字段。与Java和C++不同,Python的类只能包含一个__init__方法。

以下示例将会创建默认半径为1的圆:

class Circle:
    def __init__(self):     ⇽---  ❶
        self.radius = 1
my_circle = Circle()     ⇽---  ❷
print(2 * 3.14 * my_circle.radius)     ⇽---  ❸
6.28
my_circle.radius = 5     ⇽---  ❹
print(2 * 3.14 * my_circle.radius)     ⇽---  ❺
31.400000000000002

按照惯例,__init__方法的第一个参数名始终是self。当__init__运行时,self会被置为新建的Circle实例❶。接下来就会用到类的定义。首先创建一个Circle实例对象❷。下一行代码是基于radius字段已被初始化这一事实❸。radius字段的值可以被覆盖❹,这就导致了最后一行打印出的结果与上一条print语句的不一样了❺。

Python还有更像构造器的__ new__方法,在对象创建时将会被调用,返回未经初始化的对象。除非要创建str或int这种不可变类型的子类,或者要通过元类(metaclass)修改对象的创建过程,否则很少会覆盖已有的__new__方法。

利用真正的OOP方式,能够做的事情还有很多很多。如果对OOP还不熟悉,建议去学习一下。

2. 实例变量

实例变量是OOP最基本的特性。再来看一下Circle类:

class Circle:
    def __init__(self):
        self.radius = 1

radius就是Circle的实例变量。也就是说,Circle类的每个实例都拥有各自的radius副本,每个实例存储在副本radius中的值可以各不相同。在Python中,可以按需创建实例变量,只要给类实例的字段赋值即可:

instance.variable = value

如果变量尚不存在,则会自动创建,__init__正是如此创建radius变量的。

实例变量在每次使用时,不论是赋值还是访问,都需要显式给出包含该变量的实例,即采用instance.variable的格式。单单引用variable并不是对实例变量的引用,而是对当前执行的方法中的局部变量的引用。这与C++和Java不同,它们对实例变量的引用方式与局部的函数变量相同。个人更喜欢Python这种显式给出实例的要求,因为能清楚地分辨出实例变量和局部函数变量。

3. 方法

方法是与某个类关联的函数。上面已经介绍了特殊的__init__方法,当创建实例时会对新实例调用该方法。在以下示例中,Circle类定义了另一个方法area,用于计算并返回Circle实例的面积。与大部分用户自定义的方法一样,调用area采用的是方法调用(invocation)语法,类似于对实例变量的访问方式:

>>> class Circle:
...     def __init__(self):
...         self.radius = 1
...     def area(self):
...         return self.radius * self.radius * 3.14159
...
>>> c = Circle() 
>>> c.radius = 3
>>> print(c.area())
28.27431

方法调用语法包括实例名,加上一个句点,再加上要在该实例上调用的方法。当以这种方式调用方法时,就是一种已绑定(bound)的方法调用。

但是,方法还可以用非绑定(unbound)的方式调用,即通过类来访问方法。这种用法不太方便,几乎没人使用。因为在非绑定方法调用时,第一个参数必须是定义该方法的类的实例,调用关系不够清晰:

>>> print(Circle.area(c))
28.27431

与__init__类似,area方法被定义为类定义内部的函数。方法的第一个参数一定是发起调用的实例,按惯例命名为self。在许多编程语言中,这时的实例常被命名为this,而且是作为隐含参数的,从来不会被显式传递。但Python的设计理念是,更愿意让事情明确。

如果方法定义了能接受的参数,就可以在调用时使用。以下版本的Circle为__init__方法添加了一个参数,以便能创建给定半径的圆,而无须在对象创建之后再另行设置:

class Circle:
    def __init__(self, radius):
        self.radius = radius
    def area(self):
        return self.radius * self.radius * 3.14159

注意,这里用到了两个radius。self.radius是实例变量,单个radius则是局部的函数参数。这两个radius可不是一回事!在实际编程时,局部的函数参数也许应该用r或rad之类的名称,以免混淆。

有了上述定义的Circle,就能通过调用一次Circle类生成任意半径的圆对象了。以下代码将创建半径为5的Circle对象:

c = Circle(5)

Python的所有标准函数特性,都可以用于方法,这些特性如参数默认值、不定参数、关键字参数等。

__init__的第一行可以定义为:

def __init__(self, radius=1):

然后在调用Circle时,带或不带参数都是可以的。Circle()将返回半径为1的圆,Circle(3)将返回半径为3的圆。

Python的方法调用毫无神奇之处,可以看成是普通函数调用的简写。对于方法调用instance.method(arg1, arg2, ...),Python将按以下规则将其转换为普通的函数调用。

(1)先在实例的命名空间中查找方法名。如果方法在该实例中被修改或添加过,那就会优先调用该实例中的方法,而不是类或父类(superclass)中的方法。

(2)如果在实例的命名空间中找不到该方法,就会找到实例的类型,也就是其所属的类,并在其中查找该方法。在以上示例中,实例c的类型为Circle,也就是说c属于Circle类。

(3)如果方法还未找到,就查找父类中的方法。

(4)如果方法找到了,就会像普通的Python函数一样被直接调用,函数的第一个参数将是instance,方法调用中的其他参数则整体向右平移一个位置传入函数。因此instance.method(arg1, arg2, ...)就会成为class.method(instance, arg1, arg2, ...)。

4. 类变量

类变量(class variable)是与类关联的变量,而不是与类的实例关联,并且可供类的所有实例访问。类变量可用于保存类级别的数据,例如,在某一时刻已创建了多少个该类的实例。尽管类变量的使用需要比其他大多数语言多花点工夫,Python还是提供了支持。并且还需要关注类和实例变量之间的交互方式。

类变量是通过类定义代码中的赋值语句创建的,而不是在__init__函数中创建的。类变量创建之后,就可被类的所有实例看到。

可以用类变量创建pi值,供Circle类的所有实例访问:

class Circle:
    pi = 3.14159
    def __init__(self, radius):
        self.radius = radius
    def area(self):
        return self.radius * self.radius * Circle.pi

有了这一定义,就可以键入:

>>> Circle.pi
3.14159
>>> Circle.pi = 4
>>> Circle.pi
4
>>> Circle.pi = 3.14159
>>> Circle.pi
3.14159

以上例子完全按照对类变量的预期执行,类变量与定义它的类关联并位于其内部。注意,该例中对Circle.pi的访问,都是在类实例创建之前。Circle.pi的存在,显然是不依赖于Circle类的任何特定实例。

在类的方法中也可以访问类变量,只要带上类名即可。在Circle.area的定义中就是这么做的,这里的area函数明确引用了Circle.pi。实际运行达到了预期效果,从类中获取了正确的pi值并用于计算:

>>> c = Circle(3)
>>> c.area()
28.27431

或许有人会反对在上述类的方法中把类名写死。通过特殊的__class__属性可以避免这种写法,该属性可供Python类的所有实例访问。

__class__属性会返回实例所属的类,例如:

>>> Circle
<class '__main__.Circle'>
>>> c.__class__
<class '__main__.Circle'>

名为Circle的类在系统内部是用一个抽象数据结构表示的,该数据结构正是从c的__class__属性获取的,c就是Circle类的一个实例。以下示例由c获取Circle.pi的值,而无须显式引用Circle类名:

>>> c.__class__.pi
3.14159

在area方法内部,就可以用这种写法摆脱对Circle类的显式引用,只要用self.__class__.pi替换Circle.pi即可。

如果不加了解,那么类变量的特异之处可能会带来问题。Python在查找实例变量时,如果找不到具有该名称的实例变量,就会在同名的类变量中查找并返回类变量值。只有在找不到合适的类变量时,Python才会报错。

类变量可以高效地实现实例变量的默认值,只需创建一个具有合适默认值的同名类变量,就能避免每次创建类实例时初始化该实例变量的时间和内存开销。但这也很容易在无意之中引用了实例变量而不是类变量,不会有任何报错信息。

首先,尽管c未包含名为pi的关联实例变量,但可以引用变量c.pi。Python首先会寻找实例变量pi。如果找不到实例变量,Python就会查找Circle并找到类变量pi:

>>> c = Circle(3)
>>> c.pi
3.14159

上述结果可能是满足需求的,也可能不满足。这种技术用起来很方便,但容易出错,所以请小心使用。

如果尝试将c.pi当作真正的类变量来使用,在某个实例中对其进行修改,并希望让所有实例都要看到这种修改,那将会发生什么?这里用到了Circle之前的定义:

>>> c1 = Circle(1)
>>> c2 = Circle(2)
>>> c1.pi = 3.14
>>> c1.pi
3.14
>>> c2.pi
3.14159
>>> Circle.pi
3.14159

以上例子并没有像真正的类变量那样工作,c1现在有了自己的pi副本,与c2访问的Circle.pi并不相同。因为对c1.pi的赋值在c1中新建了一个实例变量,它不会对类变量Circle.pi产生任何影响,所以才会如此。

后续对c1.pi的查找都会返回这个实例变量的值。而后续对c2.pi的查找则会先在c2中查找实例变量pi,可是没有找到,然后就会转而返回类变量Circle.pi的值。如果需要更改类变量的值,请通过类名进行访问,而不要通过实例变量self。

5. 静态方法和类方法

Java之类的编程语言还带有静态方法,Python类也拥有与静态方法明确对应的方法。此外,Python还拥有类方法,要比静态方法更高级一些。

1)静态方法

与Java一样,即便没有创建类的实例,静态方法也是可以调用的,当然通过类实例来调用也是可以的。请用@staticmethod装饰器来创建静态方法,代码如下所示。

circle.py文件:

"""circle module: contains the Circle class."""
class Circle:
    """Circle class"""
    all_circles = []       ⇽---  该类变量包含所有已创建实例的列表
    pi = 3.14159
    def __init__(self, r=1):
        """Create a Circle with the given radius"""
        self.radius = r
        self.__class__.all_circles.append(self)     ⇽---  如果实例已被初始化过了,就把自己加入all_circles列表
    def area(self):
        """determine the area of the Circle"""
        return self.__class__.pi * self.radius * self.radius

    @staticmethod
    def total_area():
        """Static method to total the areas of all Circles """
        total = 0
        for c in Circle.all_circles:
            total = total + c.area()
        return total

然后在交互模式下输入:

>>> import circle
>>> c1 = circle.Circle(1)
>>> c2 = circle.Circle(2)
>>> circle.Circle.total_area()
15.70795
>>> c2.radius = 3
>>> circle.Circle.total_area()
31.415899999999997

注意,这里用到了文档字符串。在实际的模块代码中,可能还会加入更多的信息字符串,在类的文档字符串中给出可用的方法,在方法的文档字符串中包括用法信息:

>>> circle.__doc__
'circle module: contains the Circle class.'
>>> circle.Circle.__doc__
'Circle class'
>>> circle.Circle.area.__doc__
'determine the area of the Circle'

2)类方法

类方法与静态方法很相像,都可以在类的对象被实例化之前进行调用,也都能通过类的实例来调用。但是类方法隐式地将所属类作为第一个参数进行传递,因此代码可以更简单,代码如下所示。

circle_cm.py文件:

"""circle_cm module: contains the Circle class."""
class Circle:
    """Circle class"""
    all_circles = []    ⇽---  该变量包含所有已创建实例的列表
    pi = 3.14159
    def __init__(self, r=1):
        """Create a Circle with the given radius"""
        self.radius = r
        self.__class__.all_circles.append(self)
    def area(self):
        """determine the area of the Circle"""
        return self.__class__.pi * self.radius * self.radius

    @classmethod     ⇽---  ❶
    def total_area(cls):     ⇽---  ❷
        total = 0
        for c in cls.all_circles:     ⇽---  ❸
            total = total + c.area()
        return total
>>> import circle_cm
>>> c1 = circle_cm.Circle(1)
>>> c2 = circle_cm.Circle(2)
>>> circle_cm.Circle.total_area()
15.70795
>>> c2.radius = 3
>>> circle_cm.Circle.total_area()
31.415899999999997

这里def方法前面加上了装饰器@classmethod❶。类作为参数,按惯例命名为cls❷。然后就可以用cls代替self.__class__❸。

利用类方法而不是静态方法,可以不必将类名硬编码写入total_area。这样,Circle的所有子类仍然可以调用total_area,但引用的是自己的成员而不是Circle的成员。

2、类与继承

1. 继承

因为Python的动态性,对语言没有加太多限制,所以其继承机制要比Java和C ++等编译型语言更加简单灵活。

为了了解如何在Python中使用继承,可先从Circle类开始,再推而广之。

不妨再定义一个正方形类Square:

class Square:
    def __init__(self, side=1):
        self.side = side            ⇽---  正方形的边长

现在,如果要在绘图程序中使用这些类,必须定义每个实例在绘图表面的位置信息。在每个实例中定义x、y坐标,即可实现这一点:

class Square:
    def __init__(self, side=1, x=0, y=0):
        self.side = side
        self.x = x
        self.y = y
class Circle:
    def __init__(self, radius=1, x=0, y=0):
        self.radius = radius
        self.x = x
        self.y = y

这种方式能起作用,但如果要扩展大量的形状类,就会产生大量重复代码,因为可能要让每种形状类都具备这种位置的概念。

毫无疑问,这正是在面向对象语言中使用继承的标准场景。不用在每个形状类中都定义变量x和y,而可以将各种形状抽象为一个通用的Shape类,并让定义具体形状的类继承自该通用类。在Python中,定义方式如下:

class Shape:
    def __init__(self, x, y):
        self.x = x
       self.y = y
class Square(Shape):       ⇽---  声明Square继承自Shape
    def __init__(self, side=1, x=0, y=0):
        super().__init__(x, y)     ⇽---  Shape的__init__方法必须得调用
        self.side = side
class Circle(Shape):     ⇽---  声明Circle继承自Shape
    def __init__(self, r=1, x=0, y=0):
        super().__init__(x, y)     ⇽---  Shape的__init__方法必须得调用
        self.radius = r

在Python中使用继承类通常有两个要求,在Circle类和Square类的粗体代码中可以看到这两个要求。第一个要求是定义继承的层次结构,在用class关键字定义类名之后的圆括号中,给出要继承的类即可。在上述代码中,Circle和Square都继承自Shape。第二个要求比较微妙一些,就是必须显式调用被继承类的__init__方法。Python不会自动执行初始化操作,但可以用super函数让Python找到被继承的类。初始化的工作在示例中由super().__ init __(x, y)这行代码来完成,这里将调用Shape的初始化函数,用适当的参数初始化实例。如果没有显式调用父类的初始化方法,则本例中的Circle和Square的实例就不会给实例变量x和y赋值。

可以不用super来调用Shape的__init__,而是用Shape.__init__(self, x, y)显式给出被继承类的名字,同样能够实现在实例初始化完毕后调用Shape的初始化函数。从长远来看,这种做法不够灵活,因为对被继承类名进行了硬编码。如果日后整体设计和继承架构发生了变化,这就可能成为问题。但在继承关系比较复杂的时候,采用super会比较麻烦。因为这两种方案无法完全混合使用,所以请把代码中采用的方案清楚地记录在文档中备查。

如果方法未在子类或派生类中定义,但在父类中有定义,继承机制也会生效。为了查看这种继承的效果,请在Shape类中再定义一个move方法,表示移动到指定位置。该方法将会把实例的x和y坐标修改为参数指定的值。

Shape的定义现在变成了:

class Shape:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def move(self, delta_x, delta_y):
        self.x = self.x + delta_x
        self.y = self.y + delta_y

如果这个Shape定义与之前的Circle、Square一起输入完毕,就可以进行以下的交互式会话:

>>> c = Circle(1)
>>> c.move(3, 4)
>>> c.x 
3
>>> c.y
4

如果在交互式会话中执行上述代码,请务必在新定义的Shape类之后将Circle类的代码重新录入一遍。

以上示例中的Circle类本身没有定义move方法,但由于继承自实现move的类,因此Circle的所有实例都可以使用move方法。用比较传统的OOP术语来描述,就是所有Python方法都是虚方法。也就是说如果方法在当前类中不存在,则会在父类中逐级搜索,并采用第一个找到的方法。

2. 类及实例变量的继承

实例可以继承类的属性。实例变量是和对象实例关联的,某个名称的实例变量在一个实例中只会存在一个。

看一下以下示例,这里会用到以下类的定义:

class P:
    z = "Hello"
    def set_p(self):
        self.x = "Class P"
    def print_p(self):
        print(self.x)
class C(P):
    def set_c(self):
        self.x = "Class C"
    def print_c(self):
        print(self.x)

执行以下代码:

>>> c = C()
>>> c.set_p()
>>> c.print_p()
Class P
>>> c.print_c()
Class P
>>> c.set_c()
>>> c.print_c()
Class C
>>> c.print_p()
Class C

上述示例中的对象c是类C的实例。C继承自P,但c并非继承自类P的某个不可见的实例,而是直接从P继承方法和类变量的。因为只存在一个实例c,在c的方法调用中,对实例变量x的任何引用都只能指向c.x。在c上无论调用哪个类定义的方法,均会如此。如上所示,由c调用的set_p和print_p,都是在类P里定义的,且都引用了同一个变量,在c上调用set_c和print_c时,引用的也是这个变量。

通常这正是实例变量应有的表现,因为对同一个名称的实例变量的引用,就应该指向同一个变量。不过有时也会有不同需求,可通过私有变量来实现。

类变量是支持继承的,但应该避免命名冲突。在以下示例中,父类P中定义了类变量z,并且通过以下3种方式都能被访问到:实例c、派生类C或直接用父类P:

>>> c.z; C.z; P.z
'Hello'
'Hello'
'Hello'

但如果通过类C来对类变量z赋值,就会在类C中创建一个新的类变量。这对P的类变量本身(通过P访问)没有影响。但以后通过类C或其实例c看到的,将会是这个新的变量,而不是原来的变量:

>>> C.z = "Bonjour"
>>> c.z; C.z; P.z
'Bonjour'
'Bonjour'
'Hello'

如果通过实例c来对z赋值,同样也会创建一个新的实例变量,最终会得到3个不同的变量:

>>> c.z = "Ciao"
>>> c.z; C.z; P.z
'Ciao'
'Bonjour'
'Hello'

3. Python类继承案例

首先,创建基类:

class Shape:
    def __init__(self, x, y):   ⇽---  __init__方法的参数为实例self和两个坐标
        self.x = x       ⇽---  通过self访问实例变量
        self.y = y
    def move(self, delta_x, delta_y):   ⇽---  move方法的参数为实例self和两个坐标的偏移量
        self.x = self.x + delta_x           
        self.y = self.y + delta_y   ⇽---  在move方法中对实例变量赋值

接下来,创建由基类Shape继承而来的子类:

class Circle(Shape):        ⇽---  类Circle继承自类Shape
    pi = 3.14159       ⇽---  pi和all_circles都是Circle的类变量
    all_circles = []
    def __init__(self, r=1, x=0, y=0):   ⇽---  Circle的__init__参数为实例self和3个带默认值的坐标
        super().__init__(x, y)   ⇽---  Circle的__init__通过super()调用Shape的__init__
        self.radius = r
        self.__class__.all_circles.append(self)  ⇽---在__init__方法中将实例加入all_circles列表
    @classmethod       ⇽---  total_area是类方法,参数为类本身cls
    def total_area(cls):
        area = 0
        for circle in cls.all_circles:
                 area += cls.circle_area(circle.radius)     ⇽---  通过参数cls访问静态方法circle_area
        return area
    @staticmethod
    def circle_area(radius):     ⇽---  circle_area是不用self和cls做参数的静态方法
        return Circle.pi * radius * radius   ⇽---  访问类变量pi,也可以用__class__.pi

下面可以创建Circle类的一些实例,看看它们的表现如何。因为Circle的__init__方法的参数都带有默认值,所以创建Circle对象时可以不带任何参数:

>>> c1 = Circle()
>>> c1.radius, c1.x, c1.y
(1, 0, 0)

 如果给出了参数,那就会用来设置实例变量值:

>>> c2 = Circle(2, 1, 1)
>>> c2.radius, c2.x, c2.y
(2, 1, 1)

在调用move()方法时,Python在Circle类中无法找到move(),因此会沿着继承架构向上查找,采用Shape的move()方法:

>>> c2.move(2, 2)
>>> c2.radius, c2.x, c2.y
(2, 3, 3)

同时,因为在__init__方法里已经把所有实例添加到一个列表(这是一个类变量)当中,所以可以获取到当前的Circle实例:

>>> Circle.all_circles
[<__main__.Circle object at 0x7fa88835e9e8>, <__main__.Circle object at 
     0x7fa88835eb00>]
>>> [c1, c2]
[<__main__.Circle object at 0x7fa88835e9e8>, <__main__.Circle object at 
     0x7fa88835eb00>]

通过Circle类本身或是其实例,也可以调用Circle类方法total_area():

>>> Circle.total_area()
15.70795
>>> c2.total_area()
15.70795

通过类本身或其实例,也可以调用静态方法circle_area()。作为一个静态方法,circle_area没有用参数传递实例或类,它的行为更像是类命名空间内的独立函数。实际上,静态方法相当常见的用途,就是把工具函数与类绑定在一起:

>>> Circle.circle_area(c1.radius)
3.14159
>>> c1.circle_area(c1.radius)
3.14159

4. 私有变量和私有方法

所谓私有变量或私有方法,是指其在定义它们的类的方法之外无法看到。私有变量和私有方法的用途有两个。一是可以通过有选择地拒绝对对象实现的重要或敏感部分的访问,以增强安全性和可靠性。二是可以防止由继承产生的名称冲突。类可以定义自己的私有变量,同时其父类也可定义同名的私有变量,这完全没有问题,因为变量是私有的,可以保证拥有各自的副本。因为显式标明了仅供类内部使用,所以私有变量能让代码更容易阅读。除私有内容之外,其他全都是类可以与外部交互的接口了。

大多数编程语言在定义私有变量时,都是通过类似“private”的关键字来实现的。Python中的约定比较简单,也更容易对是否私有一目了然。名称以双下划线“__”开头但不以它结尾的方法或实例变量,都是私有的,其他则都不是私有部分。

以下是类定义的示例:

class Mine:
    def __init__(self):
        self.x = 2
        self.__y = 3     ⇽---  名称前面加了双下划线,__y就定义为私有变量
    def print_y(self):
        print(self.__y)

根据上述定义,创建类的实例:

>>> m = Mine()

x不是私有变量,所以可以被直接访问:

>>> print(m.x)
2

而__y则是私有变量,直接访问会引发错误:

>>> print(m.__y)
Traceback (innermost last):
  File "<stdin>", line 1, in ?
AttributeError: 'Mine' object has no attribute '__y'

print_y方法不是私有的。不过因为它是在类Mine内部,所以可以访问__y并将其打印出来:

>>> m.print_y()
3

最后有一点应当引起注意,如果代码被编译为字节码(bytecode),则隐私保护机制会对私有变量和私有方法的名称进行修饰(mangle)处理。具体来说,就是给变量名称加上“_类名”前缀:

>>> dir(m)
['_Mine__y', 'x', ...]

加上类前缀的目的,是为了防止被意外访问到。要是有人想要访问,仍然可以有意模拟这种改动并访问到变量值。但这种改动增加了可读性,让代码调试起来更加容易。

5. 用@property获得更为灵活的实例变量

Python允许程序员直接访问实例变量,不必采用方法getter和setter这种额外机制,那是Java和其他面向对象语言中经常采用的做法。没了getter和setter,能让Python类更易于编写,代码也更简洁。但在某些场合,使用方法getter和setter可能会比较方便。假如在把值赋给实例变量之前就要先读取到该值,或者需要方便地动态计算出属性值。这两种情况用方法getter和setter就能胜任,但代价是享受不到Python实例变量访问的便利性了。

Python的解决方案就是使用属性(property)。属性既能够通过类似getter和setter的方法间接访问到实例变量,又能用上直接访问实例变量的句点表示法。

为方法加上property装饰符,就能创建属性,方法的名称就是属性名:

class Temperature:
    def __init__(self):
        self._temp_fahr = 0
    @property
    def temp(self):
        return (self._temp_fahr - 32) * 5 / 9

不带setter方法时,属性就是只读的。如果要能修改属性,需要添加setter方法:

@temp.setter    
def temp(self, new_temp):
    self._temp_fahr = new_temp * 9 / 5 + 32

然后就可以用标准的句点表示法来读写属性temp了。注意,上面setter方法名与属性名是相同的,但装饰器改成了属性名(这里为temp)加.setter,表示正在定义temp属性的setter:

>>> t = Temperature()
>>> t._temp_fahr
0
>>> t.temp
-17.77777777777778  

>>> t.temp = 34     ⇽---  ❶
>>> t._temp_fahr
93.2    
>>> t.temp     ⇽---  ❷
34.0

_temp_fahr存放的0在被返回之前,会被转换为摄氏度❶。34则会由setter转换回华氏度❷。

Python加入了对属性的支持,可带来一个很大的好处,即初始开发时可以采用传统的实例变量,以后可根据需要随时随地无缝切换为属性,客户端代码则完全无须改动。访问的方式仍然不变,还是句点表示法。 

6. 类实例的作用域规则和命名空间

在类的方法中,可以直接访问局部命名空间(在方法内声明的参数和变量)、全局命名空间(在模块级别声明的函数和变量)以及内置命名空间(内置函数和内置异常)。三者将按以下顺序进行查找:本地命名空间、全局命名空间、内置命名空间,如下图所示。

通过self变量也能访问到实例的命名空间(实例变量、私有实例变量和父类的实例变量)、类的命名空间(方法、类变量、私有方法和私有类变量)以及父类的命名空间(父类方法和父类的类变量)。

这3种命名空间的查找顺序是:实例、类、父类:

通过self变量无法访问到私有父类实例变量、私有父类方法和私有父类类变量。类能够对其继承者隐藏这些名称。

cs.py文件中的模块,将两个示例整合在了一起,详细演示了在方法中能够访问到哪些内容。

cs.py文件:

"""cs module: class scope demonstration module."""
mv ="module variable: mv"
def mf():
    return "module function (can be used like a class method in " \
           "other languages): mf()"
class SC:
    scv = "superclass class variable: self.scv"
    __pscv = "private superclass class variable: no access"
    def __init__(self):
        self.siv = "superclass instance variable: self.siv " \
                   "(but use SC.siv for assignment)"
        self.__psiv = "private superclass instance variable: " \
                      "no access"
    def sm(self):
        return "superclass method: self.sm()"
    def __spm(self):
        return "superclass private method: no access"
class C(SC):
    cv = "class variable: self.cv (but use C.cv for assignment)"
    __pcv = "class private variable: self.__pcv (but use C.__pcv " \
            "for assignment)"
    def __init__(self):
        SC.__init__(self)
        self.__piv = "private instance variable: self.__piv"
    def m2(self):
        return "method: self.m2()"
    def __pm(self):
        return "private method: self.__pm()"
    def m(self, p="parameter: p"):
        lv = "local variable: lv"
        self.iv = "instance variable: self.xi"
        print("Access local, global and built-in " \
              "namespaces directly")
        print("local namespace:", list(locals().keys()))
        print(p)     ⇽---  参数

        print(lv)     ⇽---  局部变量
        print("global namespace:", list(globals().keys()))

        print(mv)     ⇽---  模块变量
        print(mf())     ⇽---  模块函数
        print("Access instance, class, and superclass namespaces " \

              "through 'self'")
        print("Instance namespace:",dir(self))
        print(self.iv)     ⇽---  实例变量

        print(self.__piv)     ⇽---  私有实例变量

        print(self.siv)     ⇽---  父类的实例变量
        print("Class namespace:",dir(C))
        print(self.cv)     ⇽---  类变量

        print(self.m2())     ⇽---  方法

        print(self.__pcv)     ⇽---  私有类变量

        print(self.__pm())     ⇽---  私有方法
        print("Superclass namespace:",dir(SC))
        print(self.sm())     ⇽---  父类的方法

        print(self.scv)     ⇽---  通过实例访问父类的类变量

输出的信息相当多,所以下面将分段查看。

第一部分,类C的方法m的本地命名空间中,包含了参数self(self是实例变量)和p,还有局部变量lv,这些全都可以直接访问:

>>> import cs
>>> c = cs.C()
>>> c.m()
Access local, global and built-in namespaces directly
local namespace: ['lv', 'p', 'self']
parameter: p
local variable: lv

接下来,在方法m的全局命名空间中,包含了模块变量mv和模块函数mf。模块函数可用于提供类方法的功能。模块中还定义了类C和父类SC,这些类全都可以直接访问:

global namespace: ['C', 'mf', '__builtins__', '__file__', '__package__', 
    'mv', 'SC', '__name__', '__doc__']
module variable: mv
module function (can be used like a class method in other languages): mf()

 在实例c的命名空间中,包含了实例变量iv和父类的实例变量siv。siv与常规的实例变量没有区别。还包含了名称经过修饰的私有实例变量__piv(可以通过self访问)和父类私有实例变量__psiv(无法访问):

Access instance, class, and superclass namespaces through 'self'
Instance namespace: ['_C__pcv', '_C__piv', '_C__pm', '_SC__pscv',  
    '_SC__psiv', '_SC__spm', '__class__', '__delattr__', '__dict__', 
    '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
    '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', 
    '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
    '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 
    '__weakref__', 'cv', 'iv', 'm', 'm2', 'scv', 'siv', 'sm']
instance variable: self.xi 
private instance variable: self.__piv
superclass instance variable: self.siv (but use SC.siv for assignment)

在类C的命名空间中,包含了类变量cv和名称经过修饰的私有类变量__pcv,两者都可以通过self访问,但需要通过类C才能对它们赋值。类C还包含两个类方法m和m2,以及名称经过修饰的私有方法__pm(可通过self访问): 

Class namespace: ['_C__pcv', '_C__pm', '_SC__pscv', '_SC__spm', '__class__', 
    '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__ge__', 
    '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', 
    '__lt__', '__module__', '__ne__', '__new__', '__reduce__', 
    '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', 
    '__subclasshook__', '__weakref__', 'cv', 'm', 'm2', 'scv', 'sm'] 
class variable: self.cv (but use C.cv for assignment)
method: self.m2() 
class private variable: self.__pcv (but use C.__pcv for assignment) 
private method: self.__pm()

最后一部分,在父类SC的命名空间中,包含了父类类变量scv和父类方法sm。scv可以通过self访问,但赋值时需要通过父类SC。这里还包含了名称经过修饰的私有父类方法__spm和私有父类类变量__pscv,两者均无法通过self访问: 

Superclass namespace: ['_SC__pscv', '_SC__spm', '__class__', '__delattr__', 
    '__dict__', '__doc__', '__eq__', '__format__', '__ge__', 
    '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', 
    '__lt__', '__module__', '__ne__', '__new__', '__reduce__', 
    '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', 
    '__subclasshook__', '__weakref__', 'scv', 'sm'] 
superclass method: self.sm() 
superclass class variable: self.scv

以上算是第一个完整剖析的示例,可作为自学时的参考和基础。与大多数其他Python概念一样,通过尝试一些简化的示例,就可以对运行状况获得充分的理解。

7. 多重继承

多重继承(multiple inheritance)是指对象从多个父类继承数据和行为,编译型语言对多重继承的使用做了严格的限制。例如,在C++中,多重继承的使用规则非常复杂,很多人都敬而远之。在Java中,不允许多重继承,尽管Java确实提供了接口机制。

Python对多重继承没有类似的限制。类可以继承自任意数量的父类,方式与从单个父类继承是一样的。最简单的情况是,所有类(包括通过父类间接继承的类)都不包含实例变量或同名的方法。在这种情况下,继承类的行为就像是自己和全部祖先类定义的整合。假设类A继承自类B、C和D,类B又继承自类E和F,类D则继承自类G,如下图所示。

再假设这些类中没有相同的方法名。这时,类A的实例就像是类B、C、D、E、F、G中的任一实例一样,类B的实例就像类E或F的实例一样,类D的实例则像是类G的实例一样。

如果写成代码,类的定义将如下所示:

class E:
        . . .
class F:
        . . .
class G:
        . . .
class D(G):
        . . .
class C:
        . . .
class B(E, F):
        . . .
class A(B, C, D):

如果有多个类共用相同的方法名时,情况会复杂一些,因为Python必须确定哪个名称才是要用的。假设要在类A的实例a上对方法a.f()的调用进行解析,这里的f并不是在A中定义的,而是在F、C和G中全都有定义。那么将会调用哪个方法呢?

答案取决于Python查找父类的顺序,如果方法在初始发起调用的类中没有定义,Python就会按照该顺序进行查找。在最简单的情况下,Python将按照从左到右的顺序查找所有基类。但在进入下一个基类之前,总是会先查看当前基类的所有祖先类。

在执行a.f()时,查找过程将如下所示:

(1)Python首先会在调用对象的类(也就是类A)中查找。

(2)因为类A没有定义方法f,所以Python会开始查找A的所有基类。A的第一个基类是B,因此Python会开始在B中查找。

(3)因为类B没有定义方法f,所以Python会继续查找B的基类。开始在B的第一个基类E中查找。

(4)类E仍然没有定义方法f,且没有基类,因此对E就没什么可查的了。Python会回到类B中,在其下一个基类F中查找。

类F中确实包含方法f,并且因为是第一个按名称f找到的,所以就会被采用。而类C和G中名为f的方法将会被忽略。

当然,按照这种内部查找的逻辑,程序的可读性和可维护性都不会是最好的。对于更加复杂的类层次结构,会有其他的查找策略因素加入进来,以确保同一个类不会被检索两次,并支持对super的共同调用(cooperative call)。

不过实践中碰到的层次结构,可能没有以上这么复杂。如果能坚持采用更为标准的多重继承结构,如混合类(mixin)或扩展类(addin),就可以轻松保持可读性并避免命名冲突。

有些人坚持认为,多重继承是件坏事情。它的确会被误用,Python并没有强行要求用它。创建过深的继承层次结构可能是一种最危险的事情之一,使用多重继承有时会有助于防止这种情况的发生。

这里只是演示了多重继承在Python中的工作过程,而不会解释其使用场景(如在混合类或扩展类中)。 

3、类高级特性

1. 析构函数和内存管理

上面已经介绍了类的初始化函数(__init__方法),还可以为类定义析构函数(Destructor)。但与C++语言不同,Python并不是一定要创建并调用析构函数,才能确保释放实例占用的内存。Python通过引用计数机制,提供了自动内存管理。

也就是说,Python会跟踪实例的引用数量。当引用数为0时,实例占用的内存将会被回收,并且任何被实例引用的Python对象的引用计数都会减1。析构函数似乎始终都没有定义的必要。

在删除对象时,偶尔会碰到需要显式重新分配外部资源的场合。这种场合的最佳做法是使用上下文管理器,可以用标准库中的contextlib模块创建自定义的上下文管理器。

2. 类型即对象

对于很多编程语言来说,几乎只要考虑数据类型就够了。但Python的类型是动态确定的,也就意味着数据类型是在运行时确定的,而不是在编译时。这正是Python易于使用的原因之一,也使得可以(有时是必须)用对象的类型(不只是对象本身)进行计算。

启动Python会话,测试以下代码:

>>> type(5)
<class 'int'>
>>> type(['hello', 'goodbye'])
<class 'list'>

以上示例首次演示了Python内置的type函数,可以被任何Python对象调用,返回该对象的类型。在以上示例中,大家可能都已知道了,type函数显示5是int(整数),而['hello', 'goodbye']是list。

更有意义的是,调用type之后Python返回的是对象,<class'int'>和<class'list'>是返回对象的显示形式。调用type(5)后返回的是哪种对象呢?用一个简单的方法就可以找到答案了,只要对结果再调用一次type即可:

>>> type_result = type(5)
>>> type(type_result)
<class 'type'>

type返回的对象类型就是<class'type'>,可以称其为类型对象(type object)。类型对象是另一种Python对象,其唯一突出的特征就是名称有时会引起混淆。把类型对象说成是<class 'type'>类型,这个解释的明晰程度几乎与老Abbott和Costello的喜剧节目“Who’s on First?”一样,仍是含混不清。

3. 类型的使用

现在清楚了,Python的数据类型可以被视为类型对象,那么可以进行哪些操作呢?

因为两个Python对象之间可以相互比较,所以对类型也可以进行比较:

>>> type("Hello") == type("Goodbye")
True
>>> type("Hello") == type(5)
False

"Hello"和"Goodbye"的类型是相同的,它们都是字符串。而"Hello"和5的类型是不同的。别的先不说,至少可以用这种技术在函数和方法的定义中对类型进行检查。

4. 类型和用户自定义类

对象的类型能让人感兴趣,最常见的理由就是可以识别出某个特定的对象是否为某个类的实例,特别是针对用户自定义类的实例。在确定了对象是某一类型后,代码就可以做出相应的处理。举个例子就可解释得更清楚一些了。

首先定义几个空的类,以便建立起一种简单的继承层次结构:

>>> class A:
...     pass
...
>>> class B(A):
...     pass
...

下面创建类B的一个实例:

>>> b = B()

正如预期的那样,对b调用type函数,将会显示b为类B的实例,定义在当前的__main__命名空间内:

>>> type(b)
<class '__main__.B'>

通过访问实例的特殊属性__class__,也可以获取到完全一样的信息:

>>> b.__class__
<class '__main__.B'>

下面还要用该类提取更多信息,所以先把它保存起来:

>>> b_class = b.__class__

为了强调Python中的一切皆为对象,以下可以证明由b获取到的类就是定义在B名下的类:

>>> b_class == B
True

在本例中,其实不需要把b的类保存下来,因为它已经存在了。但这是为了清楚展示出类就是另一种Python对象,可以像任何其他Python对象一样保存或传递。

有了b的类,就可以通过__name___属性得到类的名称。

>>> b_class.__name__
'B'

通过访问__bases__属性,还可以找到类是从哪些类继承而来的,该属性包含了该类的全部基类的元组:

>>> b_class.__bases__
(<class '__main__.A'>,)

将__class__、__bases__和__name__属性都一起用上,就能够对任一实例的类继承结构进行完整的分析了。

不过isinstance和issubclass这两个内置函数,提供了一种更加友好的手段,可以获取到大部分常用信息。

例如,要想确定传入函数或方法的类是否为预期类型,就应该采用isinstance函数:

>>> class C:
...     pass
...
>>> class D:
...     pass
...
>>> class E(D):
...     pass
...
>>> x = 12
>>> c = C()
>>> d = D()
>>> e = E()
>>> isinstance(x, E)
False
>>> isinstance(c, E)     ⇽---  ❶
False
>>> isinstance(e, E)
True
>>> isinstance(e, D)     ⇽---  ❷
True
>>> isinstance(d, E)     ⇽---  ❸
False
>>> y = 12
>>> isinstance(y, type(5))     ⇽---  ❹
True

issubclass函数只能用于类:

>>> issubclass(C, D) 
False
>>> issubclass(E, D)
True
>>> issubclass(D, D)     ⇽---  ❺
True
>>> issubclass(e.__class__, D)
True

对于类实例而言,isinstance是针对类进行检测的❶。e确实是类D的实例,因为E继承自D❷。但是d不是类E的实例❸。对于其他类型,可以采用一个具体的示例值来进行检测❹。类将被视为自身的子类❺。

5. 鸭子类型

利用type、isinstance和issubclass函数,代码很容易就能正确确定对象或类的继承层次结构。

虽然过程很简单,但Python还有一个特性,可以让对象用起来更加轻松:鸭子类型(duck typing)。正如“如果某个东西走起来像鸭子,叫起来也像鸭子,那它可能就是一只鸭子”,鸭子类型是指Python确定对象类型的方式,看对象是不是完成某项操作所需的类型,关注的重点是对象与外界的交互能力,而不是对象的类型。

例如,某项操作需要用到的是迭代器,则所用的对象不一定得是某种迭代器的子类,甚至可以根本就不是迭代器类。真正重要的是,当作迭代器使用的对象能够按预期方式生成一系列的对象。

相比之下,在像Java这样的语言中,需要强制执行更加严格的继承规则。简而言之,鸭子类型意味着,在Python中不需要(或许也不应该)操心对函数或方法参数进行类型检查等。相反,应该依赖高可读性、文档完善的代码,再加上彻底的测试,以确保对象能够按需“像鸭子一样嘎嘎叫”。

鸭子类型可以增加优质代码的灵活性,再加上比较高级的面向对象特性,Python就能够创建几乎涵盖任何场景的类和对象了。

6. 何为特殊方法属性

特殊方法属性(special method attribute)是Python类的一种属性,对Python而言具备特殊的含义。虽然被定义为方法,但其实并不是打算直接当作方法使用的。通常特殊方法不会被直接调用,而是由Python自动调用,以便对属于该类的对象的某种请求做出响应。

特殊方法属性最简单的例子,也许就是__str__了。如果是在类中定义的,只要Python请求该类的实例的可读字符串形式,就会调用__str__方法属性,并将其返回值用作请求的字符串。为了实际查看一下该属性,不妨把表示红绿蓝(RGB)颜色的类定义为数值三联组,每个数值分别代表红、绿、蓝色的强度。除定义标准的__init__方法用于初始化类实例之外,再定义一个__str__方法,用于返回字符串形式的实例,也就是适于人类阅读的格式。

类的定义应该如下代码所示。

color_module.py文件:

class Color:
    def __init__(self, red, green, blue):
        self._red = red
        self._green = green
        self._blue = blue
    def __str__(self):
        return "Color: R={0:d}, G={1:d}, B={2:d}".format (self._red,  
                                             self._green, self._blue)

假如把上述类定义存入了color_module.py文件,就可以按常规方式导入并使用了:

>>> from color_module import Color
>>> c = Color(15, 35, 3)

如果用print把c打印出来,就可以看到特殊方法属性__str__的效果了:

>>> print(c)
Color: R=15, G=35, B=3

 即便没有任何代码对特殊方法属性__str__发起显式的调用,Python还是会用到它的。Python知道__str__属性(假如存在的话)定义了将对象转换为用户可读字符串的方法。这正是特殊方法属性定义的特色,能以专用方式定义挂入Python的钩子(hook)函数。

此外,特殊方法属性还可用于定义一种特殊的类,其对象的行为在语法和语义上都与列表或字典相同。例如,可用来定义用法与Python列表完全相同的对象,但采用平衡树而不是数组来存储数据。对程序员而言,这种对象用起来就和列表一样,但是性能上存在差异,如数据插入速度更快,迭代起来则较慢。这种差异或许正好有助于解决现实中的问题。

7. 让对象像列表一样工作

以下示例用到了大型的文本文件,里面存放着人员记录信息。每行就是一条记录,其中包含了人名、年龄和居住地,字段之间用双冒号“::”分隔。

文件数据行可能如下所示:

.
.
.
John Smith::37::Springfield, Massachusetts
Ellen Nelle::25::Springfield, Connecticut
Dale McGladdery::29::Springfield, Hawaii 
.
.
.

假定现在需要收集文件中的人员年龄分布信息。处理文件数据行的方式有很多种,以下就是其中一种:

fileobject = open(filename, 'r')
lines = fileobject.readlines()
fileobject.close()
for line in lines:
    ...执行某些操作...

以上技术理论上是可行的,但会把整个文件一次性读入内存。如果文件太大,以至内存中容纳不下(这种文件很有可能会比较大),那么程序就无法使用了。

下面是另一种解决方案:

fileobject = open(filename, 'r')
for line in fileobject:
    ...执行某些操作...
fileobject.close()

以上代码每次只会读入一行,以此来解决内存不足的问题。这样运行没有问题,但如果想让文件打开更简单一些,并且只需获取每行的前两个字段(姓名和年龄),那就需要用到能把文本文件视为数据行列表,至少能用上for循环,但它又不能把整个文本文件一次性读进来。

8. 特殊方法属性__getitem__

采用特殊方法属性__getitem__,就是一种解决方案。在所有用户自定义类中都能定义该方法,能够让该类的实例对列表访问语法和语义做出响应。假设AClass是定义了__getitem__的Python类,obj是该类的实例,那么x = obj[n]和for x in obj:就是有意义的,obj就可以像列表那样使用。

以下代码就是答案,后面带了注释:

class LineReader:
    def __init__(self, filename):
        self.fileobject = open(filename, 'r')     ⇽---  以只读方式打开文件
    def __getitem__(self, index):
        line = self.fileobject.readline()     ⇽---  读取一行
        if line == "":       ⇽---  如果读不到数据了
            self.fileobject.close()     ⇽---  关闭文件对象
            raise IndexError      ⇽---  引发IndexError

        else:
            return line.split("::")[:2]     ⇽---  否则拆分当前行,返回前两个字段

for name, age in LineReader("filename"):
    #...执行某些操作...

乍一看,上述例子貌似比之前的解决方案更加糟糕,因为代码更多、很难理解。但是大部分代码都是在一个类中,可以放入自定义的模块中,例如,叫作myutils模块。然后程序就变成了:

import myutils
for name, age in myutils.LineReader("filename"):
    #...执行某些操作...

LineReader类负责处理所有细节,包括打开文件、每次读取一行、关闭文件。在开始的开发阶段花了较多的工夫,以此为代价得到了一个读取工具,可以更轻松地处理每行一条记录的大型文本文件,而且不易出错。

注意,Python已经提供了很多强大的文件读取方案,但是本例的优点是很容易理解。理解了这种思路后,可以把同样的原理应用到很多场景中去。

1)工作原理

LineReader是个类,__init__方法会以只读方式打开指定名称的文件,并把打开的文件对象用fileobject保存起来供后续访问。要理解__getitem__方法的使用,需要清楚以下3点。

  • 所有定义了__getitem__实例方法的对象,都可以像列表那样返回多个元素。所有object[i]形式的访问,都会由Python转换为object.__getitem__(i)形式的方法调用,将作为普通的方法调用进行处理。最终会以__getitem__(object, i) 执行,采用的是类里定义的__getitem__方法。每次调用__getitem__时,第一个参数是要从中提取数据的对象,第二个参数是该项数据的索引。
  • 因为for循环将访问列表中的每一项数据,每次访问一个数据项,所以for arg in sequence:这种形式的循环,就是通过反复调用__getitem__来完成的,每次都会递增索引值。for循环首先会把arg置为sequence.__getitem__(0),然后是sequence.__getitem__(1),以此类推。
  • for循环将捕获IndexError,处理方案就是退出循环。用for访问普通的列表或序列时,也是这样中止循环的。

LineReader类只能在for循环内使用,for循环始终会以匀速递增的索引值为参数发起调用,__getitem__(self, 0)、__getitem__(self, 1)、__getitem__(self, 2),以此类推。上面的代码充分利用了这一点,依次返回每一行,只是index参数被忽略了。

有了这些知识,就很容易理解LineReader对象是如何在for循环中模拟序列了。循环的每次迭代,都会导致在对象上调用Python特殊方法属性__getitem__。结果就是对象从其保存的fileobject中读入下一行,并对该行进行检测。如果该行非空,则返回该行数据。空行则表示已到达文件末尾,对象将关闭fileobject并引发IndexError异常。for循环体将会捕获IndexError异常,然后循环终止。

通常,用for line in fileobject:类型的循环就足以遍历文件中的数据行了。但本例确实表明,在Python中很容易就能创建一个像列表或其他类型一样工作的类。 

2)实现完整的列表功能

在以上示例中,LineReader类的对象只在一个地方表现得像是列表对象,也就是能正确响应对所读文件数据行的顺序访问请求。或许大家还想知道,该如何扩展功能,让LineReader或其他对象的行为更像列表。

首先,__getitem__方法应该能以某种方式处理其索引参数。因为LineReader类的重点完全放在了避免将大文件读入内存上,所以将整个文件放入内存并返回相应数据行是没有意义的。最佳方案可能会是每次__getitem__调用时检查索引是否大于前一次调用时的值,如果不是则引发错误。如果是对某LineReader实例第一次调用__getitem__,则索引值为0。这种做法将确保LineReader实例仅能按照预期在for循环中使用。

更加一般地来说,Python提供了几个与列表行为相关的特殊方法属性。当对象用在列表赋值的语法上下文时,如obj[n] = val,可用__setitem__定义要完成的操作。其他还有几个特殊方法属性,提供了不太显眼的列表功能,如__add__属性,让对象能够响应“+”操作符,以便执行自定义版本的列表拼接操作。

在能够完全模拟列表之前,类还需要定义其他几个特殊方法。不过通过定义适当的Python特殊方法属性,就可以实现完整的列表模拟功能。

9. 完整实现列表功能的对象

__getitem__是Python众多的特殊方法属性之一。这些特殊方法可以定义在类中,让该类的实例有能力展示特定的行为。为了了解特殊方法属性的更多应用,以便能有效地将新功能无缝集成到Python当中,下面看一个较为全面的示例。

在使用列表时,通常一个列表只会包含一种类型的元素,如字符串列表或数值列表。某些编程语言能够强制执行这一限制,如C++。在大型程序中,能够将列表声明为仅允许包含特定类型的元素,这将有助于错误的跟踪。试图把类型错误的元素添加到指定了类型的列表中,将会引发错误信息。这样在程序开发的早期阶段就可能识别出问题,而不用等到其他时候了。

Python没有内置指定了类型的列表,大多数Python程序员也不会惦记这种列表。但如果考虑要强行保证列表的类型一致性,那么采用特殊方法属性就能很容易创建出一个行为类似于指定类型列表的类。以下给出了这种类的开头部分,它大量用到了Python内置的type和isinstance函数来检查对象的类型:

class TypedList:
    def __init__(self, example_element, initial_list=[]):
        self.type = type(example_element)     ⇽---  ❶
        if not isinstance(initial_list, list):
            raise TypeError("Second argument of TypedList must " 
                          "be a list.")
        for element in initial_list:
                if not isinstance(element, self.type):
                    raise TypeError("Attempted to add an element of " 
                                  "incorrect type to a typed list.")
        self.elements = initial_list[:]

参数example_element定义了该列表能够容纳的类型,只要给出一个元素类型的示例即可❶。

这里定义的TypedList类,能够用以下形式进行调用:

x = TypedList ('Hello', ["List", "of", "strings"])

第一个参数是'Hello',它根本就不会加入结果数据中,只是用作一个示例,表示列表必须包含的元素的数据类型,本例中是字符串。第二个参数是一个可选的列表,可用于提供列表的初值。

TypedList类的__init__函数将会对创建TypedList实例时传入的所有列表元素进 行检查,看看是否与第一个参数给出的示例属于相同的类型。只要有不匹配的类型,就会引发异常。

这一版本的TypedList类还不能当作列表来使用,因为它没有对设置或访问列表元素的标准方法进行响应。要解决这个问题,需要定义特殊方法属性__setitem__和__getitem__。只要执行TypedListInstance[i] = value这种语句,Python就会自动调用__setitem__方法。只要计算表达式TypedListInstance[i]并返回TypedListInstance的第i个槽位的数据,就会调用__getitem__方法。

下面是下一版本的TypedList类。因为要对很多新元素进行类型检查,所以新抽象出了私有方法__check:

class TypedList:
    def __init__(self, example_element, initial_list=[]):
        self.type = type(example_element)
        if not isinstance(initial_list, list):
            raise TypeError("Second argument of TypedList must " 
                            "be a list.")
        for element in initial_list: 
            self.__check(element)
        self.elements = initial_list[:]
    def __check(self, element):
        if type(element) != self.type:
            raise TypeError("Attempted to add an element of " 
                            "incorrect type to a typed list.")
    def __setitem__(self, i, element):
        self.__check(element)
        self.elements[i] = element
    def __getitem__(self, i):
        return self.elements[i]

现在类TypedList的实例与列表更相像了。例如,以下代码就能正常执行了:

>>> x = TypedList("", 5 * [""])
>>> x[2] = "Hello"
>>> x[3] = "There"
>>> print(x[2] + ' ' + x[3])
Hello There
>>> a, b, c, d, e = x
>>> a, b, c, d
('', '', 'Hello', 'There')

print语句中对元素x的访问,将由__getitem__进行处理,访问请求将会传给保存在TypedList对象中的列表实例。对x[2]和x[3]的赋值,将由__setitem__进行处理,首先会检查赋给列表的元素是否具备相应的类型,然后在包含在self.elements中的列表上执行赋值。最后一行用__getitem__对x的前5个数据项进行拆包,然后分别打包到变量a、b、c、d、e中。对__getitem__和__setitem__的调用是由Python自动完成的。

要全部完成TypedList类,使TypedList对象在所有方面都表现得像列表对象一样,还需要更多的代码。首先应该定义特殊方法属性__setitem__和__getitem__,以便TypedList实例可以处理切片语法和单个数据项访问。

其次应该定义__add__,以便可以执行列表添加(拼接)操作。第三应该定义__mul__,以便可以执行列表乘法。第四应该定义__len__,以便len(TypedListInstance)调用能够正确计算出结果。第五应该定义__delitem__,以便TypedList类可以正确处理del语句。第六还应定义append方法,以便可以通过标准的列表风格的append、insert、extend方法将数据项加入TypedList实例。

10. 由内置类型派生子类

以上示例是个不错的练习,有助于理解如何从头开始实现一个类似列表的类,不过工作量也是很大的。从实践角度来看,如果要按照下面演示的代码实现自己的类似列表的数据结构,或许可以换一种思路,不妨考虑一下由列表类型或UserList类型派生子类。

1)由列表类型派生子类

不用像以上示例那样从头开始为特定类型的列表创建类,而是可以从列表类型派生子类,只要把需要关心数据类型的所有方法覆盖即可。这种方案有一大优点,就是自定义类拥有全部列表操作的默认版本方法,因为类已经是列表了。

主要是得时刻牢记,Python中的所有类型都是类,如果要对内置类型的行为做出修改,可能要考虑由该类型派生子类:

class TypedListList(list):
    def __init__(self, example_element, initial_list=[]):
        self.type = type(example_element)
        if not isinstance(initial_list, list):
            raise TypeError("Second argument of TypedList must " 
                            "be a list.")
        for element in initial_list: 
            self.__check(element)
        super().__init__(initial_list)

    def __check(self, element):
        if type(element) != self.type:
            raise TypeError("Attempted to add an element of " 
                            "incorrect type to a typed list.")

    def __setitem__(self, i, element):
        self.__check(element)
        super().__setitem__(i, element)

>>> x = TypedListList("", 5 * [""])
>>> x[2] = "Hello"
>>> x[3] = "There"
>>> print(x[2] + ' ' + x[3])
Hello There
>>> a, b, c, d, e = x
>>> a, b, c, d
('', '', 'Hello', 'There')
>>> x[:]
['', '', 'Hello', 'There', '']
>>> del x[2]
>>> x[:]
['', '', 'There', '']
>>> x.sort()
>>> x[:]
['', '', '', 'There']

注意,这时要做的就只是实现一个方法,用于检查要加入的数据项的类型。在调用list常规的__setitem__方法之前,对__setitem__稍作修改以执行检查。其他诸如sort和del之类的方法,无须再做编码即可工作。如果只需要对类的行为做一点点变动,重载内置类型可以节省相当多的时间,因为类的大部分内容都可以不加修改直接使用。

2)由UserList派生子类

如果需要一种列表的变体(如以上示例所示),则还有第三种选择,可以由UserList类派生子类,这是一个位于collections模块的列表包装类。UserList是为早期版本的Python创建的,当时无法由列表类型派生子类。但UserList仍然很有用,特别是对当前情况而言,因为底层的列表可由data属性访问到:

from collections import UserList
class TypedUserList(UserList):
    def __init__(self, example_element, initial_list=[]):
        self.type = type(example_element)
        if not isinstance(initial_list, list):
            raise TypeError("Second argument of TypedList must " 
                            "be a list.")
        for element in initial_list: 
            self.__check(element)
        super().__init__(initial_list)

    def __check(self, element):
        if type(element) != self.type:
            raise TypeError("Attempted to add an element of " 
                            "incorrect type to a typed list.")
    def __setitem__(self, i, element):
        self.__check(element)
        self.data[i] = element
    def __getitem__(self, i):
        return self.data[i]

>>> x = TypedUserList("", 5 * [""])
>>> x[2] = "Hello"
>>> x[3] = "There"
>>> print(x[2] + ' ' + x[3])
Hello There
>>> a, b, c, d, e = x
>>> a, b, c, d
('', '', 'Hello', 'There')
>>> x[:]
['', '', 'Hello', 'There', '']
>>> del x[2]
>>> x[:]
['', '', 'There', '']
>>> x.sort()
>>> x[:]
['', '', '', 'There']

这个例子与列表类型派生子类非常相似,只不过是在实现类的内部,数据列表可通过data属性内部获得。某些场合中,能够直接访问底层数据结构是很有用的。除UserList之外,还有UserDict和UserString两个包装类可供选用。

11. 特殊方法属性的适用场景

通常,使用特殊方法属性最好是谨慎一点。用到这些代码的其他程序员会很好奇,为什么某个序列类型的对象能够对标准索引的语法做出正确响应,而另一个对象却不行。

以下两种情况下可以使用特殊方法属性。

  • 如果自己的代码中有一个频繁用到的类,在某些方面表现得像是某个Python内置类型,那就可以定义对应的特殊方法属性。当对象的行为与序列多少有点类似时,就是这种情况发生最多的时候。
  • 如果自定义类的行为与内置类的行为相同或几乎相同,则既可以选择对所有的特殊方法属性都进行定义,也可以从Python内置类型派生子类并发布类。用平衡树实现的列表可以算是后一种方案的示例,虽然访问速度较慢,但插入速度却比标准的列表类要快。

以上规则并不是一成不变的。例如,为类定义特殊方法属性__str__往往就很不错,这样就可以在调试代码中放入print(instance),以便能在屏幕上看到可读性很好的对象信息。

总结:

  • 定义一个类实际上是创建了一种新的数据类型。
  • __init__用于在创建类的新实例时初始化数据,但不是构造函数。
  • self参数指向了类的当前实例,将作为第一个参数传入类方法。
  • 不需要创建类的实例,就能直接调用静态方法,因此静态方法没有self参数。
  • 类方法通过cls参数传递,cls参数是类的引用,而不是self。
  • 所有的Python方法都是虚方法。也就是说,如果方法未被子类覆盖,也不是父类私有的,那么该方法就可以被所有子类访问。
  • 只要不是用双下划线“__”开头,类变量就会从父类继承而来。双下划线开头的变量为私有变量,无法被子类访问,也可以用这种方式让方法成为私有的。
  • 通过定义getter和setter方法,就可以拥有属性,其行为和普通的实例属性类似。
  • Python允许多重继承,常用于混合类。
  • 必要时可以用代码进行类型检查,Python为此提供了工具函数。但利用鸭子类型可以编写出更加灵活的代码,不需要去操心类型检查问题。
  • 特殊方法属性和由内置类派生子类,均可用于在用户自建类中添加类似列表的行为。
  • 正因为Python有了鸭子类型、特殊方法属性和派生子类,使得以各种方式构造和组合多个类成为可能。

七、模块与包

1、模块

1. 模块简介

模块是一个包含代码的文件,其中定义了一组Python函数或其他对象,并且模块的名称来自文件名。

模块通常包含Python源代码,但也可以是经过编译的C或C++对象文件。经过编译的模块和Python源代码模块的用法是一样的。

模块不仅可以将相互关联的Python对象归并成组,还有助于避免命名冲突(name-clash)问题。例如,可能为自己的程序编写了一个名为mymodule的模块,其中定义了一个名为reverse的函数。在同一个程序中,可能还要用到别人的模块othermodule,其中也定义了一个名为reverse的函数,但是执行的操作与自己的reverse函数不同。在没有模块的编程语言中,不可能使用两个都叫reverse的不同函数。在Python中,这很容易处理,在主程序中用mymodule.reverse和othermodule.reverse就可引用这两个函数了。

因为Python采用了命名空间(namespace)的机制,所以使用模块名可以同时保留两个reverse函数。

命名空间本质上就是标识符的字典,可用于代码块、函数、类、模块等,每个模块都有自己的命名空间,这有助于防止命名冲突。

使用模块还能让Python本身更易于管理。大多数标准的Python函数并没有内置于语言内核中,而是通过特定的模块提供的,可以按需加载。

2. 编写第一个模块

学习模块的最好方式,可能就是自己动手写一个。

新建一个名为mymath.py的文本文件,在其中输入如下图中的Python代码。

mymath.py文件:

"""mymath - our example math module"""
pi = 3.14159
def area(r): 
    """area(r): return the area of a circle with radius r."""
    global pi
    return(pi * r * r)

现在将以上代码保存在Python可执行文件所在的目录中。这段代码只是给pi赋了值,并定义了一个函数。强烈建议对所有Python代码文件都带上.py文件名后缀,以便通知Python解释器这是一个Python代码文件。与函数一样,可以选择在模块的第一行放入文档字符串。

现在启动Python shell并键入以下代码: 

>>> pi
Traceback (innermost last):
  File "<stdin>", line 1, in ?
NameError: name 'pi' is not defined
>>> area(2)
Traceback (innermost last):
  File "<stdin>", line 1, in ?
NameError: name 'area' is not defined

也就是说,Python并没有内置常量pi和函数area。然后键入:

>>> import mymath
>>> pi
Traceback (innermost last):
  File "<stdin>", line 1, in ?
NameError: name 'pi' is not defined
>>> mymath.pi
3.14159
>>> mymath.area(2)
12.56636
>>> mymath.__doc__
'mymath - our example math module'
>>> mymath.area.__doc__
'area(r): return the area of a circle with radius r.'

利用import语句,从mymath.py文件中引入了pi和area的定义。在检索mymath模块的定义文件时,import操作会自动在模块名后面加上.py后缀。但新引入的对象定义不能直接访问,只输入pi会报错,只输入area(2)也会报错。

请把pi和area预先放在包含它们的模块名后面,才能访问它们,以此来保证对象名称的安全使用。也许别的外部模块也定义了pi,也许该模块的作者将pi视为3.14或3.14159265,但这都没有关系。即便同时导入了那个模块,它的pi也是要经由othermodulename.pi访问的,与mymath.pi并不相同。这种访问形式常被称为限定名称(qualification),即变量pi是受模块mymath限定的。也可以将pi称为mymath的属性。

模块中的对象可以直接访问同一模块内定义的其他对象,不需要带上模块名。函数mymath.area访问常量mymath.pi时,只需要用pi即可。

还可以要求从模块中导入指定的对象名称,这样在使用时就不需要带上模块名了。例如:

>>> from mymath import pi
>>> pi
3.14159
>>> area(2)
Traceback (innermost last):
  File "<stdin>", line 1, in ?
NameError: name 'area' is not defined

因为用from mymath import pi语句做了特定的要求,所以上面的pi已经可以直接访问了。而函数area仍需要用mymath.area进行调用,因为没有进行显式的导入。

也许在新建模块时,大家会用基本交互模式或IDLE的Python shell对该模块进行渐进式的测试。但如果修改了磁盘中的模块文件,那么再次输入import命令并不会重新加载模块,这时要用到模块importlib的reload函数才可以。importlib模块为访问模块导入的后台机制提供了一个接口:

>>> import mymath, importlib
>>> importlib.reload(mymath)
<module 'mymath' from '/home/doc/quickpythonbook/code/mymath.py'>

当模块被重新加载(或第一次导入)时,其所有的代码都会被解析一遍。如果发现错误,则会引发语法异常。反之,如果一切正常就会创建包含Python字节码的.pyc文件,如mymath.pyc。

重新加载模块后的状态,与新开会话并首次导入时的状态并不完全一样。但两者的差别通常不会引发什么问题。

当然,模块不仅是在交互式Python shell中需要用到。模块还可以被导入脚本文件,或者其他模块中,只要在代码文件的开头输入合适的import语句即可。从本质上来说,对于Python,交互式会话和脚本也被认为是模块。

总结如下:

  • 模块是定义Python对象的文件。
  • 假定模块文件的名称是modulename.py,那么模块的Python名称就是modulename。
  • 通过import modulename语句,即可导入名为modulename的模块。该导入语句执行完毕后,模块modulename中定义的对象就能以modulename.objectname的格式被访问了。
  • 通过from modulename import objectname语句,可以将模块中的指定对象名称直接导入代码。该语句使得代码无须带modulename前缀即可直接访问objectname,这在导入某个频繁使用的对象名称时会很有用。

3. import语句

import语句有3种格式。

最基本的格式就是:

import modulename

 这时会搜索给定名称的Python模块,解析模块内容并使之进入可用状态。发起导入的代码可以使用模块中的内容,但是引用模块中的对象名称时仍必须带有模块名前缀。如果未找到指定名称的模块,就会报错。

第二种格式允许指定模块中的名称,将其显式导入代码中:

from modulename import name1, name2, name3, ...

这些name1、name2等modulename中的对象名称,就可供发起导入的代码使用了。在import语句之后的代码,可以直接使用name1、name2、name3等名称,不需要再带上模块名前缀了。

最后一种是通用的from ... import ...语句格式:

from modulename import *

“*”代表模块modulename中所有导出(exported)的对象名称。from modulename import *将会导入modulename模块中所有公有对象名称,也就是未以下划线开头的名称。这些对象名称可供发起导入的代码直接使用,无须带上模块名前缀。但如果在模块(或者包的__init__.py文件)中存在名为__all__的名称列表,那么该列表中的名称都会被导入,无论它们是否以下划线开头。

在使用这种特殊格式的导入时应该十分地小心。如果两个模块都定义了同一个对象名,并且用这种导入格式导入了这两个模块,则最终会发生命名冲突,并且第二个模块中的名称将会替换第一个模块中的名称。这种技术还会让阅读代码的人更难确定对象名称的来源。如果使用的是前两种格式的导入语句,就可以提供名称来源的明确信息。

不过某些模块(如tkinter)命名的函数,可以明显看出名称的来源,以至于不太可能发生命名冲突。在使用交互式shell时,利用通用导入格式来减少击键数量也是常用的技巧。

4. 模块搜索路径

Python搜索模块的确切路径是在一个名为path的变量中定义的,可以通过模块sys访问path变量。请键入以下代码:

>>> import sys
>>> sys.path
_list of directories in the search path_

实际在_list of directories in the search path_位置显示的值,取决于当前的系统配置。不管具体内容是什么,该字符串给出的是一个目录列表。当准备执行import语句时,Python将按顺序遍历该目录列表,并采用第一个满足import需求的模块。如果搜索路径中找不到合适的模块,则会引发ImportError异常。

如果使用的是IDLE,可以用Path Browser窗口以图形方式查看搜索路径和其中的模块,从Python shell窗口的File菜单中可启动Path Browser窗口。

sys.path变量的初始值,来自操作系统环境变量PYTHONPATH(如果存在)的值,或者来自安装时的默认值。此外,无论何时运行Python脚本,都会把脚本文件所在目录插入其sys.path变量中,作为第一个元素。这为确定当前执行的Python程序所在的路径提供了一种便捷方法。例如,在上述交互式会话中,sys.path的第一个元素被设为空字符串,Python会将其视为首先应在当前目录中搜索模块。

5. 自建模块的存放位置

模块mymath之所以能被Python访问,原因可能有两个。一是因为以交互模式执行Python时,sys.path的第一个元素为"",这会告知Python要在当前目录中查找模块。二是因为在mymath.py文件所在目录中执行Python。在生产环境中,这两种条件通常都不具备。既不会以交互模式运行Python,Python代码文件也不会位于当前目录中。

为确保程序可以使用自己编写的模块,需要做到:

  • 将自己的模块放入Python的常规模块搜索路径中去;
  • 将Python程序要用到的全部模块,都和程序放在同一目录中;
  • 新建目录用于保存自己的模块,并修改sys.path变量,使之包含该新建目录;

在这3种方式中,第一种显然最简单,但也是永远不应采用的方式,除非Python默认的模块搜索路径中包含了本地代码所在的目录。这些目录专门用于存放当前环境特有(site-specific)的代码(适用于当前机器的专用代码),因为不属于Python安装目录,所以以后安装Python时也不会被覆盖。如果sys.path中引用了这类目录,可以把自己的模块放进去。

如果模块与特定的程序关联,那么第二种方式是种很好的选择。只要把模块和程序放在一起就可以了。

如果模块专用于某环境,将被同一部署环境下的多个程序调用,那么第三种方式就是正确的选择。修改sys.path的方式有很多。可以在代码中赋值,这很简单,但也会把目录位置写死(hardcode)在程序代码中。还可以设置环境变量PYTHONPATH,相对来说还算简单,但可能无法适用于当前环境的所有用户。或者还可以利用.pth文件将目录追加到默认搜索路径中。

在Python文档中,有设置环境变量PYTHONPATH的例子,位于“Python Setup and Usage”(Python安装和使用)部分的“Command line and environment”(命令行和环境变量)。

在环境变量中设置的目录,将会插入到sys.path变量的最前面。如果用了PYTHONPATH,注意不要把模块名定义为已有库模块的名称。如果模块同名,则自己的模块就会先于库模块被搜索到。某些情况下也许这正是需要的结果,但可能并不常见。

利用.pth文件可以避免上述问题,因为这时目录会添加到sys.path变量的末尾。最后这种机制最好还是通过一个例子来说明。在Windows中,可以将.pth文件放在sys.prefix指向的目录中。

假定sys.prefix为“c:\ program files\python”,将下面代码文件放入该目录中。

myModules.pth文件:

mymodules
c:\Users\naomi\My Documents\python\modules

下一次Python解释器启动时,sys.path中将会加入c:\program files\python\mymodules和c:\Users\naomi\My Documents\python\modules(前提是目录存在)。现在就可以把模块放入这两个目录中了。

注意,mymodules目录仍然存在被以后的安装覆盖的危险。modules目录就比较安全了。以后在升级Python时,可能还得先将mymodules.pth文件移走保存起来或者重新创建一个。

6. 模块内部私有名称

可以用from module import *从模块中导入几乎所有的对象名称。这里的例外是,模块中下划线开头的标识符不能用from module import *导入。大家编写的模块,可以是准备供from module import *导入用的,但也可以保留某些函数或变量不被导入。将所有内部对象名称(即不允许模块外部访问的名称)都以下划线开头,就可以确保from module import *只引入用户需要访问的名称。

下面来看看这种技术的实际应用。

modtest.py文件:

"""modtest: our test module"""
def f(x):
    return x
def _g(x):
    return x
a = 4
_b = 2

下面启动一个交互式会话并输入以下命令:

>>> from modtest import *
>>> f(3)
3
>>> _g(3)
Traceback (innermost last):
  File "<stdin>", line 1, in ?
NameError: name '_g' is not defined
>>> a
4
>>> _b
Traceback (innermost last):
  File "<stdin>", line 1, in ?
NameError: name '_b' is not defined

如上所示,名称f和a被成功导入,但名称_g和_b在modtest之外是不可见的。注意,只有from import *才会有这种行为。按以下方式操作还是可以访问到_g和_b的:

>>> import modtest
>>> modtest._b
2
>>> from modtest import _g
>>> _g(5)
5

用前导下划线表示私有名称的约定,整个Python中都在使用,而不仅用在模块中。

为了更加便于管理,标准的Python发行版被拆分为多个模块。Python安装完成后,这些库模块的所有功能都是可用的。只要在使用之前,显式导入合适的模块、函数、类等即可。

2、Python作用域

随着Python编程经验的增长,Python的作用域规则和命名空间将变得有意思起来。如果是Python的新手,可能什么都不用做,只需要快速浏览本节有个基本概念就行了。

这里的核心概念是命名空间。Python中的命名空间是从标识符到对象的映射,也就是Python如何跟踪变量和标识符是否活动以及指向什么。因此,像x = 1这样的语句,会把x添加到命名空间(假定尚不存在)并将其与值1关联。

当在Python中执行一个代码块时,它拥有3个命名空间:局部(local)、全局(global)和内置(built-in),如下图所示。

在运行期间遇到标识符时,Python首先会在局部命名空间中查找。如果没有找到,则接下来查看全局命名空间。如果仍未找到,则检查内置命名空间。如果标识符还不存在,将会被认为是错误,并引发NameError。

对模块而言,在交互式会话中执行的命令或运行来自文件的脚本,全局命名空间和局部命名空间是相同的。创建变量和函数,或者从其他模块导入对象,都会导致在该命名空间中创建新条目或与已有条目进行绑定(binding)。

但在进行函数调用时,会创建一个局部命名空间,并在其中为函数调用的每个参数都加入一个绑定关系。以后无论何时在函数中创建一个变量,都会在该局部命名空间中加入一个新的绑定关系。函数的全局命名空间,是包含函数(可能是模块、脚本文件或交互式会话)的代码块的全局命名空间,与被调用处的动态上下文是相互独立的。

在所有上述场合,内置命名空间都是模块__builtins__的命名空间。除其他内容外,该模块中包含了所有已介绍过的内置函数(如len、min、max、int、float、list、tuple、range、str和repr)以及其他如异常之类(如NameError)的Python内置类。

有时候Python编程新手会在一件事情上栽跟头,那就是内置模块中的定义可能会被覆盖掉。例如,假设在程序中创建了一个列表,并将其放入一个名为list的变量,那么此后内置的list函数就不能用了。首先被找到的对象名称,是自定义的list变量。函数和模块的名称与其他对象没有区别。对给定标识符的最近一次绑定操作,将会被编译器采用。

内容介绍得差不多了,该看一些例子了。下面的示例用到了两个内置函数:locals和globals。这两个函数返回的是字典,分别包含了局部和全局命名空间中的绑定关系。

请新开一个交互式会话: 

>>> locals()
{'__builtins__': <module 'builtins' (built-in)>, '__name__': '__main__', 
   '__doc__': None, '__package__': None}
>>> globals()
{'__builtins__': <module 'builtins' (built-in)>, '__name__': '__main__', 
   '__doc__': None, '__package__': None}>>>

在这个新开的交互式会话中,局部和全局命名空间是相同的。它们有3个初始的供内部使用的键/值对:(1)一个空的文档字符串__doc__;(2)主模块名__name__(对于由文件运行的交互式会话和脚本,则始终为__main__);(3)用于内置命名空间__builtins__的模块(模块__builtins__)。

接下来,如果继续创建变量并从模块导入,将会创建多个绑定关系:

>>> z = 2
>>> import math
>>> from cmath import cos
>>> globals()
{'cos': <built-in function cos>, '__builtins__': <module 'builtins' 
    (built-in)>, '__package__': None, '__name__': '__main__', 'z': 2,
    '__doc__': None, 'math': <module 'math' from 
    '/usr/local/lib/python3.0/libdynload/math.so'>}
>>> locals()
{'cos': <built-in function cos>, '__builtins__':
    <module 'builtins' (built-in)>, '__package__': None, '__name__': 
    '__main__', 'z': 2, '__doc__': None, 'math': <module 'math' from
    '/usr/local/lib/python3.0/libdynload/math.so'>}
>>> math.ceil(3.4)
4

正如预期的那样,局部和全局命名空间仍然是相同的。z被添加为数字,math添加为模块,cmath模块中的cos添加为函数。

用del语句可以从命名空间中移除这些新创建的绑定关系,包括用import语句创建的模块绑定关系:

>>> del z, math, cos
>>> locals()
{'__builtins__': <module 'builtins' (built-in)>, '__package__': None, 
    '__name__': '__main__', '__doc__': None}
>>> math.ceil(3.4)
Traceback (innermost last):
  File "<stdin>", line 1, in <module>
NameError: math is not defined
>>> import math
>>> math.ceil(3.4)
4

以上的执行结果还不算极端,因为能够导入math模块并再次使用。在处于交互模式时,del的这种用法可能就很方便了。

用了del然后再次导入,并不能反映出磁盘上的模块改动情况。这样做并不会从内存中移除并从磁盘重新加载模块。而只是将绑定关系移除,然后再放回当前的命名空间而已。如果要将文件的变化反映出来,仍需要用到importlib.reload。

对好事者而言,确实能够用del来移除__doc_、__main__和__builtins__这些条目。但请勿这样做,因为这对会话的正常运行无益!

下面来看看在交互式会话中创建的函数:

>>> def f(x):
...     print("global: ", globals())
...     print("Entry local: ", locals())
...     y = x
...     print("Exit local: ", locals())
...    
>>> z = 2
>>> globals()
{'f': <function f at 0xb7cbfeac>, '__builtins__': <module 'builtins' 
    (built-in)>, '__package__': None, '__name__': '__main__', 'z': 2, 
    '__doc__': None}
>>> f(z)
global:  {'f': <function f at 0xb7cbfeac>, '__builtins__': <module 
    'builtins' (built-in)>, '__package__': None, '__name__': '__main__',
    'z': 2, '__doc__': None}
Entry local:  {'x': 2}
Exit local:  {'y': 2, 'x': 2}
>>>

以上代码稍显混乱,仔细分析一下就能发现,正如预期的那样,一开始参数x是f函数的局部命名空间中的初始条目,而y则是后来加入的。全局命名空间与交互式会话的相同,也就是定义f的地方。注意,全局命名空间中包含了z,是在f之后定义的。

在生产环境下,通常会调用定义于模块中的函数。这些函数的全局命名空间,就是定义函数的模块的全局命名空间。

scopetest.py文件:

"""scopetest: our scope test module"""
v = 6
def f(x):
    """f: scope test function"""
    print("global: ", list(globals().keys()))
    print("entry local:", locals())
    y = x
    w = v
    print("exit local:", locals().keys())

注意,这里只会打印出globals返回的字典键(标识符),这样显示的结果就不会那么凌乱了。因为模块均已经过优化,会将整个__builtins__字典对象存储为__builtins__键的值,所以只要打印出字典键即可:

>>> import scopetest
>>> z = 2
>>> scopetest.f(z)
global:  ['__name__', '__doc__', '__package__', '__loader__', '__spec__', 
     '__file__', '__cached__', '__builtins__', 'v', 'f']
entry local: {'x': 2}
exit local: dict_keys(['x', 'w', 'y'])

现在的全局命名空间是scopetest模块的全局命名空间,且包含了函数f和整数v,但不包括来自交互式会话的z。因此在创建模块时,可以完全掌控其内部函数的命名空间。

局部和全局命名空间介绍完了,接下来将介绍内置命名空间。以下示例引入了另一个内置函数dir,它会返回给定模块中定义的对象名称列表:

>>> dir(__builtins__) 
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
     'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 
     'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 
     'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 
     'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 
     'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 
     'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 
     'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 
     'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 
     'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 
     'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 
     'OSError', 'OverflowError', 'PendingDeprecationWarning', 
     'PermissionError', 'ProcessLookupError', 'RecursionError', 
     'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 
     'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 
     'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 
     'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 
     'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 
     'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 
     'ZeroDivisionError', '__build_class__', '__debug__', '__doc__', 
     '__import__', '__loader__', '__name__', '__package__', '__spec__', 
     'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 
     'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright',
     'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 
     'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 
     'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 
     'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 
     'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 
     'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 
     'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 
     'sum', 'super', 'tuple', 'type', 'vars', 'zip']

这里的条目有很多。

最后一组(从abs到zip)是Python的内置函数。

利用help()函数或直接把文档字符串打印出来,还可以轻松获取到所有函数的文档字符串: 

>>> print(max.__doc__)
max(iterable[, key=func]) -> value
max(a, b, c, ...[, key=func]) -> value

With a single iterable argument, return its largest item.
With two or more arguments, return the largest argument.

如前所述,对于Python编程新手来说,无意中把内置函数覆盖掉,并非是前所未有的事:

>>> list("Peyto Lake")
['P', 'e', 'y', 't', 'o', ' ', 'L', 'a', 'k', 'e']
>>> list = [1, 3, 5, 7]
>>> list("Peyto Lake")
Traceback (innermost last):
  File "<stdin>", line 1, in ?
TypeError: 'list' object is not callable

即便使用了内置list函数的语法,Python解释器也不会跳过list的最新绑定关系而理解为内置的list函数。

当然,如果在某个命名空间中,相同的标识符用了两次,也会发生同样的情况。无论数据类型是什么,先前的值都会被覆盖:

>>> import mymath
>>> mymath = mymath.area
>>> mymath.pi
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'pi'

只要意识到这种覆盖情况的存在,这也就不算是一个大问题了。即便对于不同类型的对象,标识符重用也无益于编写可读性最佳的代码。如果在交互模式下无意中犯下了某个错误,恢复起来是很容易的。

可以用del移除绑定关系,重新获得对被覆盖内置对象的访问能力,或者再次导入模块也可重新获得:

>>> del list
>>> list("Peyto Lake")
['P', 'e', 'y', 't', 'o', ' ', 'L', 'a', 'k', 'e']
>>> import mymath
>>> mymath.pi
3.14159

函数locals和globals可用作简单的调试工具。dir函数不会给出当前的设置,但如果调用时不带参数,则会返回经过排序的局部命名空间中的标识符列表。

这种用法有助于捕获变量类型错误,而在那些变量需要预先声明的编程语言中,该错误通常由编译器捕获:

>>> x1 = 6
>>> xl = x1 - 2
>>> x1
6
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
     '__package__', '__spec__', 'x1', 'xl']

IDLE捆绑的调试器带有配置参数,允许在遍历代码时查看局部和全局变量的设置,可将函数locals和globals的输出显示出来。 

3、包

模块可以让小块代码轻松得以重新利用。当项目不断壮大,就会出现多种问题,需要重载的代码在物理或逻辑上都会超出单个文件的合理大小。如果庞大的模块文件不是令人满意的方案,那么一大堆毫无关联的小模块也不见得能好多少。解决这个问题的方案是把有关联的模块组合到同一个包中。

1. 包简介

模块是容纳代码的文件,一个模块定义了一组Python函数和其他对象,通常这些函数和对象都是关联的。模块的名称由文件名称而来。

如果理解了模块,包就容易理解了,因为包就是包含代码和子目录的目录。包里包含了一组通常相互关联的代码文件(模块)。包的名称由主目录名而来。

包是模块概念的自然扩展,旨在应付非常大型的项目。模块把相互关联的函数、类和变量进行了分组,同理,包则是把相互关联的模块进行了分组。

2. 包的第一个示例

为了了解包在实践中的工作原理,下面考虑一种天生就十分庞大的项目设计,类似于Mathematica、Maple、MATLAB的通用数学计算包。例如,Maple就是由数千个文件组成的,代码的组织结构对于保持项目的井然有序至关重要。不妨把整个项目命名为mathproj。

这种项目的组织方式可以有很多种,有一种比较合理的设计是把项目分为两部分。ui由UI部分组成,comp则包含了计算部分。在comp中,进一步把计算部分拆分为symbolic(实数和复数符号计算,如高中代数)和numeric(实数和复数数值计算,如数值积分),这样可能会比较合理。而且比较合理的设计是,symbolic和numeric部分都包含constants.py文件。

numeric部分中的constants.py文件对pi进行了如下定义:

pi = 3.141592

而symbolic部分中的constants.py文件,则把pi定义为:

class PiClass:
     def __str__(self):
         return "PI"
pi = PiClass()

这就意味着同一个名称pi可能会在两个不同的constants.py文件中用到,当然也会被导入,如下图所示。

在symbolic中的constants.py文件里,pi定义为一个抽象的Python对象,它是PiClass类的唯一实例。随着项目的开发,可以在该类中实现各种操作,返回的都是符号而不是数值。

从上述设计结构到目录结构,存在很自然的映射关系。项目的顶级目录名为mathproj,下面包含子目录ui和comp,comp之下又包含了子目录symbolic和numeric。symbolic和numeric中都各自包含了constants.py文件。

有了以上的目录结构,假定根目录mathproj已放入Python搜索路径中,则mathproj包内外的Python代码就可以通过mathproj.symbolic.constants.pi和mathproj.numeric.constants.pi访问两个pi。换句话说,Python包内对象的名称,反映的就是包含该对象的文件所在的目录路径名。

有关包的内容就这么多。包是把大型Python代码集组织成有条理的各个整体的方案,允许代码分布在不同的文件和目录中,施行一种基于包文件目录结构的模块/子模块命名体系。可惜包在实际运用时并没有这么简单,很多细节问题的干扰让包的使用比理论上更为复杂一些。

3. 包的实际例子

这里用一个能够运行的例子来演示包机制的内部工作原理。

文件名和路径将显示为普通文本,以便明确标识当前提及的是文件/目录,还是由该文件/目录定义的模块/包。

给出了这个包示例中将会用到的文件。

mathproj/__init__.py文件:

print("Hello from mathproj init")
__all__ = ['comp']
version = 1.03

 mathproj/comp/__init__.py文件:

__all__ = ['c1'] 
print("Hello from mathproj.comp init")

 mathproj/comp/c1.py文件:

x = 1.00

mathproj/comp/numeric/__init__.py文件:

print("Hello from numeric init")

 mathproj/comp/numeric/n1.py文件:

from mathproj import version
from mathproj.comp import c1
from mathproj.comp.numeric.n2 import h
def g():
    print("version is", version)
    print(h())

 mathproj/comp/numeric/n2.py文件:

def h():
    return "Called function h in module n2"

现在假定mathproj目录中已经创建了上述这些文件,并且mathproj目录已位于Python搜索路径中。只要在执行这些示例时,保证Python的当前工作目录中包含了mathproj目录,也就足矣。

注意:这里没有必要为每个例子新开一个Python shell。

通常可以在之前运行过示例的Python shell中执行别的示例,仍然可以看到运行结果。

要让本示例能够正常运行,Python命名空间必须是干净的,不能被之前的import语句修改过。如果确实要运行以下示例,请确保在各自的shell中单独运行各个示例。在IDLE中需要退出并重新启动程序,而不能只是关闭再重新打开其shell窗口。

1)包内的__init__.py文件

大家大概已经注意到了,包中的所有目录都会包含一个名为__init__.py的文件,mathproj、mathproj/comp和mathproj/comp/numeric里都有。__init__.py文件有两个用途。

Python要求,只有包含__init__.py文件的目录才会被识别为包。这可防止意外导入包含其他Python代码的目录。

当第一次加载包或子包时,Python会自动执行__init__.py文件。有了这种自动执行的机制,就能够完成任何必要的包初始化工作。

上述第一点,通常更要紧一些。很多包都不需要在其__init__.py文件中写入任何内容,只要保证有个空的__init__.py文件就行了。

2)mathproj包的基本用法

在详细介绍包之前,先来看看如何访问mathproj包中的内容。新开一个Python shell,然后执行以下语句:

>> import mathproj
Hello from mathproj init

如果一切顺利,应该会看到一个新的命令提示符,不会有错误信息。同时,mathproj/__init__.py文件中的代码会把消息“Hello from mathproj init”显示在屏幕上。下面马上还会对__init__.py文件做详细介绍,现在只要明白第一次加载包时会自动执行该文件。

mathproj/__ init__.py文件将把变量version赋值为1.03。变量version在mathproj包命名空间的作用域内,创建完毕后就可以通过mathproj访问,甚至在mathproj/__ init__.py文件之外也能看到它:

>>> mathproj.version
1.03

在使用过程中,包看起来与模块很类似,都可以通过属性访问到其内部定义的对象。这并不奇怪,因为包就是模块的汇总。 

3)子包和子模块的加载

下面开始介绍mathproj包中定义的各个文件是如何相互交互的。不妨来调用一下mathproj/comp/numeric/n1.py文件中定义的函数g。

显然第一个问题就是,该模块是否已经加载完毕了。包mathproj已经加载过了,但它的子包呢?键入以下语句,即可查看Python是否已认到了该模块:

>>> mathproj.comp.numeric.n1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'mathproj' has no attribute 'comp'

也就是说,只加载包的顶级模块是不够的,这并不会加载其全部子模块。这是符合Python理念的,即不应该在背地里执行操作。清晰比简洁更重要。

这种限制克服起来很容易。只要导入所需模块即可,然后就能执行该模块中的函数g了:

>>> import mathproj.comp.numeric.n1
Hello from mathproj.comp init
Hello from numeric init
>>> mathproj.comp.numeric.n1.g() 
version is 1.03
Called function h in module n2

不过请注意,加载mathproj.comp.numeric.n1会有一个副作用,就是把Hello开头的行打印出来。这两行是被__init__.py文件中的print语句打印出来的,__init__.py文件分别位于目录mathproj/comp和mathproj/comp/numeric中。

换句话说,在Python能够导入mathproj.comp.numeric.n1之前,必须先导入mathproj.comp再导入mathproj.comp.numeric。每当包首次导入时,都会执行其关联的__init__.py文件,于是就生成了这两条Hello行。如果要确认在导入mathproj.comp.numeric.n1的过程中,会把mathproj.comp和mathproj.comp.numeric都一并导入,可以检查mathproj.comp和mathproj.comp.numeric当前是否已被Python会话识别了:

>>> mathproj.comp
<module 'mathproj.comp' from 'mathproj/comp/__init__.py'>
>>> mathproj.comp.numeric
<module 'mathproj.comp.numeric'  from 'mathproj/comp/numeric/__init__.py'>

4)包内的import语句

包内的文件不会自动获得对包内其他文件的访问权限,无法访问同一包内其他文件中定义的对象。与外部模块一样,必须用import语句显式访问包内其他文件中的对象。如果要了解这种import在实践中的使用情况,请回顾一下n1子包。

n1.py中包含了以下代码:

from mathproj import version
from mathproj.comp import c1
from mathproj.comp.numeric.n2 import h
def g():
    print("version is", version)
    print(h())

g用到了两个version,一个来自顶级mathproj包,另一个来自n2模块的函数h。

因此,g所在的模块必须把version和h都导入才能访问到它们。version的导入可以像mathproj包外的import语句一样,写成from mathproj import version。

在本例中,通过from mathproj.comp.numeric.n2 import h语句显式导入了h。这种技术可用于导入任何文件,显式导入包文件始终都是允许的。但因为n2.py与n1.py位于同一个目录中,所以还可以使用相对导入方式,只要在子模块名前加一个句点即可。也就是可以把n1.py的第三行写成以下格式,一样可以奏效:

from .n2 import h

可以增加多个句点,以便在包的层次结构中多级上移,并且可以添加模块名称。可以不写成:

from mathproj import version
from mathproj.comp import c1
from mathproj.comp.numeric.n2 import h

而可以把导入n1.py写成:

from ... import version
from .. import c1
from . n2 import h

相对导入输入起来比较方便快捷,但请注意它们与模块的__name__属性相关。因此,任何执行主模块和__name__属性为__main__的模块,都不能采用相对导入方式。

4. __all__属性

回顾一下mathproj中定义的各个__init__.py文件,其中有些文件里定义了一个名为__all__的属性。该属性与from ... import *这类语句的执行有关,需要在此说明一下。

一般来说,如果外部代码执行了mathproj import *语句,就应该从mathproj导入全部的非私有对象名称。实际上这比较难以实现。主要问题是,有些操作系统对文件名的定义规则比较含糊。由于包中的对象可能是由文件或目录定义的,这就导致子包导入后其确切名称也含糊不定。例如,写的是from mathproj import *,那么comp会被导入为comp、Comp还是COMP呢?如果想要只依赖操作系统给出的名称,那么结果可能是不可预测的。

上述问题没有很好的解决方案,这是由于操作系统设计欠佳造成的先天不足。作为最佳修正方案,Python引入了__all__属性。如果__init__.py文件中包含__all__,__all__应该给出一个字符串列表,定义对该包执行from ... import *时应该导入的名称。如果未提供__all__,则from ... import *不会对该包执行任何操作。因为文本文件中的大小写始终是有意义的,所以在其名下要导入对象的名称不会是含糊不定的。如果操作系统认为comp与COMP是一样的,那就是操作系统的问题。

请再次启动Python,键入以下语句:

>>> from mathproj import *
Hello from mathproj init
Hello from mathproj.comp init

mathproj/__init__.py文件中的__all__属性只包含一项数据comp,因此import语句就只会导入comp。要想查看当前Python会话是否识别了comp,这相当简单:

>>> comp
<module 'mathproj.comp' from 'mathproj/comp/__init__.py'>

但是注意,用from ... import *语句不会发生递归导入名称。comp包的__all__属性包含c1,但是from mathproj import *语句并不会把c1也神奇地加载进来:

>>> c1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c1' is not defined

如果要从mathproj.comp插入对象名,必须再显式导入一次:

>>> from mathproj.comp import c1
>>> c1
<module 'mathproj.comp.c1' from 'mathproj/comp/c1.py'>

5. 包的合理使用

大多数包的结构,都不应该像以上例子暗示的那么复杂。有了Python包机制,包设计时的复杂程度和嵌套层次就能有很大的自由度了。显然能够构建非常复杂的包,但是否应该那么复杂却并不那么明显。

以下是几条适用于大多数情况的建议:

  • 包不应采用嵌套很深的目录结构。除非代码量极其庞大,否则没有必要这样做。大多数包只需要一个顶级目录即可。两层目录结构就应该能有效处理绝大部分情况。正如Tim Peters在“Python之禅”中所述,“平直胜于嵌套”,参见附录A。
  • 只要不在__all__属性中列出,就可以用__all__属性对from ... import *隐藏这些对象名称。尽管如此,但这可能并不算是一种好方案,因为这会导致不同导入方式的结果不一致。如果需要隐藏对象名称,请用前导双下划线让它们成为私有对象。

八、Python库

1、Python标准库

在Python中,所谓的库是由多个组件组成的,包括无须import语句即可使用的数值和列表之类的内置数据类型和常量,以及一些内置函数和异常。库中最大的部分是大量的模块。只要安装了Python,就可以用库来操作各类数据和文件、与操作系统交互、为众多互联网协议编写服务端和客户端、开发和调试代码。

下面将挑重点概述一下。虽然大多数主要模块都已经提到过了,但最新最全的信息还是建议花时间自行查看一下库参考手册,位于Python文档中。特别是在去寻找外部库之前,请务必浏览一遍Python已有的库。也许会有令人惊讶的发现。

1. 各种数据类型的管理

标准库中天然就包含了对Python内置类型的支持。此外,标准库中还有处理多种数据类型的3大类模块,即字符串服务、数据类型和数值模块。

字符串服务包括了处理字节和字符串的模块,下表已列出。

模块 说明和应用场景
string 与数字或空白符这种字符串常量进行比较;格式化字符串
re 用正则表达式查找和替换文本
struct 将字节数据理解为打包的二进制数据,以及从文件读写结构化数据
difflib 用于评估差异的助手类,可查找字符串或序列之间的差异,可创建补丁和差异文件
textwrap 打包和填充文本,以及通过分割行或添加空格来设置文本格式

这些模块主要处理3类操作,即字符串及文本、字节序列、Unicode操作。

数据类型大类则涵盖了各种各样的数据类型模块,特别是时间、日期和集合,如下表所示。

模块 说明和应用场景
datetime 、 calendar 日期、时间和日历操作
collections 容器数据类型
enum 允许创建枚举器类,将符号名称绑定到常量值上
array 高效的数值型数组
sched 事件调度器
queue 同步队列类
copy 浅复制和深复制操作
pprint 对数据进行美观打印
typing 支持像对象类型提示那样的代码注释,特别是针对函数的参数和返回值

正如其名,数值和数学模块用于处理数字和数学运算,其中最常见的模块在下表中列出。

模块 说明和应用场景
numbers 数值对象的抽象基类
math 、 cmath 实数和复数用到的数学函数
decimal 十进制定点和浮点运算
statistics 进行数学统计计算的函数
fractions 有理数
random 生成伪随机数,随机选取、打乱序列成员
itertools 为高效循环创建迭代器的函数
functools 针对可调用对象的高阶函数和操作
operator 函数形式的标准运算符

如果需要创建自己的数值类型,并处理很多数学运算操作,有这些模块足矣。

2. 文件和存储操作

标准库中的另一大类是文件、存储和数据持久化操作模块,在下表中汇总列出。此大类包括文件访问模块、数据持久化和压缩模块和特殊文件格式处理模块。

模块 说明和应用场景
os.path 执行常见的路径名操作
pathlib 以面向对象的方式处理路径名
fileinput 从多个输入流迭代遍历数据行
filecmp 比较文件和目录
tempfile 生成临时文件和目录
glob 、 fnmatch 采用UNIX风格的路径名和文件名模式处理
linecache 实现对文本文件的随机访问
shutil 执行高级文件操作
pickle 、 shelve 提供Python对象序列化和持久化能力
sqlite3 操作SQLite数据库的DB-API 2.0接口
zlib 、 gzip 、 bz2 、 zipfile 、 tarfile 操作归档文件及进行压缩
csv 读写CSV文件
configparser 使用配置文件解析器,读写Windows风格的.ini配置文件

3. 操作系统服务的访问

这是另一大类标准库,包含了与操作系统打交道的模块。

如下表所示,此大类包括了很多工具库,例如,处理命令行参数、重定向文件及打印输出和输入、写入日志文件、运行多个线程或进程、加载供Python使用的非Python(通常为C)库。

模块 说明
os 各种操作系统接口函数
io 用于处理流的核心工具
time 时间的访问和转换
optparse 强大的命令行参数解析工具
logging Python的日志记录工具
getpass 可移植的密码输入工具
curses 文本终端界面下用于控制字符区域的显示
platform 访问底层平台的标识信息
ctypes 让Python能调用外部函数库
select 等待I/O完成
threading 线程的高层接口
multiprocessing 基于进程的线程接口
subprocess 子进程的管理

4. 互联网协议及其数据格式的使用

互联网协议及其数据格式这一大类,涉及的任务是对很多互联网数据交换标准格式进行编/解码,从MIME及其他编码,到JSON及XML。这里还包含为常见服务(尤其是HTTP)编写服务端和客户端的模块,以及为自定义服务编写通用套接字服务端。

下表列出了其中最常用的模块:

模块 说明
socket 、 ssl 底层网络接口及套接字对象的SSL封装
email 电子邮件和MIME处理包
json JSON编/解码
mailbox 以各种格式处理邮箱
mimetypes 将文件名映射为MIME类型
base64 、 binhex 、 binascii 、 quopri 、 uu 用各种编码格式对文件或流进行编/解码
html.parser 、 html.entities 解析HTML和XHTML
xml.parsers.expat 、 xml.dom 、 xml.sax 、 xml.etree.ElementTree 各种XML解析器和工具
cgi 、 cgitb CGI(Common Gateway Interface)支持
wsgiref WSGI工具和参考实现
urllib.request 、 urllib.parse 打开及解析URL
ftplib 、 poplib 、 imaplib 、 nntplib 、 smtplib 、 telnetlib 各种互联网协议的客户端
socketserver 网络服务端的框架
http.server HTTP服务端
xmlrpc.client 、 xmlrpc.server XML-RPC客户端和服务端

5. 开发调试工具及运行时服务

Python提供了几个运行时模块,可帮助大家在运行时对Python代码进行调试、测试、修改和其他交互操作。

如下表所示,该大类包括两个测试工具、多个性能分析器、与错误的跟踪信息(Traceback)进行交互的模块、解释器的垃圾回收器等,还包括可对其他模块的导入进行调整的模块。

模块 说明
pydoc 文档生成器和在线帮助系统
doctest 测试交互式Python例程
unittest 单元测试框架
test.support 用于测试的工具函数
pdb Python调试器
profile 、 cProfile Python性能分析器
timeit 对代码片段进行运行计时
trace 跟踪Python语句的执行过程
sys 系统特有的参数和函数
atexit 程序退出过程的处理
__future__ 定义未来语句,这是指将要加入Python的新特性
gc 垃圾回收器接口
inspect 查看活跃对象的信息
imp 访问导入机制内部
zipimport 从zip存档文件中导入模块
modulefinder 查找脚本用到的所有模块

2、标准库之外的库

Python的“功能齐备”理念和足量的标准库,意味着只要打开Python就能完成很多工作。但是有时不可避免地会需要用到一些Python未能自带的功能。如果需要执行标准库中没有的操作,这里提供一些可供选择的方案。

1. 添加其他Python库

查找Python包或模块的过程十分简单,类似在搜索引擎中输入要寻找的功能(如mp3标签和Python),然后结果还会排序。幸运的话,可能会找到按照操作系统打包的模块,或是Windows可执行程序,或是macOS安装程序,或是Linux版的软件包。

因为安装程序或包管理器会正确处理将模块加入系统的全部细节,所以以上技术是向已安装好的Python环境添加库的最简单方式之一。比较更复杂的库也可能采用这种安装方案,例如,构建条件(build requirement)和依赖关系(dependency)都很复杂的科学计算库。

除科学计算库之外,一般而言这种预置包并不是Python软件的规范设计。这种包往往有点陈旧,安装位置和安装方式也不够灵活。

2. 通过pip和venv安装Python库

如果所需第三方模块没有按照平台预先打包,那就必须选用其源代码发行版。这就存在几个问题。
为了安装模块,必须先找到并下载下来。

即便只想正确安装一个Python模块,也可能会在处理Python路径和系统权限时遇到一定的麻烦。因此标准化的安装系统很有用处。

针对这两个问题,Python提供了pip作为目前的解决方案。pip会在Python Package Index中(不久会有更多源)查找模块,然后下载模块及其全部依赖项,并负责安装。pip的基本语法非常简单。

例如,要在命令行安装现在很流行的requests库,仅需执行以下语句即可:

$ python3.6 -m pip install requests

要将库升级到最新版本,只需要加上--upgrade参数即可:

$ python3.6 -m pip install –-upgrade requests

如果需要指定包的某个版本,可以在包名后面加上版本号,如下所示:

$ python3.6 -m pip install requests==2.11.1 
$ python3.6 -m pip install requests>=2.9

1)带--user 标志的安装

很多时候,不能或不想在Python的主系统实例中安装Python包。也许要用的是一个前沿版本的库,但其他某些应用程序(或系统本身)仍然要用旧的版本。或者可能没有权限修改系统默认的Python环境。在这些情况下,有一种解决方案是安装库时带上--user标志,该标志会把库安装在用户的主目录中,其他任何用户都无法访问该目录。

以下语句只会为本地用户安装requests库:

$ python3.6 -m pip install --user requests

如上所述,如果当前系统没有足够的管理员权限来安装软件,或者要安装不同版本的模块,那么本方案就特别有用。如果需求超出了这里讨论的基本安装方法,那么最好是从Python文档的“Installing Python Modules”(安装Python模块)开始学习。

2)虚拟环境

如果要避免把库安装到系统Python中去,那还有一个更好的方案可供选用,这被称为虚拟环境(virtual environment,virtualenv)。虚拟环境是一个独立的目录结构,已经安装好了Python及其附属包。因为整个Python环境都处于虚拟环境中,所以在其中安装的库和模块均不会与主系统或其他虚拟环境中的库和模块发生冲突,从而允许不同的应用程序使用不同版本的Python及其包。

创建和使用虚拟环境有两个步骤。首先是创建虚拟环境:

$ python3.6 -m venv test-env

此步骤在名为test-env的目录中创建环境,其中将会安装好Python和pip。等环境创建完毕后,下一步就是将其激活。

在Windows系统中,请执行以下操作:

> test-env\Scripts\activate.bat

在UNIX或macOS系统中,请用source命令执行激活脚本:

$ source test-env/bin/activate

当环境激活完毕后,就可以像上面介绍过的那样用pip管理包了。但在虚拟环境中,pip是一条单独的命令:

$ pip install requests

此外,用于创建虚拟环境的Python版本就是该环境的默认版本,因此只能用python命令,而不能用python3或python3.6。

对管理项目及其依赖项而言,虚拟环境非常有用,并且也是非常标准的做法,尤其是适用于同时开发多个项目的人员。更多信息请查看Python在线文档中Python教程的“虚拟环境和包”部分。

3. PyPI(即“奶酪商店”)

虽然distutils包可以完成包的管理,但有一个问题:必须找到正确的包,这可能是一件苦差事。在找到包之后,最好得有一个合理可靠的下载源。

为了满足这种需求,多年来已有很多Python包存储库可用了。目前,Python代码的官方库(但绝不是唯一)是Python网站上的Python Package Index或PyPI。以前PyPI也被称为“奶酪商店”(The Cheese Shop),名称来源于Monty Python的同名短剧。从主页上的链接即可访问PyPI,也可以直接访问PyPI官方网站。PyPI中包含了6000多个适用于各个Python版本的包,按照添加日期和名称排列,并且可按类别搜索和细分。

新版本的PyPI已经就绪,目前叫作“Warehouse”。该版本仍在测试中,但有望能提供更顺畅、更友好的搜索体验。

如果在标准库找不到所需的函数,下一站应该就是PyPI。

九、异常

1、异常简介

假定程序会调用一个save_to_file函数,以便将当前文档保存到文件中去。该函数将会调用多个子函数,将整个文档的不同部分存入文件。

例如,save_text_to_file用于保存实际的文档文本,save_prefs_to_file用于保存文档的用户配置信息,save_formats_to_file用于保存文档的用户自定义格式,等等。这些子函数都可以相应地调用各自的子函数,把更细小的部分存入文件。最底层则是内置的系统函数,将原始格式的数据写入文件,并且报告文件写入操作的成功或失败信息。

在可能产生磁盘空间错误的每个函数中,都可以放入错误处理代码,但这种做法没有多大意义。错误处理代码唯一能做的事情,就是打开一个对话框,告诉用户磁盘空间不足了,要求用户删除一些文件并再次保存。在所有磁盘写入操作的地方,都复制这段代码,这没有什么意义。应该把错误处理代码放入磁盘写入的主函数save_to_file当中。

不幸的是,为了能确定何时该调用错误处理代码,save_to_file对调用的每个磁盘写入函数都必须检查磁盘空间错误,并返回标识磁盘写入成功或失败的状态值。此外,save_to_file函数也必须在每次调用磁盘写入函数时都显式检查是否成功,哪怕它不关心函数是否返回失败也需要检查。使用类C语法的代码将如下所示:

const ERROR = 1;
const OK = 0; 
int save_to_file(filename) {
    int status;
    status = save_prefs_to_file(filename);
    if (status == ERROR) {
        ...处理错误...
    }
    status = save_text_to_file(filename);
    if (status == ERROR) {
        ...处理错误...
    }
    status = save_formats_to_file(filename);
    if (status == ERROR) {
        ...处理错误...
    }     
    .
    .
    .
}
int save_text_to_file(filename) {
    int status;
    status = ...调用底层函数来写入文本大小...
    if (status == ERROR) {
        return(ERROR);
    }
    status = ...调用底层函数来写入实际的文本数据...
    if (status == ERROR) {
        return(ERROR);
    }     
    .
    .
    .
}

在函数save_prefs_to_file和save_formats_to_file中,以及需要直接或以调用函数方式写入filename文件的函数中,都要采用同样的处理方式。

在这种方式下,只要调用时有引发错误的可能,发起调用的函数和过程里就需要包含检查错误的代码,因此用于检测和处理错误的代码可能会成为整个程序的重头戏。程序员通常不会把时间或精力投入这种全面的错误检查,程序最终会变得不够可靠,容易崩溃。

在上一种方案的程序中,绝大部分的错误检查代码显然都是重复的。代码对每次文件写入错误都要进行检查,并在检测到错误时将错误状态消息回传给发起调用的过程。磁盘空间错误只在一个地方得以处理,就是在最外层的save_to_file函数中。也就是说,大部分错误处理代码只是“管道”(plumb)代码,用于将引发错误的地方和处理错误的地方连通。真正要做的事情其实应该是去掉管道代码,写成如下的代码:

def save_to_file(filename)
    尝试执行以下代码块
        save_text_to_file(filename)
        save_formats_to_file(filename)
        save_prefs_to_file(filename)
         .
         .
         .
    除非,执行上述代码块时磁盘空间不足,那就执行以下代码
         ...处理错误...

def save_text_to_file(filename) 
    ...调用底层函数来写入文本大小...
    ...调用底层函数来写入实际的文本数据...
    .
    .
    .

这里的错误处理代码完全从下级函数中移除了,错误由系统内置的文件写入函数生成,并直接传到save_to_file函数,交由错误处理代码直接处理。

虽然用C语言无法编写这段代码,但是提供“异常”机制的语言就能准确支持这种方式,Python当然就是这样一种语言。异常机制可以让代码更加清晰,错误处理得更加到位。

异常较为正式的定义:产生异常的动作被称为引发(raise)或抛出(throw)异常。在上面的示例中,所有的异常都是由磁盘写入函数引发的。不过异常也可以由其他任何函数引发,或者由自己的代码显式引发。在上述示例中,如果磁盘空间不足,底层的磁盘写入函数(代码中未给出)就会引发异常。

响应异常的动作被称为捕获(catch)异常,处理异常的代码则称为异常处理代码(exception-handling code)或简称为异常处理程序(exception handler)。在上述示例中,“除非,……”这行将会捕获磁盘写入异常,“……处理错误……”处的代码应该是磁盘写入异常(空间不足)的异常处理程序。异常处理程序还可以有很多,以便处理其他类型的异常,甚至对于同一类异常,也可以存在其他异常处理程序,只是位于代码的其他位置。

根据引发异常的事件不同,程序可能需要采取不同的操作。磁盘空间耗尽、内存不足、除零错误,引发这些异常时的处理方式都完全不同。处理多种异常的方案之一,就是全局记录一条标识异常原因的错误消息,并让所有的异常处理程序都检查该错误消息并进行适当的操作。实践证明,采用另一种方案会灵活得多。

与大多数实现了异常机制的现代语言一样,Python并不是只定义了一种异常,而是定义多种不同类型的异常,对应于可能发生的各种问题。根据底层事件的不同,可以引发不同类型的异常。此外,还可以让捕获异常的代码仅捕获特定类型的异常。

“除非,执行上述代码块时磁盘空间不足,那就执行以下代码”,此句伪代码就指定了,本段异常处理代码只对磁盘空间异常感兴趣。这段异常处理代码不会捕获其他类型的异常。其他类型的异常可能会由数字异常处理程序捕获,如果不存在对应类型异常的处理程序,那就会导致程序提前退出并报错。

2、Python中的异常

整个Python异常机制都是按照面向对象的规范搭建的,这使得它灵活而又兼具扩展性。即便大家对面向对象编程(object-oriented programming,OOP)不太熟悉,使用异常时也无须特地去学习面向对象技术。

异常是Python函数用raise语句自动生成的对象。在异常对象生成后,引发异常的raise语句将改变Python程序的执行方式,这与正常的执行流程不同了。不是继续执行raise的下一条语句,也不执行生成异常后的下一条语句,而是检索当前函数调用链,查找能够处理当前异常的处理程序。如果找到了异常处理程序,则会调用它,并访问异常对象获取更多信息。如果找不到合适的异常处理程序,程序将会中止并报错。

总体而言,Python对待错误处理的方式与Java等语言的常见方式不同。那些语言有赖于在错误发生之前就尽可能地检查出来,因为在错误发生后再来处理异常,往往要付出各种高昂的成本,也被称为“三思而后行”(Look Before You Leap, LBYL)方式。

而Python可能更依赖于“异常”,在错误发生之后再做处理。虽然这种依赖看起来可能会有风险,但如果“异常”能使用得当,代码就会更加轻巧,可读性也更好,只有在发生错误时才会进行处理。这种Python式的错误处理方法通常被称为“先斩后奏”(Easier to Ask Forgiveness than Permission, EAFP)。

1. Python异常的类型

为了能够正确反映引发错误的实际原因,或者需要报告的异常情况,可以生成各种不同类型的异常对象。Python 3.6提供的异常对象有很多类型:

BaseException
    SystemExit
    KeyboardInterrupt
    GeneratorExit
    Exception
        StopIteration
        ArithmeticError
            FloatingPointError
            OverflowError
            ZeroDivisionError
        AssertionError
        AttributeError
        BufferError
        EOFError
        ImportError
            ModuleNotFoundError
        LookupError
            IndexError
            KeyError
        MemoryError
        NameError
            UnboundLocalError
        OSError
            BlockingIOError
            ChildProcessError
            ConnectionError
                BrokenPipeError
                ConnectionAbortedError
                ConnectionRefusedError
                ConnectionResetError
            FileExistsError
            FileNotFoundError
            InterruptedError
            IsADirectoryError
            NotADirectoryError
            PermissionError
            ProcessLookupError
            TimeoutError
        ReferenceError
        RuntimeError
            NotImplementedError
        RecursionError
        SyntaxError
            IndentationError
                TabError
            SystemError
        TypeError
        ValueError
            UnicodeError
                UnicodeDecodeError
                UnicodeEncodeError
                UnicodeTranslateError
        Warning
            DeprecationWarning
            PendingDeprecationWarning
            RuntimeWarning
            SyntaxWarning
            UserWarning
            FutureWarning
            ImportWarning
            UnicodeWarning
            BytesWarningException
            ResourceWarning

Python的异常对象是按层级构建的,上述异常列表中的缩进关系正说明了这一点,可以从__builtins__模块中获取按字母顺序排列的异常对象清单。

每种异常都是一种Python类,继承自父异常类。但大家如果还未接触过OOP,也不必担心。例如,IndexError也是LookupError类和Exception类(通过继承),且还是BaseException。

这种层次结构是有意为之的,大部分异常都继承自Exception,强烈建议所有的用户自定义异常也都应是Exception的子类,而不要是BaseException的子类。理由如下:

try:
    # 执行一些异常操作
except Exception:
    # 处理异常

上述代码中,仍旧可以用Ctrl+C中止try语句块的执行,且不会引发异常处理代码。因为KeyboardInterrupt异常不是Exception的子类。

虽然在文档中可以找到每种异常的解释,但最常见的几种异常通过动手编程就能很快熟悉了。

2. 引发异常

异常可由很多Python内置函数引发:

>>> alist = [1, 2, 3]
>>> element = alist[7]
Traceback (innermost last):
  File "<stdin>", line 1, in ?
IndexError: list index out of range

Python内置的错误检查代码,将会检测到第二行请求读取的列表索引值不存在,并引发IndexError异常。该异常一直传回顶层,也就是交互式Python解释器,解释器对其处理的方式是打印出一条消息表明发生了异常。

在自己的代码中,还可以用raise语句显式地引发异常。raise语句最基本的形式如下:

raise exception(args)

exception(args)部分会创建一个异常对象。新异常对象的参数通常应是有助于确定错误情况的值,后续将会介绍。在异常对象被创建之后,raise会将其沿着Python函数堆栈向上层抛出,也就是当前执行到raise语句的函数。新创建的异常将被抛给堆栈中最近的类型匹配的异常捕获代码块。如果直到程序顶层都没有找到相应的异常捕获代码块,程序就会停止运行并报错,在交互式会话中则会把错误消息打印到控制台。

请尝试以下代码:

>>> raise IndexError("Just kidding")
Traceback (innermost last):
  File "<stdin>", line 1, in ?
IndexError: Just kidding

上面用raise生成的消息,乍一看好像与之前所有的Python列表索引错误消息都很类似。再仔细查看一下就会发现,情况并非如此。实际的错误并不像之前的错误那么严重。

创建异常时,常常会用到字符串参数。如果给出了第一个参数,大部分内置Python异常都会认为该参数是要显示出来的信息,作为对已发生事件的解释。不过情况并非总是如此,因为每个异常类型都有自己的类,创建该类的异常时所需的参数,完全由类的定义决定。

此外,由程序员创建的自定义异常,经常用作错误处理之外的用途,因此可能并不会用文本信息作为参数。

3. 捕获并处理异常

异常机制的重点,并不是要让程序带着错误消息中止运行。要在程序中实现中止功能,从来都不是什么难事。异常机制的特别之处在于,不一定会让程序停止运行。通过定义合适的异常处理代码,就可以保证常见的异常情况不会让程序运行失败。或许可以通过向用户显示错误消息或其他方法,或许还可能把问题解决掉,但是不会让程序崩溃。

以下演示了Python异常捕获和处理的基本语法,用到了try、except,有时候还会用else关键字:

try:
    body
except exception_type1 as var1:
    exception_code1
except exception_type2 as var2:
    exception_code2
    .
    .
    .
except:
    default_exception_code 
else:
    else_body
finally:
    finally_body

首先执行的是try语句的body部分。如果执行成功,也就是try语句没有捕获到有异常抛出,那就执行else_body部分,并且try语句执行完毕。因为这里有条finally语句,所以接着会执行finally_body部分。如果有异常向try抛出,则会依次搜索各条except子句,查找关联的异常类型与抛出的异常匹配的子句。如果找到匹配的except子句,则将抛出的异常赋给变量,变量名在关联异常类型后面给出,并执行匹配except子句内的异常处理代码。例如,except exception_type as var:这行匹配上了某抛出的异常exc,就会创建变量var,并在执行该except语句的异常处理代码之前,将var的值赋为exc。var不是必需的,可以只出现except exception_type:这种写法,给定类型的异常仍然能被捕获,只是不会把异常赋给某个变量了。

如果没有找到匹配的except子句,则该try语句就无法处理抛出的异常,异常会继续向函数调用链的上一层抛出,期望有外层的try能够处理。try语句中的最后一条except子句,可以完全不指定任何异常类型,这样就会处理所有类型的异常。对于某些调试工作和非常快速的原型开发,这种技术可能很方便。但通常这不是个好做法,所有错误都被except子句掩盖起来了,可能会让程序的某些行为令人难以理解。

try语句的else子句是可选的,也很少被用到。当且仅当try语句的body部分执行时没有抛出任何错误时,else子句才会被执行。

try语句的finally子句也是可选的,在try、except、else部分都执行完毕后执行。如果try块中有异常引发并且没被任何except块处理过,那么finally块执行完毕后会再次引发该异常。因为finally块始终会被执行,所以能在异常处理完成后,通过关闭文件、重置变量之类的操作提供一个加入资源清理代码的机会。

4. 自定义新的异常

定义自己的异常十分简单。用以下两行代码就能搞定:

class MyError(Exception):
    pass

上述代码创建了一个类,该类将继承基类Exception中的所有内容。不过如果不想弄清楚细节,则大可不必理会。

以上异常可以像其他任何异常一样引发、捕获和处理。如果给出一个参数,并且未经捕获和处理,参数值就会在跟踪信息的最后被打印出来:

>>> raise MyError("Some information about what went wrong")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
__main__.MyError: Some information about what went wrong

当然,上述参数在自己编写的异常处理代码中也是可以访问到的:

try:
    raise MyError("Some information about what went wrong") 
except MyError as error:
    print("Situation:", error)

运行结果将如下所示:

Situation: Some information about what went wrong

如果引发异常时带有多个参数,这些参数将会以元组的形式传入异常处理代码中,元组通过error变量的args属性即可访问到:

try:
    raise MyError("Some information", "my_filename", 3)
except MyError as error:
    print("Situation: {0} with file {1}\n error code: {2}".format(
       error.args[0],
error.args[1], error.args[2]))

 运行结果将如下所示:

Situation: Some information with file my_filename
error code: 3

异常类型是常规的Python类,并且继承自Exception类,所以建立自己的异常类型层次架构,供自己的代码使用,就是一件比较简单的事情。

如何创建自己的异常,完全由需求决定。如果正在编写的是个小型程序,可能只会生成一些唯一的错误或异常,那么如上所述采用Exception类的子类即可。如果正在编写大型的、多文件的、完成特定功能的代码库(如天气预报库),那就可以考虑单独定义一个名为WeatherLibraryException的类,然后将库中所有的不同异常都定义为WeatherLibraryException的子类。

5. 用assert语句调试程序

assert语句是raise语句的特殊形式:

assert expression, argument

如果expression的结算结果为False,同时系统变量__debug__也为True,则会引发携带可选参数argument的AssertionError异常。__debug__变量默认为True。带-O或-OO参数启动Python解释器,或将系统变量PYTHONOPTIMIZE设为True,则可以将__debug__置为False。可选参数argument可用于放置对该assert的解释信息。

如果__debug__为False,则代码生成器不会为assert语句创建代码。在开发阶段,可以用assert语句配合调试语句对代码进行检测。assert语句可以留存在代码中以备将来使用,在正常使用时不存在运行开销:

>>> x = (1, 2, 3)
>>> assert len(x) > 5, "len(x) not > 5"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: len(x) not > 5

6. 异常的继承架构

之前已经介绍过,Python的异常是分层的架构。这里将对这种架构作深入介绍,包括这种架构对于except子句如何捕获异常的意义。

请看以下代码:

try:
    body
except LookupError as error:
    exception code
except IndexError as error:
    exception code

这里将会捕获IndexError和LookupError这两种异常。正巧IndexError是LookupError的子类。如果body抛出IndexError,那么错误会首先被“except LookupError as error:”这行检测到。由于IndexError继承自LookupError,因此第一条except子句会成功执行,第二条except子句永远不会用到,因为它的运行条件被第一条except子句包含在内了。

相反,将两条except子句的顺序互换一下,可能就有意义了。这样第一条子句将处理IndexError,第二条子句将处理除IndexError之外的LookupError。

7. 示例:用Python编写的磁盘写入程序

在把文档写入磁盘时,该程序需要检查磁盘空间不足的情况:

def save_to_file(filename) :
    try:
       save_text_to_file(filename)
       save_formats_to_file(filename)
       save_prefs_to_file(filename)
       .
       .
       .
    except IOError:
       ...处理错误...
def save_text_to_file(filename):
    ...调用底层函数来写入文本大小...
    ...调用底层函数来写入实际的文本数据...
    .
    .
    .

注意,错误处理代码很不显眼,在save_to_file函数中与一系列磁盘写入调用放在一起了。那些磁盘写入子函数都不需要包含任何错误处理代码。程序一开始会比较容易开发,以后要添加错误处理代码也很简单。程序员经常这么干,尽管这种实现顺序不算最理想。

还有一点也值得注意,上述代码并不是只会对磁盘满的错误做出响应,而是会响应所有IOError异常。Python的内置函数无论何时无法完成I/O请求,不管什么原因都会自动引发IOError异常。可能这么做能满足需求,但如果要单独识别磁盘已满的情况,就得再做一些操作。

可以在except语句体中检查磁盘还有多少可用空间。如果磁盘空间不足,显然是发生了磁盘满的问题,应该在except语句体内进行处理。如果不是磁盘空间问题,那么except语句体中的代码可以向调用链的上层抛出该IOError,以便交由其他的except语句体去处理。如果这种方案还不足以解决问题,那么还可以进行一些更为极端的处理,例如,找到Python磁盘写入函数的C源代码,并根据需要引发自定义的DiskFull异常。最后的这种方案并不推荐,但在必要时应该知道有这种可能性的存在,这是很有意义的。

8. 示例:正常计算过程中的异常

异常最常见的用途就是处理错误,但在某些应被视作正常计算过程的场合,也会非常有用。设想一下电子表格程序之类的实现时可能会遇到的问题。像大多数电子表格一样,程序必须能实现涉及多个单元格的算术运算,并且还得允许单元格中包含非数字值。在这种应用程序中,进行数值计算时碰到的空白单元格,其内容可能被视作0值。包含任何其他非数字字符串的单元格可能被视作无效,并表示为Python的None值。任何涉及无效值的计算,都应返回无效值。

下面首先编写一个函数,用于对电子表格单元格中的字符串进行求值,并返回合适的值:

def cell_value(string):
    try:
        return float(string)
    except ValueError:
        if string == "":
            return 0
        else:
            return None

Python的异常处理能力使这个函数写起来十分简单。在try块中,将单元格中的字符串用内置float函数转换为数字,并返回结果。如果参数字符串无法转换为数字,float函数会引发ValueError异常。然后异常处理代码将捕获该异常并返回0或None,具体取决于参数字符串是否为空串。

有时候在求值时可能必须要对None值做出处理,下一步就来解决这个问题。在不带异常机制的编程语言中,常规方案就是定义一组自定义的算术求值函数,自行检查参数是否为None,然后用这些自定义函数取代内置函数,执行所有电子表格计算。但是,这个过程会非常耗时且容易出错。而且实际上这是在电子表格程序中自建了一个解释器,所以会导致运行速度的降低。本项目采用的是另一种方案。

所有电子表格公式实际上都可以是Python函数,函数的参数是被求值单元格的x、y坐标和电子表格本身,用标准的Python算术操作符计算结果,用cell_value从电子表格中提取必要的值。可以定义一个名为safe_apply的函数,在try块中用相应参数完成公式的调用,根据公式是否计算成功,返回其计算结果或者返回None: 

def safe_apply(function, x, y, spreadsheet):
    try:
        return function(x, y, spreadsheet)
    except TypeError:
        return None

上述两步改动,足以在电子表格的语义中加入空值(None)的概念。如果不用异常机制来开发上述功能,那将会是一次很有教益的练习(言下之意是,能体会到相当大的工作量)。

9. 异常的适用场合

使用异常来处理几乎所有的错误,是很自然的解决方案。往往是在程序的其余部分基本完成时,错误处理部分才会被加入进来。很遗憾事实就是如此,不过异常机制特别擅长用易于理解的方式编写这种事后错误处理的代码,更好听的说法是事后多加点错误处理代码。

如果程序中有计算分支已明显难以为继,然后可能有大量的处理流程要被舍弃,这时异常机制也会非常有用。电子表格示例就是这种情况,其他应用场景还有分支限界(branch-and-bound)算法和语法解析(parsing)算法。

3、用到with关键字的上下文管理器

有些操作场景遵循一种可预期的模式,有始必有终,如文件的读取操作。读取文件时,通常只需要打开一次文件,读取数据后关闭文件。按照异常的编程结构,可以将这种文件访问代码编写如下:

try:
    infile = open(filename)
    data = infile.read()
finally:
    infile.close()

Python 3为这种场景提供了一种更加通用的解决方案,即上下文管理器(context manager)。上下文管理器将代码块包裹起来,对进入(entry)和离开(departure)代码块时的操作进行集中管理,用with关键字进行标记。文件对象就是一种上下文管理器,可以利用这种能力读取文件:

with open(filename) as infile:
    data = infile.read()

这两行代码等效于上面的5行。无论操作是否成功,这两种方式都可预知,最后一次读取完成后将会立即关闭文件。第二种方式下关闭文件也能得以保证,因为这是文件对象的上下文管理功能之一,不需要再编写代码。也就是说,用了with关键字和上下文管理功能(本例中为文件对象),就无须操心例行的资源清理操作了。

正如所预期的那样,可以按需创建自己的上下文管理器。更多信息请查看标准库中contextlib模块的文档,包括如何创建上下文管理器,以及对它的各种操控方式。

上下文管理器非常适用于资源加/解锁、关闭文件、提交数据库事务之类的操作。自从引入以来,上下文管理器就已成为此类场景下标准的最佳实践。 

总结:

  • Python的异常处理机制和异常类,可为处理代码的运行时错误提供丰富的功能支持。
  • 利用try、except、else和finally块,挑选合适的异常类型,甚至可以自建异常类,就可以对处理和忽略异常的方式进行非常精细的控制。
  • Python的理念就是,除非明确把错误显式标识为静默处理,否则就不应该让错误默默地传递下去。
  • Python的异常类型是按照层次结构组织起来的,因为和Python的所有对象一样,异常对象也是基于类实现的。

十、Python程序

1、创建一个很简单的程序

只要是连续存放于某个文件中的多条Python语句,就可以被用作程序或脚本(script)。但更标准和实用的做法是引入更多的程序架构。最基本的形式下,只需要在文件中创建主控函数并调用该函数即可,代码如下所示。

script1.py文件:

def main():        ⇽---  主控函数main
    print("this is our first test script file")
main()             ⇽---  调用main函数

上述脚本中,main是主控函数,也是唯一一个函数。先定义主控函数,然后调用它。虽然在小型程序中没有太大的差别,但在创建较为大型的应用程序时,采用这种结构可以拥有更多选择和控制能力。因此从一开始就习惯这种用法,会是个好主意。

1. 从命令行启动脚本

如果用的是Linux/UNIX系统,请确保Python位于命令搜索路径中,并且当前目录是脚本文件所在目录。

然后在命令行中输入以下命令启动脚本:

python script1.py

如果用的是运行OS X的Macintosh系统,过程与其他UNIX系统是一样的。这里需要打开一个终端程序,该程序位于Applications(应用)文件夹的Utilities目录中。在OS X中运行脚本时还可有几个参数可选,下面很快就会介绍。

如果用的是Windows系统,请打开命令提示符(command prompt)或PowerShell。根据不同的Windows版本,命令提示符在菜单中的位置也不相同。在Windows 10系统中,命令提示符位于“Windows系统”菜单中。这两种shell打开时都会位于当前用户的主目录,必要时可用cd命令切换到某个子目录中去。

如果script1.py保存在桌面上,则运行命令如下所示:

C:\Users\naomi> cd Desktop        ⇽---  切换到桌面目录

C:\Users\naomi\Desktop> python script1.py        ⇽---  运行script1.py
this is our first test script file         ⇽---  script1.py的输出

C:\Users\naomi\Desktop>

2. 命令行参数

传递命令行参数的方式十分简单,代码如下所示。

script2.py文件:

import sys
def main():
    print("this is our second test script file")
    print(sys.argv)
main()

如果用以下命令行调用上述脚本:

python script2.py arg1 arg2 3

结果会是:

this is our second test script file
['script2.py', 'arg1', 'arg2', '3']

由此可见,命令行参数以字符串列表的形式存入sys.argv中了。

3. 脚本输入/输出的重定向

利用命令行参数,可以将脚本的输入和输出重定向。代码如下所示的这段简短的脚本,将用于展示重定向技术。

replace.py文件:

import sys
def main():
    contents = sys.stdin.read()    ⇽---  从stdin读取输入并存入contents
    sys.stdout.write(contents.replace(sys.argv[1], sys.argv[2]))     ⇽---  用第二个参数值替换第一个参数值
main()

以上脚本读取标准输入并原样写入标准输出,并将第一个参数替换为第二个参数。当按以下方式调用时,上述脚本将会把infile的副本写入outfile中,而且会把所有的“zero”替换为“0”:

python replace.py zero 0 <infile> outfile

注意,上述脚本在UNIX系统中可以正常执行。但在Windows系统中,只有从命令提示符窗口中启动脚本,输入/输出的重定向才会生效。一般来说,命令行:

python script.py arg1 arg2 arg3 arg4 < infile > outfile

的运行效果是,把input或sys.stdin的所有操作都定向为来自infile,把print或sys.stdout的所有操作都定向到outfile。这种效果如同是将sys.stdin设为只读模式('r')的infile,将sys.stdout设为只写('w')模式的outfile:

python replace.py a A < infile >> outfile

上面这行将使输出结果追加至outfile末尾,而不是像上一个例子中那样把文件覆盖掉。

还可以把一条命令的输出作为另一条命令的输入,也就是管道(pipe):

python replace.py 0 zero < infile | python replace.py 1 one > outfile

以上代码将使outfile包含infile的内容,只是其中所有的“0”都改成了“zero”,所有的“1”都改成了“one”。

4. argparse模块

经过配置,脚本可以接受命令行选项及参数。argparse正是为解析各类参数提供支持的模块,甚至还能生成用法帮助信息。

如果要使用模块argparse,可以创建一个ArgumentParser的实例,填入一定的参数,然后就可以把可选参数(optional argument)和位置参数(positional argument)都读取出来。

下面代码演示了该模块的用法。

opts.py文件:

from argparse import ArgumentParser

def main():
    parser = ArgumentParser()
    parser.add_argument("indent", type=int, help="indent for report")
    parser.add_argument("input_file", help="read data from this file")      ⇽---  ❶
    parser.add_argument("-f", "--file", dest="filename",     ⇽---  ❷
                  help="write report to FILE", metavar="FILE")
    parser.add_argument("-x", "--xray", 
                  help="specify xray strength factor")
    parser.add_argument("-q", "--quiet",
                  action="store_false", dest="verbose", default=True,       ⇽---  ❸
                  help="don't print status messages to stdout")

    args = parser.parse_args()

    print("arguments:", args)
main()

上述代码首先创建了一个ArgumentParser的实例,然后添加了两个位置参数indent和input_file,这是在全部可选参数都解析完毕后输入的参数。位置参数是指那些没有前缀字符(通常是“-”)且必须给定的参数。在上述情况中,参数indent必须是能被解析为int类型的❶。

接下来的一行,添加了一个可选的文件名参数,可以是"-f"或"--file"❷。最后一个可选参数"quiet"添加完之后,也就增加了关闭verbose选项的功能,默认值为True(action="store_false")。这些参数以前缀字符“-”开头,这就告知了解析器其为可选项。

最后一个参数"-q"也带有默认值(这里为True),当未给出本参数时将设为默认值。参数action="store_false"则表示,如果给出了本参数,则会将False值存入目标变量中❸。

模块argparse将返回一个Namespace对象,其属性就包含了上面这些参数。可以用句点符号“.”获取这些参数的值。如果某个选项没有给出实参,则其值为None。

因此,假如用以下命令调用上述脚本:

python opts.py -x100 -q -f outfile 2 arg2      ⇽---  选项跟在脚本文件名的后面给出

则输出结果将如下所示:

arguments: Namespace(filename='outfile', indent=2, input_file='arg2', 
    verbose=False, xray='100')

如果发现有非法的实参,或者必须提供的实参没有给出,parse_args将会引发错误。

python opts.py -x100 –r

运行以上命令行将会看到以下反馈信息:

usage: opts.py [-h] [-f FILE] [-x XRAY] [-q] indent input_file
opts.py: error: the following arguments are required: indent, input_file

5. fileinput模块的使用

有时候fileinput模块对脚本还挺有用的,它能处理来自一个或多个文件的输入。它会自动读取命令行参数(由sys.argv)并将其视为输入文件的列表。然后可按顺序读取数据行。下面代码给出的是一个简单的示例脚本,演示了该模块的基本用法,作用是剔除所有以“##”开头的行。

script4.py文件:

import fileinput
def main():    
    for line in fileinput.input():
        if not line.startswith('##'):
            print(line, end="")
main()

现假定有两个数据文件。

sole1.tst文件:

## sole1.tst: test data for the sole function
0 0 0
0 100 0
##
0 100 100

sole2.tst文件:

## sole2.tst: more test data for the sole function
12 15 0
##
100 100 0

再假定脚本调用命令如下:

python script4.py sole1.tst sole2.tst

结果将会把这两个文件的数据拼接起来,并剔除所有的注释行,如下所示:

0 0 0
0 100 0
0 100 100
12 15 0
100 100 0

如果未给出命令行参数,所有数据都会从标准输入读取。如果有参数为“-”,则此参数处的数据会从标准输入读取。

该模块还提供了很多其他函数,可随时了解已读取的总行数(lineno)、已从当前文件读取的行数(filelineno)、当前文件名(filename)、当前行是否为文件的首行(isfirstline)、是否正从标准输入读取(isstdin)。还可随时跳到下一个文件(nextfile)或关闭整个文件流(close)。

下面代码演示这些函数的用法,目标是把输入文件中的文本行拼接起来,并加上文件开始分界符。

script5.py文件:

import fileinput
def main(): 
    for line in fileinput.input():
        if fileinput.isfirstline():
            print("<start of file {0}>".format(fileinput.filename()))
        print(line, end="")
main()

调用:

python script5.py file1 file2

将会得到以下结果,省略号表示输入文件中的内容:

<start of file file1>
.......................
.......................
<start of file file2>
.......................
.......................

最后,如果调用fileinput.input时带了一个文件名或文件名列表作为参数,这些文件就会被用作输入文件,而不再采用sys.argv中的参数。fileinput.input还有一个可选参数inplace,可将输出结果存回输入文件中,同时将原始文件保留为备份文件。

2、让脚本在UNIX下直接运行

如果是在UNIX下运行,让Python脚本直接可运行是很简单的。请在脚本文件的第一行加入以下命令,并将文件属性修改正确(即chmod +x replace.py):

#! /usr/bin/env python

注意,如果默认不是Python 3.x版本,则可能需要把上面这行代码中的python改为python3、python3.6之类,以指定运行Python 3.x而不是默认的旧版本。

如果脚本位于当前可执行路径(如bin目录中),那就不用考虑当前目录了,直接键入文件名和所需参数即可执行:

replace.py zero 0 <infile> outfile

UNIX天然具备输入/输出的重定向能力。如果用的是新式的shell,则命令行历史记录和自动完成功能也会一应俱全。

如果是在UNIX下编写系统管理脚本,有几个可用的库模块可能会比较有用,包括访问用户组数据库的grp、访问密码数据库的pwd、访问资源使用情况的resource、使用syslog工具的syslog和处理由os.stat调用获取的文件或目录信息的stat。

3、macOS系统中的脚本

Python脚本在macOS系统中的行为,很多方面都与Linux/UNIX相同。与所有UNIX设备完全一样,Python脚本可以在macOS的终端窗口中运行。但在Mac上,还可以由Finder运行Python程序,通过将脚本文件拖入Python Launcher应用程序或将Python Launcher配置为打开脚本文件(或所有扩展名为.py的文件)的默认应用程序。

如果对编写macOS系统管理脚本感兴趣,那就该看看用于融合Apple的Open Scripting Architectures(OSA)和Python的包,如appscript和PyOSA。

4、Windows中多种脚本执行方式

如果是在Windows系统中,启动脚本的方式会有好多种,功能和易用性各不相同。遗憾的是,针对目前在用的多个Windows版本,这些执行方式及其配置都存在很大的差别。

这里重点介绍在命令提示符或PowerShell中运行Python。

1. 从命令窗口或PowerShell中启动脚本

要在命令窗口或PowerShell窗口中运行Python脚本,请先打开一个命令提示符或PowerShell窗口。在命令提示符下,进入脚本文件所在的目录,就能以Python命令方式运行脚本,这与UNIX/Linux/macOS系统几乎相同:

> python replace.py zero 0 < infile> outfile

Python无法运行?

如果在Windows命令提示符中输入python,但是Python没有运行起来,那么可能意味着Python可执行文件所在目录没有包含在命令执行路径中。这时需要手动将Python可执行文件路径添加到系统的PATH环境变量中,或者重新运行安装程序也能完成同样操作。

以上是在Windows系统中最灵活的一种Python脚本运行方式,因为可以使用输入/输出重定向功能。

2. Windows中的其他运行方式

还可试试其他的运行方式。如果对批处理文件的编写比较熟悉,就可以将运行命令放入其中。Cygwin实用工具集中自带一个GNU BASH shell,为Windows系统提供了类似UNIX的shell功能,相关信息可以在Cygwin官网获取。

在Windows系统中,可以编辑环境变量,将.py添加为可直接运行的扩展名,这样脚本就可自动执行了:

PATHEXT=.COM;.EXE;.BAT;.CMD;.VBS;.JS;.PY

5、程序和模块

对于只包含几行代码的短脚本,只用一个函数就能正常运行了。但如果代码量超过了这个大小,那么将主控函数和其余代码拆分开,就是个好的选择。

就从一个简单的主控函数示例开始吧,下面代码所示的脚本将返回给定的0到99之间数字的英文说法。

script6.py文件:

#! /usr/bin/env python3
import sys
# 转换关系的映射
_1to9dict = {'0': '', '1': 'one', '2': 'two', '3': 'three', '4': 'four',
             '5': 'five', '6': 'six', '7': 'seven', '8': 'eight',
             '9': 'nine'}
_10to19dict = {'0': 'ten', '1': 'eleven', '2': 'twelve',
               '3': 'thirteen', '4': 'fourteen', '5': 'fifteen',
               '6': 'sixteen', '7': 'seventeen', '8': 'eighteen', 
               '9': 'nineteen'}
_20to90dict = {'2': 'twenty', '3': 'thirty', '4': 'forty', '5': 'fifty',
               '6': 'sixty', '7': 'seventy', '8': 'eighty', '9': 'ninety'}
def num2words(num_string):
    if num_string == '0': 
        return'zero'
    if len(num_string) > 2:  
        return "Sorry can only handle 1 or 2 digit numbers"
    num_string = '0' + num_string     ⇽---  如果是一位数则在左侧补0
    tens, ones = num_string[-2], num_string[-1]
    if tens == '0': 
        return _1to9dict[ones]
    elif tens == '1': 
        return _10to19dict[ones]
    else: 
        return _20to90dict[tens] + ' ' + _1to9dict[ones]
def main():
    print(num2words(sys.argv[1]))      ⇽---  ❶
main()

如果用以下命令调用:

python script6.py 59

就会得到以下结果:

fifty nine

这里的主控函数会用适当的实参调用函数num2words,并打印出结果❶。标准的做法是把调用主控函数的代码放在脚本文件的末尾,但有时会在文件开头看到主控函数的定义。推荐把主控函数的定义放在末尾,紧挨着其调用代码的上面,这样就不必在最后找到名字后再往回翻找其定义了。这种做法还将脚本部分(scripting plumbing)与文件的其余部分清楚地分隔开来,当需要将脚本和模块放在一起时就会非常有用。

如果要让脚本中已建好的函数能被其他模块或脚本调用,可以将脚本代码和模块结合在一起。此外,模块还可以装备起来作为脚本运行,既能为用户提供快速的函数访问接口,又能为自动化模块测试提供钩子(hook)。

将脚本代码和模块结合在一起是很简单的,也就是在主控函数之外加上以下条件测试:

if __name__ == '__main__':
    main()
else:
    # 本模块的初始化代码

如果被作为脚本调用,运行时的当前模块名称将会是__main__,于是会调用主控函数main。如果条件测试已导入交互式会话或其他模块中,则模块名称将会是其文件名。

在创建脚本时,常常一开始先设定为模块。这样就能将其导入交互会话中,以便在函数创建时就能进行交互式测试和调试。只有主控函数需要在模块之外进行调试。如果脚本代码逐渐增加,或者某些自编函数可能需要供其他地方调用,就可以将函数分离出来自成模块,以供其他模块导入。

下面代码中的脚本是在上一个脚本基础上的扩展,但是修改为模块的用法。在功能上也做了扩展,允许输入0到999999999999999之间的数字,而不仅是从0到99。主控函数main会对参数的有效性进行验证,同时还会去除参数中的逗号。这样就允许输入可读性更好的数字,如1 234 567。

n2w.py文件:

#! /usr/bin/env python3
"""n2w: number to words conversion module: contains function
    num2words. Can also be run as a script
usage as a script: n2w num          ⇽---  给出本模块的用法及例子
           (Convert a number to its English word description)
           num: whole integer from 0 and 999,999,999,999,999 (commas are
           optional)
example: n2w 10,003,103
           for 10,003,103 say: ten million three thousand one hundred three
"""
import sys, string, argparse
_1to9dict = {'0': '', '1': 'one', '2': 'two', '3': 'three', '4': 'four',   ⇽---  转换关系的映射
             '5': 'five', '6': 'six', '7': 'seven', '8': 'eight',
             '9': 'nine'}   
_10to19dict = {'0': 'ten', '1': 'eleven', '2': 'twelve',
               '3': 'thirteen', '4': 'fourteen', '5': 'fifteen',
               '6': 'sixteen', '7': 'seventeen', '8': 'eighteen', 
               '9': 'nineteen'}
_20to90dict = {'2': 'twenty', '3': 'thirty', '4': 'forty', '5': 'fifty',
               '6': 'sixty', '7': 'seventy', '8': 'eighty', '9': 'ninety'}
_magnitude_list = [(0, ''), (3, ' thousand '), (6, ' million '),
                   (9, ' billion '), (12, ' trillion '),(15, '')]
def num2words(num_string):
    """num2words(num_string): convert number to English words"""
    if num_string == '0':      ⇽---  处理特殊情况(数字为0或过大)
        return 'zero'
    num_string = num_string.replace(",", "")      ⇽---  从数字中去除逗号
    num_length = len(num_string)
    max_digits = _magnitude_list[-1][0]
    if num_length > max_digits:
         return "Sorry, can't handle numbers with more than " \
                "{0} digits".format(max_digits)
    num_string = '00' + num_string       ⇽---  填充左边的数字
    word_string = ''       ⇽---  初始化数字字符串
    for mag, name in _magnitude_list:         
        if mag >= num_length:
            return word_string
        else:
            hundreds, tens, ones = num_string[-mag-3], \
                 num_string[-mag-2], num_string[-mag-1]
            if not (hundreds == tens == ones == '0'):
                word_string = _handle1to999(hundreds, tens, ones) + \
                                            name + word_string         创建数字字符串
def _handle1to999(hundreds, tens, ones):
    if hundreds == '0':
        return _handle1to99(tens, ones)
    else:
        return _1to9dict[hundreds] + ' hundred ' + _handle1to99(tens, ones)
def _handle1to99(tens, ones):
    if tens == '0': 
        return _1to9dict[ones]
    elif tens == '1':
        return _10to19dict[ones]
    else:
        return _20to90dict[tens] + ' ' + _1to9dict[ones]
def test():      ⇽---  模块测试模式下用到的函数
    values = sys.stdin.read().split()
    for val in values:
        print("{0} = {1}".format(val, num2words(val)))
def main():
    parser = argparse.ArgumentParser(usage=__doc__)
    parser.add_argument("num", nargs=’*’)      ⇽---  将所有实参值加入列表
    parser.add_argument("-t", "--test", dest="test",
                      action='store_true', default=False,
                      help="Test mode: reads from stdin")
    args = parser.parse_args()
    if args.test:              ⇽---  如果设置了test变量,说明运行在测试模式
        test()
    else:
        try:
            result = num2words(args.num[0]) 
        except KeyError:       ⇽---  捕获非数字的参数导致的KeyError 
            parser.error('argument contains non-digits')
        else: 
            print("For {0}, say: {1}".format(args.num[0], result))
if __name__ == '__main__':
    main()     ⇽---  ❶
else: 
    print("n2w  loaded as a module")

如果作为脚本调用,运行时的当前模块名称将会是__main__。如果作为模块导入,则模块名称将会是n2w❶。main函数演示了主控函数在命令行脚本中的作用,实际上是建立了一个简单的用户交互界面。

它可以处理以下工作:

  • 确保命令行参数数量、类型都正确。如果参数不对,则向用户给出用法信息。以上函数保证必须得有一个参数,但没有对参数只能包含数字进行显式检测。
  • 能够处理特殊运行模式。以上例子中,带上'--test'参数就能进入测试模式。
  • 将命令行参数映射为函数的参数,并以适当的方式发起调用。以上例子中,在剔除了逗号后调用了函数num2words。
  • 能够捕获异常并以更为友好的方式显示信息。以上例子中,将会捕获KeyErrors,如果参数中包含非数字字符,就会引发该异常。[1]
  • 必要时可将输出映射为更友好的形式,当然以上例子只是用了print语句。如果在Windows系统中运行,可能会想用鼠标双击打开脚本,也就是用input来给出参数提示,而不是以命令行参数的形式获取,让输出信息一直显示在屏幕上,直至用以下代码结束脚本的运行:
input("Press the Enter key to exit")

但测试模式可能还是保留以命令行参数方式为好。

下面代码给出的测试模式,能对以上模块及其num2words函数提供回归(regression)测试支持。

n2w.tst文件:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 98 99 100
101 102 900 901 999 
999,999,999,999,999
1,000,000,000,000,000

在这个例子中,使用时先在文件中写入一些数字,然后输入命令:

python n2w.py --test < n2w.tst > n2w.txt

从输出文件中就很容易检查结果的正确性。以上测试用例会被运行多次,并且无论何时num2words或其他需要调用的函数发生了改动,都能再次使用。当然,这里确实没有进行完全测试。必须承认,该程序对大于999万亿的输入数字,确实没有检查有效性!

为模块提供测试模式的支持,往往是模块内部脚本代码的唯一用途。据我所知,至少有一家公司有此规定,必须为每个已开发的Python模块创建一个测试用的函数。Python内置的数据对象类型和方法,通常能让这一过程变得比较简单。采用了这种技术的人,似乎一致相信这种做法是值得的。

另一种编码方案是创建一个单独的文件并导入n2w模块,文件中只包含了main函数的参数处理部分。这样在n2w.py的main函数中,就只留下供测试模式调用的代码了。

6、发布Python应用程序

Python脚本和应用程序的发布方式可以有很多种。当然,可以把源代码文件分享出去,或许可以打包在zip或tar文件中。假设应用程序是可移植的,也可以只发布字节码.pyc文件。不过,这两种方式通常都有很多不足之处。

1. wheel包

目前,打包和发布Python模块和应用程序的标准方法,是使用名为wheel的包。wheel旨在让Python代码的安装更加可靠,并能帮助管理代码依赖的包。

2. zipapp和pex

如果应用程序分散在多个模块中,则还可以发布为可执行的zip文件。这种格式有赖于Python的两个特点。

第一,如果zip文件中包含名为__main__.py的文件,Python就可以用该文件作为归档文件的入口点,并直接执行__main__.py文件。此外,zip文件内容还会被添加到sys.path中,以供__main__.py导入和执行。

第二,zip文件允许在归档文件开头添加任意内容。如果添加一条可供Python解释器读取的shebang行,如#!/usr/bin/env python3,并赋予该zip文件必要的权限,则其就能成为一个独立可执行的文件。

实际上,手动创建可执行的zipapp并不困难。先创建一个包含__main__.py的zip文件,并将shebang行添加到zip文件开头,并设置为可执行权限。

从Python 3.5开始,zipapp模块就已被包含在标准库中了,标准库可以从命令行或用库API创建zipapp。

还有一个更强大的工具pex,并未包含在标准库中,但可以通过pip从包索引中获取。pex不仅能完成同样的基础性打包工作,还提供了更多特性和配置选项。如果需要,在Python 2.7中已经提供了pex。无论采用哪种方式,对于准备运行的多文件Python应用程序来说,zipapp都是将其打包和发布的便捷途径。

3. py2exe和py2app

值得一提的是py2exe能够制作独立的Windows程序,而py2app则在macOS平台完成相同功能。这里的“独立”是指,可以在没有安装Python的机器上运行的单个可执行文件。独立的可执行文件在很多方面并不够理想,往往比原生Python应用程序体积更大,也更不灵活,但在某些情况下,它是最好的解决方案,有时还是唯一方案。

4. 用freeze创建可执行程序

利用freeze工具,也可以创建可执行的Python程序,使其能在没有安装Python环境的机器上运行。在Python源代码目录的Tools子目录中,有个freeze目录,其中的Readme文件给出了使用说明。如果打算使用freeze,可能还需要下载Python源代码。

在封装(freeze)Python程序的过程中,将会创建C程序文件,然后会用C编译器对其进行编译和链接,因此需要在系统中装有C编译器。封装好的应用程序,将只能在支持封装C编译器的平台上运行。

其他还有一些工具,也想方设法要把Python解释器/环境和应用程序进行转换,打包成一个独立运行的应用程序。但一般情况下,由于这种方式复杂且仍有难度,也许还是不用为好。除非是需求真的很强烈,而且有充足的时间和资源来保证程序能够正常工作。

pycharm+python3.6下载地址:

链接:百度网盘 请输入提取码

提取码:cmvb 

Guess you like

Origin blog.csdn.net/qq_35029061/article/details/130027833