# 文件对象

# 打开文件

Python 中可以调用 open() 函数来打开文件,返回一个文件对象。函数定义如下:

open(
      file,               # 指定要打开的文件路径,或者一个 int 型的文件描述符
      mode      = 'r',    # 打开文件的模式
      buffering = -1,     # 缓冲策略
      encoding  = None,   # 编码格式,仅在文本模式可用
      errors    = None,   # 处理编码、解码错误的策略,仅在文本模式可用。取值为 None 时相当于 'strict' ,会抛出异常
      newline   = None,   # 换行符,仅在文本模式可用
      closefd   = True,   # 输入文件描述符打开文件时,调用 close() 方法是否关闭该文件描述符
      ...)
  • encoding 参数可以取值为 'utf-8'、'gbk' 等。
    • 如果不指定 encoding 参数,Python 会采用当前平台的默认编码格式,取决于 locale.getpreferredencoding(False)
      • 在 Linux 系统上,它通常是 'utf-8' 。
      • 在 Windows 中文版系统上,它通常是 'cp936' 。
    • 建议总是指定 encoding 参数,有利于跨平台。
  • 处理文件内容的基本流程:
    1. 调用 open() 函数来打开文件,它会返回一个文件对象。
    2. 调用文件对象的方法,比如 read() 读取数据、 write() 写入数据。
    3. 调用文件对象的 close() 方法来关闭文件。
  • 进程每打开一个文件,会自动分配一个 int 型的文件描述符,相当于文件编号。
    • 在 Windows 系统上,如果一个文件已经被某个进程打开了,就不允许该文件被其它进程修改,直到前一个进程释放文件描述符。

# 文件模式

  • open() 函数中,mode 参数可用于指定文件的读写方式,取值如下:

    • r
      • :read ,只读模式,默认采用这种。
      • 如果文件不存在,则会报错:FileNotFoundError: No such file or directory
    • w
      • :write ,只写模式。
      • 如果文件不存在,则自动创建它,再进行写入。
      • 如果文件已存在,则将长度截断为 0 ,再进行写入。这样是完全覆盖式写入。
    • a
      • :append ,追加写模式。
      • 如果文件不存在,则自动创建它,再进行写入。
      • 如果文件已存在,则到文件尾部进行追加写入。
    • x
      • :create ,先创建文件,再以只写模式打开。
      • 如果文件已存在,则会报错:FileExistsError: [Errno 17] File exists
    • +
      • :可读可写模式。
      • 必须与 r、w、a、x 模式其中之一组合使用,比如 r+w+
      • r+ 模式支持部分覆盖式写入,比如通过 seek()+write() 修改文件中的部分内容。
  • open() 函数中,mode 参数也可用于指定文件流的类型,取值如下:

    • b
      • :binary ,二进制模式,读写的值是 bytes 类型。
      • 此时文件对象绑定的 IO 对象为二进制流,又称为字节流。
      • 所有类型的文件都是以二进制格式存储的,因此可以按二进制格式直接读写。不过通常将二进制值转换成字节,便于阅读。
    • t
      • :text ,文本模式,读写的值是 str 类型。
      • 此时文件对象绑定的 IO 对象为文本流,又称为字符流。
      • 与二进制模式相比,文本模式更方便让人阅读,但只适合打开文本格式的文件,不适合打开图片、音频等特殊编码的文件。
        • 读取文件时,需要先读取文件中存储的二进制数据,再按某种编码格式转换成 str 类型。
        • 写入文件时,需要先将写入的 str 类型的值,按某种编码格式转换成 bytes 类型,再写入文件。
    • 打开文件时,t、b 模式只能二选一。
      • 而且必须与一种读写方式组合使用,比如 rtrbrb+
      • 只指定读写方式时,默认采用 t 模式,比如 r 相当于 rt

