简介
在这个实验中,你将学习探索 Python 函数和方法的基本方面。你还将通过有效地设计参数,让函数更加灵活。
此外,你将实现类型提示(type hints),以提高代码的可读性和安全性,这对于编写高质量的 Python 代码至关重要。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
在这个实验中,你将学习探索 Python 函数和方法的基本方面。你还将通过有效地设计参数,让函数更加灵活。
此外,你将实现类型提示(type hints),以提高代码的可读性和安全性,这对于编写高质量的 Python 代码至关重要。
在之前的练习中,你可能遇到过读取 CSV 文件并将数据存储在各种数据结构中的代码。这段代码的目的是从 CSV 文件中获取原始文本数据,并将其转换为更有用的 Python 对象,如字典或类实例。这种转换至关重要,因为它使我们能够在 Python 程序中以更结构化、更有意义的方式处理数据。
读取 CSV 文件的典型模式通常遵循特定的结构。以下是一个读取 CSV 文件并将每行转换为字典的函数示例:
import csv
def read_csv_as_dicts(filename, types):
records = []
with open(filename) as file:
rows = csv.reader(file)
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
让我们来详细分析这个函数的工作原理。首先,它导入了 csv
模块,该模块为 Python 处理 CSV 文件提供了功能。该函数接受两个参数:filename
,即要读取的 CSV 文件的名称;types
,这是一个函数列表,用于将每列的数据转换为适当的数据类型。
在函数内部,它初始化了一个名为 records
的空列表,用于存储表示 CSV 文件每行的字典。然后,它使用 with
语句打开文件,这确保了在代码块执行完毕后文件会被正确关闭。csv.reader
函数用于创建一个迭代器,用于读取 CSV 文件的每一行。第一行被假定为表头,因此使用 next
函数获取。
接下来,函数遍历 CSV 文件中剩余的行。对于每一行,它使用字典推导式创建一个字典。字典的键是列名,值是将 types
列表中相应的类型转换函数应用于该行中的值的结果。最后,将该字典添加到 records
列表中,函数返回字典列表。
现在,让我们看一个类似的函数,它将 CSV 文件中的数据读取为类实例:
def read_csv_as_instances(filename, cls):
records = []
with open(filename) as file:
rows = csv.reader(file)
headers = next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
这个函数与前一个函数类似,但它不是创建字典,而是创建类的实例。该函数接受两个参数:filename
,即要读取的 CSV 文件的名称;cls
,即要创建其实例的类。
在函数内部,它遵循与前一个函数类似的结构。它初始化一个名为 records
的空列表,用于存储类实例。然后,它打开文件,读取表头,并遍历剩余的行。对于每一行,它调用类 cls
的 from_row
方法,使用该行的数据创建类的一个实例。然后将该实例添加到 records
列表中,函数返回实例列表。
在这个实验中,我们将重构这些函数,使其更加灵活和健壮。我们还将探索 Python 的类型提示(type hinting)系统,它允许我们指定函数参数和返回值的预期类型。这可以使我们的代码更具可读性,更易于理解,特别是对于可能使用我们代码的其他开发者来说。
让我们先创建一个 reader.py
文件,并将这些初始函数添加到其中。在进行下一步之前,请确保测试这些函数,以确保它们能正常工作。
让我们先创建一个 reader.py
文件,其中包含两个用于读取 CSV 数据的基本函数。这些函数将帮助我们以不同的方式处理 CSV 文件,例如将数据转换为字典或类实例。
首先,我们需要了解什么是 CSV 文件。CSV 代表逗号分隔值(Comma-Separated Values)。它是一种用于存储表格数据的简单文件格式,其中每行代表一行数据,每行中的值用逗号分隔。
现在,让我们创建 reader.py
文件。按照以下步骤操作:
打开代码编辑器,在 /home/labex/project
目录下创建一个名为 reader.py
的新文件。我们将在这里编写读取 CSV 数据的函数。
将以下代码添加到 reader.py
中:
## reader.py
import csv
def read_csv_as_dicts(filename, types):
'''
Read CSV data into a list of dictionaries with optional type conversion
Args:
filename: Path to the CSV file
types: List of type conversion functions for each column
Returns:
List of dictionaries with data from the CSV file
'''
records = []
with open(filename) as file:
rows = csv.reader(file)
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
def read_csv_as_instances(filename, cls):
'''
Read CSV data into a list of class instances
Args:
filename: Path to the CSV file
cls: Class to create instances from
Returns:
List of class instances with data from the CSV file
'''
records = []
with open(filename) as file:
rows = csv.reader(file)
headers = next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
在 read_csv_as_dicts
函数中,我们首先使用 open
函数打开 CSV 文件。然后,使用 csv.reader
逐行读取文件。next(rows)
语句读取文件的第一行,通常这一行包含表头。之后,我们遍历剩余的行。对于每一行,我们创建一个字典,其中键是表头,值是该行中对应的数值,并可选择使用 types
列表进行类型转换。
read_csv_as_instances
函数类似,但它不是创建字典,而是创建给定类的实例。它假设该类有一个名为 from_row
的静态方法,可以从一行数据创建一个实例。
test_reader.py
的新文件,并添加以下代码:## test_reader.py
import reader
import stock
## Test reading CSV as dictionaries
portfolio_dicts = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("First portfolio item as dictionary:", portfolio_dicts[0])
print("Total items:", len(portfolio_dicts))
## Test reading CSV as class instances
portfolio_instances = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
print("\nFirst portfolio item as Stock instance:", portfolio_instances[0])
print("Total items:", len(portfolio_instances))
在 test_reader.py
文件中,我们导入刚刚创建的 reader
模块和 stock
模块。然后,通过使用一个名为 portfolio.csv
的示例 CSV 文件调用这两个函数来进行测试。我们打印投资组合中的第一个项目和项目总数,以验证函数是否按预期工作。
python test_reader.py
输出应该类似于以下内容:
First portfolio item as dictionary: {'name': 'AA', 'shares': 100, 'price': 32.2}
Total items: 7
First portfolio item as Stock instance: Stock('AA', 100, 32.2)
Total items: 7
这证实了我们的两个函数工作正常。第一个函数将 CSV 数据转换为经过适当类型转换的字典列表,第二个函数使用提供的类的静态方法创建类实例。
下一步,我们将重构这些函数,使其更加灵活,允许它们处理任何可迭代的数据来源,而不仅仅是文件名。
目前,我们的函数仅限于读取由文件名指定的文件,这限制了它们的可用性。在编程中,让函数更具灵活性通常是有益的,这样它们就能处理不同类型的输入。就我们的情况而言,如果我们的函数能够处理任何能产生行的可迭代对象,比如文件对象或其他数据源,那就太好了。这样,我们就能在更多场景中使用这些函数,例如从压缩文件或其他数据流中读取数据。
让我们重构代码以实现这种灵活性:
reader.py
文件。我们将对其进行修改,添加一些新函数。这些新函数将使我们的代码能够处理不同类型的可迭代对象。以下是你需要添加的代码:## reader.py
import csv
def csv_as_dicts(lines, types):
'''
Parse CSV data from an iterable into a list of dictionaries
Args:
lines: An iterable producing CSV lines
types: List of type conversion functions for each column
Returns:
List of dictionaries with data from the CSV lines
'''
records = []
rows = csv.reader(lines)
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
def csv_as_instances(lines, cls):
'''
Parse CSV data from an iterable into a list of class instances
Args:
lines: An iterable producing CSV lines
cls: Class to create instances from
Returns:
List of class instances with data from the CSV lines
'''
records = []
rows = csv.reader(lines)
headers = next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
def read_csv_as_dicts(filename, types):
'''
Read CSV data into a list of dictionaries with optional type conversion
Args:
filename: Path to the CSV file
types: List of type conversion functions for each column
Returns:
List of dictionaries with data from the CSV file
'''
with open(filename) as file:
return csv_as_dicts(file, types)
def read_csv_as_instances(filename, cls):
'''
Read CSV data into a list of class instances
Args:
filename: Path to the CSV file
cls: Class to create instances from
Returns:
List of class instances with data from the CSV file
'''
with open(filename) as file:
return csv_as_instances(file, cls)
让我们仔细看看我们是如何重构代码的:
csv_as_dicts()
和 csv_as_instances()
。这些函数旨在处理任何能产生 CSV 行的可迭代对象。这意味着它们可以处理不同类型的输入源,而不仅仅是由文件名指定的文件。read_csv_as_dicts()
和 read_csv_as_instances()
函数,以使用这些新函数。这样,通过文件名从文件中读取数据的原始功能仍然可用,但现在它是基于更灵活的函数构建的。test_reader_flexibility.py
的文件,并添加以下代码。这段代码将使用不同类型的输入源来测试新函数:## test_reader_flexibility.py
import reader
import stock
import gzip
## Test opening a regular file
with open('portfolio.csv') as file:
portfolio = reader.csv_as_dicts(file, [str, int, float])
print("First item from open file:", portfolio[0])
## Test opening a gzipped file
with gzip.open('portfolio.csv.gz', 'rt') as file: ## 'rt' means read text
portfolio = reader.csv_as_instances(file, stock.Stock)
print("\nFirst item from gzipped file:", portfolio[0])
## Test backward compatibility
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("\nFirst item using backward compatible function:", portfolio[0])
test_reader_flexibility.py
文件所在的目录。然后运行以下命令:python test_reader_flexibility.py
输出应该类似于以下内容:
First item from open file: {'name': 'AA', 'shares': 100, 'price': 32.2}
First item from gzipped file: Stock('AA', 100, 32.2)
First item using backward compatible function: {'name': 'AA', 'shares': 100, 'price': 32.2}
这个输出证实了我们的函数现在可以处理不同类型的输入源,同时保持了向后兼容性。重构后的函数可以处理来自以下数据源的数据:
open()
打开的普通文件gzip.open()
打开的压缩文件这使得我们的代码更加灵活,并且更容易在不同的场景中使用。
在数据处理领域,并非所有 CSV 文件的第一行都包含表头。表头是 CSV 文件中为每列赋予的名称,它能帮助我们了解每列所包含的数据类型。当 CSV 文件没有表头时,我们需要一种方法来妥善处理它。在本节中,我们将修改函数,允许调用者手动提供表头,这样我们就可以处理有表头和没有表头的 CSV 文件。
reader.py
文件,并更新它以包含表头处理功能:## reader.py
import csv
def csv_as_dicts(lines, types, headers=None):
'''
Parse CSV data from an iterable into a list of dictionaries
Args:
lines: An iterable producing CSV lines
types: List of type conversion functions for each column
headers: Optional list of column names. If None, first row is used as headers
Returns:
List of dictionaries with data from the CSV lines
'''
records = []
rows = csv.reader(lines)
if headers is None:
## Use the first row as headers if none provided
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
def csv_as_instances(lines, cls, headers=None):
'''
Parse CSV data from an iterable into a list of class instances
Args:
lines: An iterable producing CSV lines
cls: Class to create instances from
headers: Optional list of column names. If None, first row is used as headers
Returns:
List of class instances with data from the CSV lines
'''
records = []
rows = csv.reader(lines)
if headers is None:
## Skip the first row if no headers provided
next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
def read_csv_as_dicts(filename, types, headers=None):
'''
Read CSV data into a list of dictionaries with optional type conversion
Args:
filename: Path to the CSV file
types: List of type conversion functions for each column
headers: Optional list of column names. If None, first row is used as headers
Returns:
List of dictionaries with data from the CSV file
'''
with open(filename) as file:
return csv_as_dicts(file, types, headers)
def read_csv_as_instances(filename, cls, headers=None):
'''
Read CSV data into a list of class instances
Args:
filename: Path to the CSV file
cls: Class to create instances from
headers: Optional list of column names. If None, first row is used as headers
Returns:
List of class instances with data from the CSV file
'''
with open(filename) as file:
return csv_as_instances(file, cls, headers)
让我们来理解这些函数所做的关键更改:
我们为所有函数添加了一个 headers
参数,并将其默认值设置为 None
。这意味着如果调用者没有提供任何表头,函数将使用默认行为。
在 csv_as_dicts
函数中,只有当 headers
参数为 None
时,我们才将第一行用作表头。这使我们能够自动处理有表头的文件。
在 csv_as_instances
函数中,只有当 headers
参数为 None
时,我们才跳过第一行。这是因为如果我们提供了自己的表头,文件的第一行就是实际数据,而不是表头。
让我们使用没有表头的文件来测试这些修改。创建一个名为 test_headers.py
的文件:
## test_headers.py
import reader
import stock
## Define column names for the file without headers
column_names = ['name', 'shares', 'price']
## Test reading a file without headers
portfolio = reader.read_csv_as_dicts('portfolio_noheader.csv',
[str, int, float],
headers=column_names)
print("First item from file without headers:", portfolio[0])
print("Total items:", len(portfolio))
## Test reading the same file as instances
portfolio = reader.read_csv_as_instances('portfolio_noheader.csv',
stock.Stock,
headers=column_names)
print("\nFirst item as Stock instance:", portfolio[0])
print("Total items:", len(portfolio))
## Verify that original functionality still works
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("\nFirst item from file with headers:", portfolio[0])
在这个测试脚本中,我们首先为没有表头的文件定义列名。然后,我们测试将没有表头的文件读取为字典列表和类实例列表。最后,我们通过读取有表头的文件来验证原始功能仍然有效。
python test_headers.py
输出应该类似于:
First item from file without headers: {'name': 'AA', 'shares': 100, 'price': 32.2}
Total items: 7
First item as Stock instance: Stock('AA', 100, 32.2)
Total items: 7
First item from file with headers: {'name': 'AA', 'shares': 100, 'price': 32.2}
这个输出证实了我们的函数现在可以处理有表头和没有表头的 CSV 文件。用户可以在需要时提供列名,或者依靠从第一行读取表头的默认行为。
通过进行此修改,我们的 CSV 读取函数现在更加通用,可以处理更广泛的文件格式。这是使我们的代码在不同场景中更加健壮和有用的重要一步。
在 Python 3.5 及更高版本中,支持类型提示。类型提示是一种在代码中指明变量、函数参数和返回值预期数据类型的方式。它们不会改变代码的运行方式,但能让代码更易读,还能在代码实际运行前帮助捕获某些类型的错误。现在,让我们为 CSV 读取函数添加类型提示。
reader.py
文件,并更新它以包含类型提示:## reader.py
import csv
from typing import List, Callable, Dict, Any, Type, Optional, TextIO, Iterator, TypeVar
## Define a generic type for the class parameter
T = TypeVar('T')
def csv_as_dicts(lines: Iterator[str],
types: List[Callable[[str], Any]],
headers: Optional[List[str]] = None) -> List[Dict[str, Any]]:
'''
Parse CSV data from an iterable into a list of dictionaries
Args:
lines: An iterable producing CSV lines
types: List of type conversion functions for each column
headers: Optional list of column names. If None, first row is used as headers
Returns:
List of dictionaries with data from the CSV lines
'''
records: List[Dict[str, Any]] = []
rows = csv.reader(lines)
if headers is None:
## Use the first row as headers if none provided
headers = next(rows)
for row in rows:
record = { name: func(val)
for name, func, val in zip(headers, types, row) }
records.append(record)
return records
def csv_as_instances(lines: Iterator[str],
cls: Type[T],
headers: Optional[List[str]] = None) -> List[T]:
'''
Parse CSV data from an iterable into a list of class instances
Args:
lines: An iterable producing CSV lines
cls: Class to create instances from
headers: Optional list of column names. If None, first row is used as headers
Returns:
List of class instances with data from the CSV lines
'''
records: List[T] = []
rows = csv.reader(lines)
if headers is None:
## Skip the first row if no headers provided
next(rows)
for row in rows:
record = cls.from_row(row)
records.append(record)
return records
def read_csv_as_dicts(filename: str,
types: List[Callable[[str], Any]],
headers: Optional[List[str]] = None) -> List[Dict[str, Any]]:
'''
Read CSV data into a list of dictionaries with optional type conversion
Args:
filename: Path to the CSV file
types: List of type conversion functions for each column
headers: Optional list of column names. If None, first row is used as headers
Returns:
List of dictionaries with data from the CSV file
'''
with open(filename) as file:
return csv_as_dicts(file, types, headers)
def read_csv_as_instances(filename: str,
cls: Type[T],
headers: Optional[List[str]] = None) -> List[T]:
'''
Read CSV data into a list of class instances
Args:
filename: Path to the CSV file
cls: Class to create instances from
headers: Optional list of column names. If None, first row is used as headers
Returns:
List of class instances with data from the CSV file
'''
with open(filename) as file:
return csv_as_instances(file, cls, headers)
让我们来理解代码中所做的关键更改:
我们从 typing
模块导入了类型。这个模块提供了一组可用于定义类型提示的类型。例如,List
、Dict
和 Optional
就是这个模块中的类型。
我们添加了一个泛型类型变量 T
来表示类的类型。泛型类型变量允许我们编写能以类型安全的方式处理不同类型的函数。
我们为所有函数参数和返回值添加了类型提示。这使得函数期望的参数类型以及返回值的类型一目了然。
我们使用了合适的容器类型,如 List
、Dict
和 Optional
。List
表示列表,Dict
表示字典,Optional
表示参数可以是某种类型,也可以是 None
。
我们对类型转换函数使用了 Callable
。Callable
用于表明参数是一个可调用的函数。
我们使用泛型 T
来表示 csv_as_instances
返回传入类的实例列表。这有助于 IDE 和其他工具理解返回对象的类型。
现在,让我们创建一个简单的测试文件,以确保一切仍然正常工作:
## test_types.py
import reader
import stock
## The functions should work exactly as before
portfolio = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("First item:", portfolio[0])
## But now we have better type checking and IDE support
stock_portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
print("\nFirst stock:", stock_portfolio[0])
## We can see that stock_portfolio is a list of Stock objects
## This helps IDEs provide better code completion
first_stock = stock_portfolio[0]
print(f"\nName: {first_stock.name}")
print(f"Shares: {first_stock.shares}")
print(f"Price: {first_stock.price}")
print(f"Value: {first_stock.shares * first_stock.price}")
python test_types.py
输出应该类似于:
First item: {'name': 'AA', 'shares': 100, 'price': 32.2}
First stock: Stock('AA', 100, 32.2)
Name: AA
Shares: 100
Price: 32.2
Value: 3220.0
类型提示不会改变代码的运行方式,但它们有以下几个好处:
在大型代码库中,这些好处可以显著减少 bug,并使代码更易于理解和维护。
注意: 类型提示在 Python 中是可选的,但在专业代码中越来越常用。Python 标准库中的库以及许多流行的第三方包现在都包含大量的类型提示。
在本次实验中,你学习了 Python 函数设计的几个关键方面。首先,你学习了基本的函数设计,具体来说,就是如何编写函数将 CSV 数据处理成各种数据结构。你还通过重构函数,使其能处理任何可迭代源,探索了函数的灵活性,从而增强了代码的通用性和可复用性。
此外,你掌握了添加可选参数以处理不同用例的方法,例如处理有表头或无表头的 CSV 文件,还学会了使用 Python 的类型提示系统来提高代码的可读性和可维护性。这些技能对于编写健壮的 Python 代码至关重要,随着你的程序变得更加复杂,这些设计原则将使你的代码保持条理清晰且易于理解。这些技术不仅适用于 CSV 处理,在你的 Python 编程工具包中也具有很高的价值。