Ввод-вывод, файлы, директории

Откроем текстовый файл на чтение (когда второй аргумент не указан, файл открывается именно на чтение).

In [1]:
f=open('text.txt')
f,type(f)
Out[1]:
(<_io.TextIOWrapper name='text.txt' mode='r' encoding='UTF-8'>,
 _io.TextIOWrapper)

Получился объект f одного из файловых типов. Что с ним можно делать? Можно его использовать в for цикле, каждый раз будет возвращаться очередная строка файла (включая '\n' в конце; в конце последней строки текстового файла '\n' может и не быть).

In [2]:
for s in f:
    print(s)
abcd

efgh

ijkl

Теперь файл нужно закрыть.

In [3]:
f.close()

Такой стиль работы с файлом (f=open(...); работа с f; f.close()) на самом деле не рекомендуется. Гораздо правильнее использовать оператор with. Он гарантирует, что файл будет закрыт как в том случае, когда исполнение тела with нормально дошло до конца, так и тогда, когда при этом произошло исключение, и мы покинули тело with аварийно.

В операторе with может использоваться любой объект класса, реализующего методы __enter__ и __exit__. Обычно это объект-файл, возвращаемый функцией open.

In [4]:
with open('text.txt') as f:
    for s in f:
        print(s[:-1])
abcd
efgh
ijkl

Метод f.read(n) читает n символов (когда файл близится к концу и прочитать именно n символов уже невозможно, читает меньше; в самый последний раз он читает 0 символов и возвращает ''). Прочитаем файл по 1 символу.

In [5]:
with open('text.txt') as f:
    while True:
        c=f.read(1)
        if c=='':
            break
        else:
            print(c)
a
b
c
d


e
f
g
h


i
j
k
l


Вызов f.read() без аргумента читает файл целиком (что не очень разумно, если в нём много гигабайт).

In [6]:
with open('text.txt') as f:
    s=f.read()
s
Out[6]:
'abcd\nefgh\nijkl\n'

f.readline() читает очередную строку (хотя проще использовать for s in f:).

In [7]:
with open('text.txt') as f:
    while True:
        s=f.readline()
        if s=='':
            break
        else:
            print(s)
abcd

efgh

ijkl

Метод f.readlines() возвращает список строк (опять же его лучше не применять для очень больших файлов).

In [8]:
with open('text.txt') as f:
    l=f.readlines()
l
Out[8]:
['abcd\n', 'efgh\n', 'ijkl\n']

Теперь посмотрим, чем же оператор with лучше, чем пара open - close.

In [9]:
def a(name):
    global f
    f=open(name)
    s=f.readline()
    n=1/0
    f.close()
    return s
In [10]:
a('text.txt')
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-10-d62372657d26> in <module>()
----> 1 a('text.txt')

<ipython-input-9-7f445757684d> in a(name)
      3     f=open(name)
      4     s=f.readline()
----> 5     n=1/0
      6     f.close()
      7     return s

ZeroDivisionError: division by zero
In [11]:
f.closed
Out[11]:
False
In [12]:
f.close()

Произошло исключение, мы покинули функцию до строчки close, и файл не закрылся.

In [13]:
def a(name):
    global f
    with open(name) as f:
        s=f.readline()
        n=1/0
    return s
In [14]:
a('text.txt')
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-14-d62372657d26> in <module>()
----> 1 a('text.txt')

<ipython-input-13-cabd1416e96c> in a(name)
      3     with open(name) as f:
      4         s=f.readline()
----> 5         n=1/0
      6     return s

ZeroDivisionError: division by zero
In [15]:
f.closed
Out[15]:
True

Теперь всё в порядке.

Чтобы открыть файл на запись, нужно включить второй аргумент 'w'.

In [16]:
f=open('newtext.txt','w')
In [17]:
f.write('aaa\n')
Out[17]:
4
In [18]:
f.write('bbb\n')
Out[18]:
4
In [19]:
f.write('ccc\n')
Out[19]:
4
In [20]:
f.close()

Метод write возвращает число записанных символов.

Опять же, лучше использовать with.

In [21]:
with open('newtext.txt','w') as f:
    f.write('aaa\n')
    f.write('bbb\n')
    f.write('ccc\n')
In [22]:
!cat newtext.txt
aaa
bbb
ccc

Эта функция копирует старый текстовый файл в новый. Если строки нужно как-нибудь обработать, в последней строчке вместо line будет стоять что-нибудь вроде f(line).

In [23]:
def copy(old_name,new_name):
    with open(old_name) as old,open(new_name,'w') as new:
        for line in old:
            new.write(line)
In [24]:
copy('text.txt','newtext.txt')
In [25]:
!cat newtext.txt
abcd
efgh
ijkl

Если в программе используется какой-нибудь ресурс, который обязательно надо освободить после использования (например, сетевое соединение или соединение с базой данных), то лучше написать класс, реализующий методы __enter__ и __exit__, и использовать этот ресурс в блоке with.