# 缓冲区

  • 读写文件时,默认启用了缓冲区,但没有缓存。
    • 缓冲(buffer)
      • 原理:读写数据时,等数据累积一定字节数之后,再进行实际的磁盘 IO 操作。
      • 优点:减少了进行磁盘 IO 的次数,减少了 CPU 等待磁盘 IO 的时间。
      • 缺点:读写数据有一定延迟。如果进程突然挂掉,数据就会来不及处理而丢失。
      • 例如:用 write() 方法写入数据到文件时,如果缓冲区没满,就不会立即修改磁盘中的文件。
    • 缓存(cache)
      • 原理:读取数据时,如果操作完数据,并不直接丢弃,而是临时保存一段时间。
      • 优点:方便再次读取该数据。
      • 缺点:会占用一定的缓存空间。
      • 例如:可以创建一个 io.BytesIO() 对象,在内存中缓存数据。
  • open() 函数中,buffering 参数可取值:
    • 0 :取消缓冲,仅在二进制模式可用。
    • 1 :采用行缓冲,仅在文本模式可用。
    • n > 0 :采用 n 字节大小的缓冲区。
    • 默认不指定缓冲策略,如果文件为 tty ,则采用行缓冲。其它文件采用系统默认大小的缓冲区,取决于 io.DEFAULT_BUFFER_SIZE ,通常为 8 KB 。
  • 调用 flush() 方法会刷新缓冲区,它会将缓冲区中等待写入的数据立即写入、等待读取的数据立即读取。
    • 调用 close() 方法时会自动调用 flush() 方法。

# 换行符

  • 读写文件时,可以声明换行符 newline ,用于自动划分每行文本。

    • 比如类 Unix 系统默认是 \n ,Windows 系统中的换行符默认是 \r\n
  • open() 函数中,newline 参数可取值:

    • 默认为 newline=None :
      • 读取文件时,会将文件中的 '\r'、'\n'、'\r\n' 都识别成换行符,并且统一转换成 '\n' ,再返回。
      • 写入文件时,会将待写文本中的 '\n' 都替换成当前平台的默认换行符(取决于 os.linesep ),再写入文件。
    • 如果指定了 newline='' :
      • 读取文件时,会将文件中的 '\r'、'\n'、'\r\n' 都识别成换行符,然后返回该行文本。
      • 写入文件时,会直接写入。
    • 如果指定了 newline 等于其它值:
      • 读取文件时,只会识别指定的换行符,然后返回该行文本。
      • 写入文件时,会将待写文本中的 '\n' 都替换成指定的换行符,再写入文件。

# 关闭文件

  • 操作完文件之后,应该调用 close() 方法来关闭文件。
    • 关闭文件时,会释放其缓冲区,避免内存泄漏。还会释放文件描述符。
    • 文件被关闭之后,就不能再进行读写,否则会报错:ValueError: I/O operation on closed file
  • 可用关键字 with 打开文件,进行上下文管理。
    • 例:
      with open('1.txt') as f:
          f.read()
      
    • 当 Python 解释器执行完 with 语句块或者抛出异常时,会自动调用 close() 方法完成清理。因此上例相当于:
      try:
          f = open('1.txt')
          f.read()
      finally:
          f.close()
      
    • 关键字 with 支持同时打开多个上下文:
      with open('1.txt') as f1, open('2.txt') as f2:
          pass
      

# 常用属性

  • 文件对象的常用属性:

    >>> f.name                        # 文件名
    '1.txt'
    >>> f.mode                        # 打开文件的模式
    'w'
    >>> f.fileno()                    # 文件描述符
    3
    
  • 以文本模式打开文件时,可以访问以下属性:

    >>> f.encoding
    'cp936'
    >>> f.errors
    'strict'
    >>> f.line_buffering
    False
    >>> f.newlines        # 这里 newlines 为 None
    
    >>> f.buffer
    <_io.BufferedWriter name='1.txt'>
    >>> f.buffer.mode
    'wb'
    
    • 以文本模式打开文件时,实际上是先用二进制模式打开文件,再转换成文本模式。
      • 上例中的 f.buffer 是通过一个 BinaryIO 对象,存储了文件的二进制内容。注意它并不是读写文件时的缓冲区。