In [29]:
class Connection:
    
    def __init__(self):
        self.opened=False
    
    def __enter__(self):
        print('Открываем')
        self.opened=True
    
    def __exit__(self,ex_type,ex_value,ex_traceback):
        if ex_value:
            print(f'Exception {ex_value}')
        print('Закрываем')
        self.opened=False
In [27]:
def f(x):
    with Connection() as conn:
        x=1/x
    return x
In [30]:
f(1)
Открываем
Закрываем
Out[30]:
1.0
In [31]:
f(0)
Открываем
Exception division by zero
Закрываем
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-31-421db934c3f8> in <module>()
----> 1 f(0)

<ipython-input-27-3f8e5906ed46> in f(x)
      1 def f(x):
      2     with Connection() as conn:
----> 3         x=1/x
      4     return x

ZeroDivisionError: division by zero

В интерактивной сессии (или в программе, запущенной с командной строки) можно попросить пользователя что-нибудь ввести. Аргумент функции input - это приглашение для ввода (prompt). Можно использовать просто input(), тогда приглашения не будет. Но это неудобно, т.к. в этом случае трудно заметить, что программа чего-то ждёт.

In [32]:
s=input('Введите целое число ')
Введите целое число 123
In [33]:
s
Out[33]:
'123'
In [34]:
n=int(s)
n
Out[34]:
123

Питон - интерпретатор, поэтому он может во время выполнения программы интерпретировать строки как куски исходного текста на языке питон. Так, функция eval интерпретирует строку как выражение и вычисляет его (в текущем контексте - подставляя текущие значения переменных).

In [35]:
s=input('Введите выражение ')
Введите выражение n+1
In [36]:
s
Out[36]:
'n+1'
In [37]:
eval(s)
Out[37]:
124

А функция exec интерпретирует строку как оператор и выполняет его. Оператор может менять значения переменных в текущем пространстве имён.

In [38]:
s=input('Введите оператор ')
Введите оператор x=n
In [39]:
s
Out[39]:
'x=n'
In [40]:
exec(s)
x
Out[40]:
123

Строка s может быть результатом длинного и сложного вычисления. Но лучше таких фокусов не делать, так как программа фактически становится самомодифицирующейся. Такие программы очень сложно отлаживать.

Для работы с путями к файлам и директориям в стандартной библиотеке существует модуль pathlib. Объект класса Path представляет собой путь к файлу или директории.

In [41]:
from pathlib import Path

Path() возвращает текущую директорию.

In [42]:
p=Path()
p
Out[42]:
PosixPath('.')

Очень полезный метод resolve приводит путь к каноническому виду.

In [43]:
p.resolve()
Out[43]:
PosixPath('/home/grozin/python')

Путь может быть записан в совершенно идиотском виде; resolve его исправит.

In [44]:
p=Path('.././/book')
p=p.resolve()
p
Out[44]:
PosixPath('/home/grozin/book')

Статический метод cwd возвращает текущую директорию (current working directory).

In [45]:
Path.cwd()
Out[45]:
PosixPath('/home/grozin/python')

Если p - путь к директории, то можно посмотреть все файлы в ней.

In [48]:
for f in p.iterdir():
    print(f)
/home/grozin/book/b102_strings.ipynb
/home/grozin/book/C2.pyx
/home/grozin/book/cfib.h
/home/grozin/book/foo.pxd
/home/grozin/book/b26a_sh.ipynb
/home/grozin/book/foo.c
/home/grozin/book/b27_cython.ipynb
/home/grozin/book/b23_mpmath_pdf.ipynb
/home/grozin/book/b107a_iterators.ipynb
/home/grozin/book/b25a_minuit.ipynb
/home/grozin/book/b103_lists.ipynb
/home/grozin/book/C1.pyx
/home/grozin/book/b25a_minuit_files
/home/grozin/book/b109_exceptions.ipynb
/home/grozin/book/foo.pyx
/home/grozin/book/fac.py
/home/grozin/book/d1
/home/grozin/book/Untitled.ipynb
/home/grozin/book/b24_pandas.ipynb
/home/grozin/book/newtext.txt
/home/grozin/book/b21_numpy.ipynb
/home/grozin/book/b108_oop.ipynb
/home/grozin/book/wrap.pyx
/home/grozin/book/b23_mpmath.ipynb
/home/grozin/book/b25_sympy.ipynb
/home/grozin/book/b106_dictionaries.ipynb
/home/grozin/book/b101_numbers.ipynb
/home/grozin/book/cfoo.c
/home/grozin/book/text.txt
/home/grozin/book/b21_numpy_pdf.ipynb
/home/grozin/book/b22_matplotlib.ipynb
/home/grozin/book/b24_pandas_pdf.ipynb
/home/grozin/book/b104_tuples.ipynb
/home/grozin/book/b25a_minuit_pdf.ipynb
/home/grozin/book/b26b_rpyc.ipynb
/home/grozin/book/wrap.c
/home/grozin/book/p1
/home/grozin/book/b25_sympy_pdf.ipynb
/home/grozin/book/b107_functions.ipynb
/home/grozin/book/b110_modules.ipynb
/home/grozin/book/b105_sets.ipynb
/home/grozin/book/b111_input_output.ipynb
/home/grozin/book/cfoo.h
/home/grozin/book/b22_matplotlib_pdf.ipynb
/home/grozin/book/cfib.c

Если p - путь к директории, то p/'fname' - путь к файлу fname в ней (он, конечно, тоже может быть директорией).

In [49]:
p2=p/'b101_numbers.ipynb'
p2
Out[49]:
PosixPath('/home/grozin/book/b101_numbers.ipynb')

Существует ли такой файл?

In [50]:
p2.exists()
Out[50]:
True

Является ли он симлинком, директорией, файлом?

In [51]:
p2.is_symlink(),p2.is_dir(),p2.is_file()
Out[51]:
(False, False, True)

Части пути p2.

In [52]:
p2.parts
Out[52]:
('/', 'home', 'grozin', 'book', 'b101_numbers.ipynb')

Родитель - директория, в которой находится этот файл.

In [53]:
p2.parent,p2.parent.parent
Out[53]:
(PosixPath('/home/grozin/book'), PosixPath('/home/grozin'))

Имя файла, его основа и суффикс.

In [54]:
p2.name,p2.stem,p2.suffix
Out[54]:
('b101_numbers.ipynb', 'b101_numbers', '.ipynb')

Метод stat возвращает всякую ценную информацию о файле.

In [55]:
s=p2.stat()
s
Out[55]:
os.stat_result(st_mode=33188, st_ino=2037028, st_dev=2052, st_nlink=1, st_uid=1000, st_gid=1000, st_size=17223, st_atime=1506946026, st_mtime=1506946026, st_ctime=1508389409)

Например, его размер в байтах.

In [56]:
s.st_size
Out[56]:
17223

Я написал полезную утилиту для поиска одинаковых файлов. Ей передаётся произвольное число аргументов - директорий и файлов. Она рекурсивно обходит директории, находит размер всех файлов (симлинки игнорируются) и строит словарь, сопоставляющий каждому размеру список файлов, имеющих такой размер. Это простой этап, не требующий чтения (возможно больших) файлов. После этого файлы из тех списков, длина которых $>1$, сравниваются функцией cmp из библиотечного модуля filecmp (что, конечно, требует их чтения).

In [58]:
!cat dup.py
#!/usr/bin/env python3
from sys import argv
from pathlib import Path
from filecmp import cmp

d={}

def add(p):
    global d
    if p.is_file():
        size=p.stat().st_size
        if size in d:
            d[size].append(p)
        else:
            d[size]=[p]
    elif p.is_dir():
        for x in p.iterdir():
            add(x)

for x in argv[1:]:
    p=Path(x)
    if p.exists():
        add(p.absolute())
    else:
        print(f'{x} does not exist')

for s in reversed(sorted(d.keys())):
    if len(d[s])>1:
        l=[]
        for x in sorted(d[s]):
            new=True
            for y in l:
                if cmp(str(x),str(y[0]),shallow=False):
                    y.append(str(x))
                    new=False
                    break
            if new:
                l.append([str(x)])
        for y in l:
            if len(y)>1:
                print(y)
In [59]:
!ls test
a3.txt	a4.txt	b3.txt	b4.txt	sub
In [60]:
!ls test/sub
a4.txt	b3.txt
In [62]:
!./dup.py test
['/home/grozin/python/test/a4.txt', '/home/grozin/python/test/sub/a4.txt']
['/home/grozin/python/test/b3.txt', '/home/grozin/python/test/sub/b3.txt']

В питоне можно работать с переменными окружения как с обычным словарём.

In [63]:
from os import environ
In [64]:
environ['PATH']
Out[64]:
'/usr/lib/python-exec/python3.6:/home/grozin/bin:/home/grozin/reduce-3783/bin:/usr/local/bin:/usr/bin:/bin:/opt/bin:/usr/games/bin'
In [65]:
environ['ABCD']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-65-71e016be80d8> in <module>()
----> 1 environ['ABCD']

/usr/lib64/python3.6/os.py in __getitem__(self, key)
    667         except KeyError:
    668             # raise KeyError with the original key value
--> 669             raise KeyError(key) from None
    670         return self.decodevalue(value)
    671 

KeyError: 'ABCD'
In [66]:
environ['ABCD']='abcd'
In [67]:
environ['ABCD']
Out[67]:
'abcd'

Мы не просто добавили пару ключ-значение в словарь, а действительно добавили новую переменную к текущему окружению. Если теперь вызвать из питона какую-нибудь внешнюю программу, то она эту переменную увидит. Эта переменная исчезнет, когда закончится выполнение текущей программы на питоне (или интерактивная сессия).