# 常用方法

  • 文件对象的常用方法:

    .close()  -> None     # 关闭文件
    .closed() -> bool     # 判断文件是否已关闭
    
    .fileno() -> int      # 返回文件描述符
    .flush()  -> None     # 刷新缓冲区
    .isatty() -> bool     # 判断 IO 对象的输入、输出是否指向终端设备
    
    .readable() -> bool                          # 判断是否可读
    .read(n: int = -1) -> AnyStr                 # 读取 n 个字节的数据,然后返回。默认 n 为 -1 ,即无限制
    .readline(limit: int = -1) -> AnyStr         # 读取一行,最多读取 limit 个字节,然后返回(包括换行符)。默认 limit 为 -1 ,即无限制
    .readlines(hint: int = -1) -> List[AnyStr]   # 读取文件,最多读取 hint 行,然后返回一个包含各行的列表(包括换行符)。默认 hint 为 -1 ,无 f.限制
    
    .writable() -> bool                          # 判断是否可写
    .write(s: AnyStr) -> int                     # 写入 s 的值,然后返回写入的字节数
    .writelines(lines: Sequence[AnyStr]) -> None # 写入多行,需要输入一个包含各行的序列(包括换行符),例如 f.writelines(['Hello\n','world\n'])
    
    • 上述的 AnyStr 类型是指 str 或 bytes 等类型。
      • 以文本模式打开文件时,读写的数据必须为 Str 类型。
      • 以二进制模式打开文件时,读写的数据必须为 bytes 或 bytearray 类型。
    • 读取大文件时,直接调用 read() 会占用太多内存,应该改用 read(1000) 等方式。
  • 读写文件的示例:

    >>> f = open('1.txt', 'w')
    >>> type(f)
    <class '_io.TextIOWrapper'>       # 文件对象是对 IO 对象的封装
    >>> f.read()                      # 读取文件,这里因为不能读取而报错
    io.UnsupportedOperation: not readable
    >>> f.write('Hello')              # 写入文件
    5
    >>> f.close()                     # 关闭文件
    
  • 读取文件的示例:

    >>> f = open('1.txt', 'w+')
    >>> f.write('Hello\n')
    6
    >>> f.seek(0)             # 将文件指针移到文件首部
    0
    >>> f.read()              # 读取文件的全部内容
    'Hello\n'
    >>> f.seek(0)
    0
    >>> f.readline()          # 读取一行
    'Hello\n'
    >>> f.seek(0)
    0
    >>> f.readlines()         # 读取全部行
    ['Hello\n']
    >>> f.seek(0)
    0
    >>> [line for line in f]  # 文件对象支持遍历,每次遍历一行,相当于 f.readline()
    ['Hello\n']
    >>> f.close()
    

# 文件指针

  • 读写文件时,Python 会通过文件指针记录当前处理到文件中的哪个字节了。
  • 使用 r、w 模式打开文件时,指针最初指向第 0 个字节。
    • 每次读、写 n 个字节,Python 就会自动将指针的位置向后移动 n 个字节。
  • 使用 a 模式打开文件时,每次读、写操作之前,都会自动将指针指向最后一个字节。
    • 可以调用 f.seek() ,但每次读、写操作之前依然会将指针指向最后一个字节。
  • 相关方法:
    .tell() -> int                              # 返回文件指针当前的位置
    .seekable() -> bool                         # 判断是否可以移动文件指针的位置,取决于 IO 对象是否支持随机位置访问
    .seek(offset: int, whence: int = 0) -> int  # 改变文件指针的位置,然后返回文件指针当前的位置
    
    • offset :表示指针从 wherece 开始的偏移量,即第几个字节。可以为负。
    • whence :可选参数,表示移动指针的起始位置。可取值:
      • 0 :文件首部,即第 0 个字节处。
      • 1 :指针当前位置,即 f.tell() 的值。
      • 2 :文件尾部,最最后一个字节处。
    • 例:
      >>> f.read()
      'Hello'
      >>> f.read()      # 当文件指针指向文件尾部时,如果继续读取,则返回的内容为空
      ''
      >>> f.tell()
      5
      >>> f.seek(0)
      0
      >>> f.read()
      'Hello'
      

# 截断文件

  • 用 truncate() 方法可以截断文件:
    .truncate(size: int = None) -> int   # 将文件大小调整为 size ,然后返回调整后的大小
    
    • 截断文件时,需要允许写入,即 f.writeable() == True 。
    • 如果不指定 size ,则会采用文件指针当前的位置,即 f.tell() 的值。
      • 如果指定的 size 比文件长度小,则会截断文件,删除文件尾部多余的字节。
      • 如果指定的 size 比文件长度大,则会扩展文件,在文件尾部填充一些 Null 字节。
    • 例:
      >>> f = open('1.txt', 'w+')
      >>> f.write('Hello')
      5
      >>> f.truncate(0)
      0
      >>> f.tell()            # 调用 truncate() 方法时,不会自动移动文件指针
      5
      >>> f.write('Hello')    # 此时文件长度已经截断为 0 ,但指针依然指向第 5 个字节,如果继续写入则会在前面填上 4 个 Null 字节,产生稀疏文件
      5
      >>> f.seek(0)
      0
      >>> f.read()
      '\x00\x00\x00\x00\x00Hello'
      >>> f.close()
      
    • 例:清空文件之后重新写入
      with open('1.txt', 'w+') as f:
          f.read()
          f.seek(0)
          f.truncate()
          f.write('Hello')