Архив метки: Python

Оператор with

Теория

Оператор with появился в python 2.5, но, не смотря на это, используется до сих пор недостаточно широко. Являясь упрощенной версией анонимных блоков кода with позволяет:

  • исполнить код до начала блока
  • исполнить код по выходу из блока, независимо от того это выход по исключению с помощью return или другим способом
  • обработать исключение, возникшее в блоке.

Синтаксически with выглядит следующим образом:

Hightlited/Raw

with operation:
code

with operation:
code

operation может быть объектом, выражением или конструкцией вида expression as var. Как и много других конструкций он является синтаксическим сахаром для более громоздкого выражения:

Hightlited/Raw

with operation as var:
code

with operation as var:
code

=>

Hightlited/Raw

_obj = operation

# вход в блок
var = _obj.__enter__()

try:
code
except Exception as exc:
# если произошло исключение - передаем его управляющему объекту
if not _obj.__exit__(*sys.exception_info()):
# если он вернул False(None) возбуждаем его
raise
# если True - подавляем исключение
else:
# если не было исключения - передаем None * 3
_obj.__exit__(None, None, None)

_obj = operation

# вход в блок
var = _obj.__enter__()

try:
code
except Exception as exc:
# если произошло исключение - передаем его управляющему объекту
if not _obj.__exit__(*sys.exception_info()):
# если он вернул False(None) возбуждаем его
raise
# если True - подавляем исключение
else:
# если не было исключения - передаем None * 3
_obj.__exit__(None, None, None)

Более подробно с with можно ознакомиться в соответствующем PEP-343. with управляется объектом, называемым менеджером контекста (МК) — _obj в примере выше. Есть два основных способа написания МК — класс с методами __enter__ и __exit__ и генератор:

Hightlited/Raw

import os
from contextlib import contextmanager

# Это только пример.
# Использование такого кода для генерации временных файлов
# небезопасно. Используйте функции 'os.tmpfile'.

class TempoFileCreator(object):
def __init__(self):
self.fname = None
self.fd = None< br />
def __inter__(self):
# вызывается по входу в блок
self.fname = os.tmpnam()
self.fd = open(self.fname, "w+")
return self.fname, self.fd

def __exit__(self, exc_type, exc_val, traceback):
# вызывается по выходу из блока
# если в блоке выброшено исключение, то
# его тип, значение и трейс будут переданы в параметрах

self.fd.close()
os.unlink(self.fname)
self.fd = None
self.fname = None

# здесь написано return None => исключение не будет подавляться

@contextmanager
def tempo_file():
# полностью равноценно классу TempoFileCreator
fname = os.tmpnam()
fd = open(fname, "w+")
try:
yield fname, fd
#сейчас исполняется блок
finally:
# это наш __exit__
fd.close()
os.unlink(fd)

import os
from contextlib import contextmanager

# Это только пример.
# Использование такого кода для генерации временных файлов
# небезопасно. Используйте функции 'os.tmpfile'.

class TempoFileCreator(object):
def __init__(self):
self.fname = None
self.fd = None

def __inter__(self):
# вызывается по входу в блок
self.fname = os.tmpnam()
self.fd = open(self.fname, "w+")
return self.fname, self.fd

def __exit__(self, exc_type, exc_val, traceback):
# вызывается по выходу из блока
# если в блоке выброшено исключение, то
# его тип, значение и трейс будут переданы в параметрах

self.fd.close()
os.unlink(self.fname)
self.fd = None
self.fname = None

# здесь написано return None => исключение не будет подавляться

@contextmanager
def tempo_file():
# полностью равноценно классу TempoFileCreator
fname = os.tmpnam()
fd = open(fname, "w+")
try:
yield fname, fd
#сейчас исполняется блок
finally:
# это наш __exit__
fd.close()
os.unlink(fd)

Использование:

Hightlited/Raw

with tempo_file() as (fname, fd):
# читаем-пишем в файл
# по выходу из блока он будет удален
pass

with tempo_file() as (fname, fd):
# читаем-пишем в файл
# по выходу из блока он будет удален
pass

Ядро python реализует только первый вариант для контекст менеджера, второй реализуется в contextlib.contextmanager.

В том случае если во внутреннем блоке кода есть оператор yield, т.е. мы работаем в генераторе, __exit__ будет вызван по выходу из генератора или по его удалению. Таким образом если ссылку на генератор сохранить, то __exit__ не будет вызван до тех пор, пока ссылка будет существовать:

Hightlited/Raw

@contextmanager
def cmanager():
yield
print "Exit"

def some_func():
with cmanager():
yield 1

it = some_func()
for val in it:
pass
# Exit напечатается здесь

it = some_func()

del it # или по выходу из текущего блока
# Exit напечатается здесь

@contextmanager
def cmanager():
yield
print "Exit"

def some_func():
with cmanager():
yield 1

it = some_func()
for val in it:
pass
# Exit напечатается здесь

it = some_func()

del it # или по выходу из текущего блока
# Exit напечатается здесь

Подводя итоги — with позволяет сэкономить 2-4 строки кода на каждое использование и повышает читаемость программы, меньше отвлекая нас от логики деталями реализации.

Практика

Начнем с примеров, которые встречаются в стандартной библиотеке и будем постепенно переходить к менее распространенным вариантам использования.

  • Открытие/создание объекта по входу в блок — закрытие/удаление по выходу:

Hightlited/Raw

with open('/tmp/tt.txt') as fd:
pass
# здесь файл закрывается
# переменная fd доступна, но файл уже закрыт
#

with open('/tmp/tt.txt') as fd:
pass
# здесь файл закрывается
# переменная fd доступна, но файл уже закрыт
#

Чаще всего в python программах не закрывают файл вручную, обоснованно полагаясь на подсчет ссылок. Блоки with кроме явного указания области, где файл открыт имеют еще одно небольшое преимущество, связанное с особенностями обработки исключений:

Hightlited/Raw

def i_am_not_always_close_files(fname):
fd = open(fname)

i_am_not_always_close_files("/tmp/x.txt")
# в этой точке файл уже закрыт

def i_am_not_always_close_files(fname):
fd = open(fname)

i_am_not_always_close_files("/tmp/x.txt")
# в этой точке файл уже закрыт

Если внутри фцнкции i_am_not_always_close_files будет возбуждено исключение, то файл не закроется до того момента, пока оно не будет обработано:

Hightlited/Raw

import sys

def i_am_not_always_close_files(fname):
fd = open(fname)
raise RuntimeError('')

try:
i_am_not_always_close_files("/tmp/x.txt")
except RuntimeError:
#тут файл еще открыт
traceback = sys.exc_info()[2]

# спуск на один кадр стека глубже
# 'fd' в его локальных переменных
print traceback.tb_next.tb_frame.f_locals['fd']

#

# в этой точке файл уже закрыт

import sys

def i_am_not_always_close_files(fname):
fd = open(fname)
raise RuntimeError('')

try:
i_am_not_always_close_files("/tmp/x.txt")
except RuntimeError:
#тут файл еще открыт
traceback = sys.exc_info()[2]

# спуск на один кадр стека глубже
# 'fd' в его локальных переменных
print traceback.tb_next.tb_frame.f_locals['fd']

#

# в этой точке файл уже закрыт

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

Еще пример:

Hightlited/Raw

# создадим виртуальную машину
with create_virtual_machine(root_passwd) as vm_ip:
# выполним на ней тестирования скрипта автоматической установки
test_auto_deploy_script(vm_ip, root_passwd)
# по выходу уничтожим vm_ip

# создадим виртуальную машину
with create_virtual_machine(root_passwd) as vm_ip:
# выполним на ней тестирования скрипта автоматической установки
test_auto_deploy_script(vm_ip, root_passwd)
# по выходу уничтожим vm_ip

  • Захват/освобождение объекта Эту семантику поддерживают все стандартные объекты синхронизации

Hightlited/Raw

import threading
lock = Threading.Lock()

with lock:
# блокровка захваченна
pass
# блокировка отпущенна


import threading
lock = Threading.Lock()

with lock:
# блокровка захваченна
pass
# блокировка отпущенна

  • Временное изменение настроек (примеры из документации python)

Hightlited/Raw

import warnings
from decimal import localcontext

with warnings.catch_warnings():
warnings.simplefilter("ignore")
# в этом участке кода все предепреждения игнорируются

with localcontext() as ctx:
ctx.prec = 42 # расчеты с типом Decimal выполняются с
# заоблачной точностью
s = calculate_something()

import warnings
from decimal import localcontext

with warnings.catch_warnings():
warnings.simplefilter("ignore")
# в этом участке кода все предепреждения игнорируются

with localcontext() as ctx:
ctx.prec = 42 # расчеты с типом Decimal выполняются с
# заоблачной точностью
s = calculate_something()

  • Смена текущей директории (пример использования библиотеки fabric)

Hightlited/Raw

from fabric.context_managers import lcd

os.chdir('/opt')
print os.getcwd() # => /opt

with lcd('/tmp'):
print os.getcwd() # => /tmp

print os.getcwd() # => /opt

from fabric.context_managers import lcd

os.chdir('/opt')
print os.getcwd() # => /opt

with lcd('/tmp'):
print os.getcwd() # => /tmp

print os.getcwd() # => /opt

Нужно помнить, что изменение таким образом глобальных настроек в многопоточной программе может доставить много веселых минут при отладке.

  • Подмена/восстановление объекта (временный monkey patching, пример использования библиотеки mock)

Hightlited/Raw

import mock

my_mock = mock.MagicMock()
with mock.patch('__builtin__.open', my_mock):
# open подменена на mock.MagicMock
with open('foo') as h:
pass

import mock

my_mock = mock.MagicMock()
with mock.patch('__builtin__.open', my_mock):
# open подменена на mock.MagicMock
with open('foo') as h:
pass

  • Транзакции баз данных….

Менеджер транзакций для sqlalchemy

Hightlited/Raw

from config import DB_URI
from db_session import get_session

class DBWrapper(object):

def __init__(self):
self.session = None

def __enter__(self):
self.session = get_session(DB_URI)

def __exit__(self, exc, *args):
# при выходе из 'with':
if exc is None:
# если все прошло успешно коммитим
# транзакцию и закрываем курсор
self.session.commit()

# если было исключение - откатываем
self.session.close()

# тут методы, скрывающие работу с базой

with DBWrapper() as dbw: # открываем транзакцию
dbw.get_some_data()
dbw.update_some_data("...")

from config import DB_URI
from db_session import get_session

class DBWrapper(object):

def __init__(self):
self.session = None

def __enter__(self):
self.session = get_session(DB_URI)

def __exit__(self, exc, *args):
# при выходе из 'with':
if exc is None:
# если все прошло успешно коммитим
# транзакцию и закрываем курсор
self.session.commit()

# если было исключение - откатываем
self.session.close()

# тут методы, скрывающие работу с базой

with DBWrapper() as dbw: # открываем транзакцию
dbw.get_some_data()
dbw.update_some_data("...")

  • ….и не только баз данных

Hightlited/Raw

from threading import local
import subprocess

# обобщенная транзакция - выполняет набор обратных действий
# при возникновении в блоке 'with' не обработанного исключения

class Transaction(object):
def __init__(self, parent):
self.rollback_cmds = []
self.set_parent(parent)

def set_parent(self, parent):
# родительская транзакция
# если откатывается родительская транзакция, то она автоматом
# откатывает и дочерние, даже если они было уже успешно закрыты
# если откатывается дочерняя, то родительская может продолжить
# исполнение, если код выше по стеку обработает исключение

if parent is not None:
self.parent_add = parent.add
else:
self.parent_add = lambda *cmd : None

def __enter__(self):
return self

def __exit__(self, exc, *dt):
if exc is None:
self.commit()
else:
self.rollback()

def add(self, cmd):
self.parent_add(cmd)
self.transaction.append(cmd)

def commit(self):
self.transaction = []

def rollback(self):
for cmd in reversed(self.transaction):
if isinstance(cmd, basestring):
subprocess.check_call(cmd, shell=True)
else:
cmd[0](*cmd[1:])


class AutoInheritedTransaction(object):
# словарь, id потока => [список вложенных транзакций]
# позволяет автоматически находить родительскую транзакцию
# в том случае, если для каждого потока может быть не более
# одной цепи вложенных транзакций

transactions = local()

def __init__(self):
super(AutoInheritedTransaction, self).__init__(self.current())
self.register()

def register(self):
self.transaction.list = getattr(self.transaction, 'list') + [self]

@classmethod
def current(cls):
return getattr(self.transaction, 'list', [None])[-1]

used_loop_devs = []

with AutoInheritedTransaction() as tr:
# создаем loop устройство
loop_name = subprocess.check_output("losetup -f --show /tmp/fs_image")
# вызов для его удаления
tr.add("losetup -d " + loop_name)

# записываем новое устройство в массив
used_loop_devs.append(loop_name)
tr.add(lambda : used_loop_devs.remove(
used_loop_devs.index(
loop_name)))

# монтируем его
subprocess.check_output("mount {0} /mnt/some_dir")
tr.add("umount /mnt/some_dir")

some_code

from threading import local
import subprocess

# обобщенная транзакция - выполняет набор обратных действий
# при возникновении в блоке 'with' не обработанного исключения

class Transaction(object):
def __init__(self, parent):
self.rollback_cmds = []
self.set_parent(parent)

def set_parent(self, parent):
# родительская транзакция
# если откатывается родительская транзакция, то она автоматом
# откатывает и дочерние, даже если они было уже успешно закрыты
# если откатывается дочерняя, то родительская может продолжить
# исполнение, если код выше по стеку обработает исключение

if parent is not None:
self.parent_add = parent.add
else:
self.parent_add = lambda *cmd : None

def __enter__(self):
return self

def __exit__(self, exc, *dt):
if exc is None:
self.commit()
else:
self.rollback()

def add(self, cmd):
self.parent_add(cmd)
self.transaction.append(cmd)

def commit(self):
self.transaction = []

def rollback(self):
for cmd in reversed(self.transaction):
if isinstance(cmd, basestring):
subprocess.check_call(cmd, shell=True)
else:
cmd[0](*cmd[1:])


class AutoInheritedTransaction(object):
# словарь, id потока => [список вложенных транзакций]
# позволяет автоматически находить родительскую транзакцию
# в том случае, если для каждого потока может быть не более
# одной цепи вложенных транзакций

transactions = local()

def __init__(self):
super(AutoInheritedTransaction, self).__init__(self.current())
self.register()

def register(self):
self.transaction.list = getattr(self.transaction, 'list') + [self]

@classmethod
def current(cls):
return getattr(self.transaction, 'list', [None])[-1]

used_loop_devs = []

with AutoInheritedTransaction() as tr:
# создаем loop устройство
loop_name = subprocess.check_output("losetup -f --show /tmp/fs_image")
# вызов для его удаления
tr.add("losetup -d " + loop_name)

# записываем новое устройство в массив
used_loop_devs.append(loop_name)
tr.add(lambda : used_loop_devs.remove(
used_loop_devs.index(
loop_name)))

# монтируем его
subprocess.check_output("mount {0} /mnt/some_dir")
tr.add("umount /mnt/some_dir")

some_code

Эта модель программирования позволяет группировать в одной точке код прямой и обратной операции и избавляет от вложенных try/finally. Также with предоставляет естественный интерфейс для STM. cpython-withatomic — один из вариантов STM для руthon с поддержкой with.

  • Подавление исключений

Hightlited/Raw

def supress(*ex_types):
# стоит добавить логирования подавляемого исключения
try:
yield
except Exception as x:
if not isinstance(x, ex_types):
raise

with supress(OSError):
os.unlink("some_file")

def supress(*ex_types):
# стоит добавить логирования подавляемого исключения
try:
yield
except Exception as x:
if not isinstance(x, ex_types):
raise

with supress(OSError):
os.unlink("some_file")

  • Генерация XML/HTML других структурированных языков.

Hightlited/Raw

from xmlbuilder import XMLBuilder

# новый xml документ

x = XMLBuilder('root')
x.some_tag
x.some_tag_with_data('text', a='12')

# вложенные теги
with x.some_tree(a='1'):
with x.data:
x.mmm
x.node(val='11')

print str(x) # <= string object


from xmlbuilder import XMLBuilder

# новый xml документ

x = XMLBuilder('root')
x.some_tag
x.some_tag_with_data('text', a='12')

# вложенные теги
with x.some_tree(a='1'):
with x.data:
x.mmm
x.node(val='11')

print str(x) # <= string object

Получим в итоге:

Hightlited/Raw



/>
a="12">text
a="1">

/>
val="11" />






text






Код библиотеки находится на xmlbuilder.

  • Трассировка блока в логере (установка sys.settrace)

Hightlited/Raw

import sys
import contextlib

def on_event(fr, evt, data):
print fr, evt, data
return on_event

@contextlib.contextmanager
def trace_me():

prev_trace = sys.gettrace()
sys.settrace(on_event)
try:
yield
finally:
sys.settrace(prev_trace)
print "after finally"


with trace_me():
print "in with"
x = 1
y = 2
print "before gettrace"
sys.gettrace()
print "after gettrace"

import sys
import contextlib

def on_event(fr, evt, data):
print fr, evt, data
return on_event

@contextlib.contextmanager
def trace_me():

prev_trace = sys.gettrace()
sys.settrace(on_event)
try:
yield
finally:
sys.settrace(prev_trace)
print "after finally"


with trace_me():
print "in with"
x = 1
y = 2
print "before gettrace"
sys.gettrace()
print "after gettrace"

Этот
код напечатает:

    in with
before gettrace
after gettrace
call None
line None
line None
line None
call None
line None
after finally

Для лучшего понимания трассировки питона — python-aware-python.

Ссылки:
          www.python.org/dev/peps/pep-0343
          docs.python.org/reference/compound_stmts.html#the-with-statement
          github.com/koder-ua/megarepo/tree/master/xmlbuilder/xmlbuilder
          www.voidspace.org.uk/python/mock/compare.html#mocking-a-context-manager
          en.wikipedia.org/wiki/Monkey_patch
          www.sqlalchemy.org
          fabfile.org
          en.wikipedia.org/wiki/Software_Transaction_Memory
          bitbucket.org/arigo/cpython-withatomic
          blip.tv/pycon-us-videos-2009-2010-2011/pycon-2011-python-aware-python-4896752

Исходники этого и других постов со скриптами лежат тут — github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.

Автор: konstantin danilov

Динамические атрибуты в python. Часть 1, теоретическая — "в поисках атрибута"

Disclamer: рассмотрены только новые классы — все наследуемые от object. В «старых» классах все работает немного по-другому.

Во всей статье классы именуются с заглавной буквы, а экземпляры классов со строчной. A это класс экземпляра a.

Вопрос о том что происходит когда python исполняет конструкцию a.b очень важен для понимания многих других тем. Особенно учитывая что имитация атрибутов один из наиболее часто используемых приемов для написания библиотек в pythonic стиле.

Сначала посмотрим где принципиально может быть '»b»'. Из документации python можно узнать о следующих возможных вариантах.

  • A.__getattrib__(a, «b»)
  • 'a.__dict__[«b»]'' ; пока без '__slots__
  • 'A.__dict__[«b»]'', вместо 'A может быть один из базовых для A классов.
  • A.__dict__[«b»](a) — свойство (property)
  • A.__getattr__(a, «b»)

A.__getattrib__(a, «b») гарантировано вызывается первым, A.__getattr__(a, «b») последним, а a.__dict__[«b»] имеет приоритет над 'A.__dict__[«b»]'. Без ответов остаются вопросы о приоритете property и о том, что происходит если на отдельных фазах будут возбужденны исключения.

Ответить на эти вопросы можно разными способами, но только чтение исходников гарантированно ответит на них со всеми тонкостями. Все не желающие идти путь самурая по С коду могут смело пролистать вниз ~250 сток до итогового результата и Очень Важной Картинки.

Итак скачиваем последнюю версию исходников cpython и будем погружаться. Все начинается в cpython/Python/ceval.c со строки вида TARGET(LOAD_ATTR) (у меня строка 2228). Мы находимся прямо в сердце виртуальной машины сpython, в цикле eval — здесь сpython по очереди исполняет инструкции байтокода. dis.dis говорит нам, что a.b компилируется в две инструкции:

    >>> import dis
>>> dis.dis(lambda : a.b )

1 0 LOAD_GLOBAL 0 (a)
3 LOAD_ATTR 1 (b)
6 RETURN_VALUE

Так что инструкция LOAD_ATTR это как раз то, что нам нужно. Тело TARGET(LOAD_ATTR) содержит стандартную возню со стеком, подсчет ссылок и интересующий нас вызов:

Hightlited/Raw

x = PyObject_GetAttr(v, w); //Здесь 'v' - 'a', а 'w' -  'b'.

x = PyObject_GetAttr(v, w); //Здесь 'v' - 'a', а 'w' -  'b'.

Hightlited/Raw

//Ф-ция PyObject_GetAttr находится в cpython/objects/object.c

PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{
// получаем тип v
PyTypeObject *tp = Py_TYPE(v);

// бла-бла-бла, проверка типов
// получение атрибута
if (tp->tp_getattro != NULL)
return (*tp->tp_getattro)(v, name);

// не важный код
}

//Ф-ция PyObject_GetAttr находится в cpython/objects/object.c

PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{
// получаем тип v
PyTypeObject *tp = Py_TYPE(v);

// бла-бла-бла, проверка типов
// получение атрибута
if (tp->tp_getattro != NULL)
return (*tp->tp_getattro)(v, name);

// не важный код
}

Ок, следующие два участка кода, которые могут ответить на наш вопрос это структура object и метод type.__new__. Первый наследуют почти все классы, а второй может повлиять на структуру новых классов.

Hightlited/Raw

//cpython/Objects/typeobject.c:3260

PyTypeObject PyBaseObject_Type < span style="color: #666666">= { // эта девушка! (с)
// bla-bla-bla
"object", /* tp_name */
// bla-bla-bla

// __getattribute__
PyObject_GenericGetAttr, /* tp_getattro */
// .......
}


// PyObject_GenericGetAttr:
// cpython/objects/object.c

PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
return _PyObject_GenericGetAttrWithDict(obj, name, NULL);
}

// cpython/objects/object.c
// первый солидный кусок кода
PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict)
{
PyTypeObject *tp = Py_TYPE(obj);

.......

if (!PyUnicode_Check(name)){
.... // исключение
}
else
Py_INCREF(name);

// ......

// ищем name по mro
descr = _PyType_Lookup(tp, name);

.....

if (descr != NULL) {

// если у descr есть __get__ - перед нами дескриптор
f = descr->ob_type->tp_descr_get;

// если дескриптор данных
if (f != NULL && PyDescr_IsData(descr)) {

// получаем из него значение и выходим
res = f(descr, obj, (PyObject *)obj->ob_type);
Py_DECREF(descr);
goto done;
}
}

// dict это параметр для рекурсивного поиска
// при вызове из tp_getattro он всегда NULL
if (dict == NULL) {
/* Inline _PyObject_GetDictPtr */
// тут получаем смещение __dict__ внутри объекта
// ну и сам __dict__ по нему
dictptr = (PyObject **) ((char *)obj + dictoffset);
dict = *dictptr;
}

// если dict нашелся
if (dict != NULL) {
Py_INCREF(dict);

// получаем a.__dict__['b']
// если 'b' not in a.__dict__, то PyDict_GetItem установит
// исключение и вернет NULL
// PyObject_GetAttr затем заменит это исключение(KeyError) на AttributeError
res = PyDict_GetItem(dict, name);
if (res != NULL) {
....
goto done;
}
....
}

// возвращаемся к дескриптору
// если он хоть какой-нить
// используем его
if (f != NULL) {
res = f(descr, obj, (PyObject *)Py_TYPE(obj));
Py_DECREF(descr);
goto done;
}

// если вообще нашли что-то в базовых классах
if (descr != NULL) {
res = descr;
/* descr was already increfed above */
goto done;
}

// иначе raise AttributeError
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%U'",
tp->tp_name, name);
done:
Py_DECREF(name);
return res;
}

//cpython/Objects/typeobject.c:3260

PyTypeObject PyBaseObject_Type = { // эта девушка! (с)
// bla-bla-bla
"object", /* tp_name */
// bla-bla-bla

// __getattribute__
PyObject_GenericGetAttr, /* tp_getattro */
// .......
}


// PyObject_GenericGetAttr:
// cpython/objects/object.c

PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
return _PyObject_GenericGetAttrWithDict(obj, name, NULL);
}

// cpython/objects/object.c
// первый солидный кусок кода
PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict)
{
PyTypeObject *tp = Py_TYPE(obj);

.......

if (!PyUnicode_Check(name)){
.... // исключение
}
else
Py_INCREF(name);

// ......

// ищем name по mro
descr = _PyType_Lookup(tp, name);

.....

if (descr != NULL) {

// если у descr есть __get__ - перед нами дескриптор
f = descr->ob_type->tp_descr_get;

// если дескриптор данных
if (f != NULL && PyDescr_IsData(descr)) {

// получаем из него значение и выходим
res = f(descr, obj, (PyObject *)obj->ob_type);
Py_DECREF(descr);
goto done;
}
}

// dict это параметр для рекурсивного поиска
// при вызове из tp_getattro он всегда NULL
if (dict == NULL) {
/* Inline _PyObject_GetDictPtr */
// тут получаем смещение __dict__ внутри объекта
// ну и сам __dict__ по нему
dictptr = (PyObject **) ((char *)obj + dictoffset);
dict = *dictptr;
}

// если dict нашелся
if (dict != NULL) {
Py_INCREF(dict);

// получаем a.__dict__['b']
// если 'b' not in a.__dict__, то PyDict_GetItem установит
// исключение и вернет NULL
// PyObject_GetAttr затем заменит это исключение(KeyError) на AttributeError
res = PyDict_GetItem(dict, name);
if (res != NULL) {
....
goto done;
}
....
}

// возвращаемся к дескриптору
// если он хоть какой-нить
// используем его
if (f != NULL) {
res = f(descr, obj, (PyObject *)Py_TYPE(obj));
Py_DECREF(descr);
goto done;
}

// если вообще нашли что-то в базовых классах
if (descr != NULL) {
res = descr;
/* descr was already increfed above */
goto done;
}

// иначе raise AttributeError
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%U'",
tp->tp_name, name);
done:
Py_DECREF(name);
return res;
}

Выясним что такое PyDescr_IsData:

Hightlited/Raw

//cpython/Include/descrobject.h
#define PyDescr_IsData(d) (Py_TYPE(d)->tp_descr_set != NULL)

//cpython/Include/descrobject.h
#define PyDescr_IsData(d) (Py_TYPE(d)->tp_descr_set != NULL)

Переведем на почти python

Hightlited/Raw

def PyObject_GenericGetAttr(obj, name):
tp_attr = getattr(obj.__class__, name, NULL)
f = getattr(tp_attr, '__get__', NULL)
if f != NULL:
if hasattr(tp_attr, '__set__'):
return f(obj)
if obj.have_a_slot(__dict__):
return obj.__dict__[name]
if f != NULL:
return f(obj)
if tp_attr != NULL:
return tp_attr
raise AttributeError('......')

def PyObject_GenericGetAttr(obj, name):
tp_attr = getattr(obj.__class__, name, NULL)
f = getattr(tp_attr, '__get__', NULL)
if f != NULL:
if hasattr(tp_attr, '__set__'):
return f(obj)
if obj.have_a_slot(__dict__):
return obj.__dict__[name]
if f != NULL:
return f(obj)
if tp_attr != NULL:
return tp_attr
raise AttributeError('......')

Ситуация проясняется, переходим к type.__new__.

Hightlited/Raw

//cpython/objects/typeobject.c 
static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
// ... 453 строки разного, не важного для нас сейчас кода

/* Put the proper slots in place */
fixup_slot_dispatchers(type);

return (PyObject *)type;
}

//cpython/objects/typeobject.c 
static PyObject *
type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)
{
// ... 453 строки разного, не важного для нас сейчас кода

/* Put the proper slots in place */
fixup_slot_dispatchers(type);

return (PyObject *)type;
}

fixup_slot_dispatchers должен настроить слоты создаваемого объекта (в python CAPI слотами называются все поля PyObject tp_getattr/tp_getattro — слоты). fixup_slot_dispatchers использует slotdefs для обновления слотов. Это стуруктура содержит разнообразную информацию о слотах по умолчанию для классов. Cледующая остановка — slot_tp_getattro и slot_tp_getattr_hook — именно они описанны, как значения по умолчанию для tp_getattro в slotdefs.

Hightlited/Raw

// Используется если __getattribute__ перегружен а __getattr__ - нет.
// Изначально fixup_slot_dispatchers всегда помещает в объект
// slot_tp_getattr_hook и уже она произведет подмену себя на
// slot_tp_getattro, если в объекте нет __getattr__
// __getattribute__ есть всегда

static PyObject *
slot_tp_getattro(PyObject *self, PyObject *name)
{
static PyObject *getattribute_str = NULL;
return call_method(self, "__getattribute__", &getattribute_str,
"(O)", name);
}


static PyObject *
slot_tp_getattr_hook(PyObject *self, PyObject *name)
{
PyTypeObject *tp = Py_TYPE(self);

.....
// всякие проверки
.....

// если нет __getattr__ вызываем slot_tp_getattro
// и подменяем слот на slot_tp_getattro для будущих вызовов
getattr = _PyType_Lookup(tp, getattr_str);
if (getattr == NULL) {
/* No __getattr__ hook: use a simpler dispatcher */
tp->tp_getattro = slot_tp_getattro;
return slot_tp_getattro(self, name);
}


getattribute = _PyType_Lookup(tp, getattribute_str);

// если __getattribute__ не определен, или если он
// ссылается на PyObject_GenericGetAttr
// маленькая оптимизация
if (getattribute == NULL ||
(Py_TYPE(getattribute) == &PyWrapperDescr_Type &&
((PyWrapperDescrObject *)getattribute)->d_wrapped ==
(void *)PyObject_GenericGetAttr))
// ищем атрибут, используя PyObject_GenericGetAttr
res = PyObject_GenericGetAttr(self, name);
else {
// есть __getattribute__, оличный от PyObject_GenericGetAttr

Py_INCREF(getattribute);
res = call_attribute(self, getattribute, name);
Py_DECREF(getattribute);
}

// если ничего не нашли и есть исключение PyExc_AttributeError - очищаем исключение
// и вызываем self.__getattr__

if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
res = call_attribute(self, getattr, name);
}
Py_DECREF(getattr);
return res;
}

// Используется если __getattribute__ перегружен а __getattr__ - нет.
// Изначально fixup_slot_dispatchers всегда помещает в объект
// slot_tp_getattr_hook и уже она произведет подмену себя н а
// slot_tp_getattro, если в объекте нет __getattr__
// __getattribute__ есть всегда

static PyObject *
slot_tp_getattro(PyObject *self, PyObject *name)
{
static PyObject *getattribute_str = NULL;
return call_method(self, "__getattribute__", &getattribute_str,
"(O)", name);
}


static PyObject *
slot_tp_getattr_hook(PyObject *self, PyObject *name)
{
PyTypeObject *tp = Py_TYPE(self);

.....
// всякие проверки
.....

// если нет __getattr__ вызываем slot_tp_getattro
// и подменяем слот на slot_tp_getattro для будущих вызовов
getattr = _PyType_Lookup(tp, getattr_str);
if (getattr == NULL) {
/* No __getattr__ hook: use a simpler dispatcher */
tp->tp_getattro = slot_tp_getattro;
return slot_tp_getattro(self, name);
}


getattribute = _PyType_Lookup(tp, getattribute_str);

// если __getattribute__ не определен, или если он
// ссылается на PyObject_GenericGetAttr
// маленькая оптимизация
if (getattribute == NULL ||
(Py_TYPE(getattribute) == &PyWrapperDescr_Type &&
((PyWrapperDescrObject *)getattribute)->d_wrapped ==
(void *)PyObject_GenericGetAttr))
// ищем атрибут, используя PyObject_GenericGetAttr
res = PyObject_GenericGetAttr(self, name);
else {
// есть __getattribute__, оличный от PyObject_GenericGetAttr

Py_INCREF(getattribute);
res = call_attribute(self, getattribute, name);
Py_DECREF(getattribute);
}

// если ничего не нашли и есть исключение PyExc_AttributeError - очищаем исключение
// и вызываем self.__getattr__

if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
res = call_attribute(self, getattr, name);
}
Py_DECREF(getattr);
return res;
}

Теперь можно восстановить всю картину: a.b => Если:

  • __getattribute__ был перегружен, то вызывается A.__getattribute__(a, «b»).
  • есть свойство-данное — вызывается оно A.b.__get__(a)
  • b присутствует в a.__dict__, возвращаем a.__dict__[«b»]
  • есть какое-нить свойство — вызывается оно A.b.__get__(a)
  • b найдено в A.__dict__ или в __dict__ одного из базовых для A классов, => X.__dict__[«b»]
  • Если не перегружен __getattr__ — исключение/результат возвращаются
  • Если перегружен __getattr__ и ничего не найдено и тип исключения AttributeError — вызывается A.__getattr__(a, «b»)
  • исключение/результат возвращаются

Очень Важная Картинка

Нетривиальный результат:

Hightlited/Raw

class X(object):
def __getattribute__(self, name):
raise AttributeError(name)

f = 1

def __getattr__(self, name):
return name

class X(object):
def __getattribute__(self, name):
raise AttributeError(name)

f = 1

def __getattr__(self, name):
return name

В таком классе поле f будет всегда игнорироваться, но если __getattribute__ убрать — то все вернется на свои места и 'X().f' будет равно 1, а 'X().d == d'.

После просмотра кода возникает вопрос — какой ценой достается эта гибкость? Замеряем скорость доступа к разным вариантам атрибутов:

Hightlited/Raw

# глобальная переменная
global_var = 1

# пустая функция
def empty_func():
pass

# доступ к глобальной переменной из функции
def global_var_from_func():
global_var

# доступ к локальной переменной из функции
def local_var_from_func():
local_var = 1
local_var

# сложение целых
a = 1
# int + int
a + a

class A1(object):
def __getattribute__(self, name):
return None

class A2(object):

def get_b(self):
return None

def set_b(self, val):
pass

b = property(get_b, set_b)

class A3(object):
def __init__(self):
self.b = None

class A4(object):
def get_b(self):
return None

b = property(get_b)

class A5(object):
b = None


# создаем иерархию со 128ю уровнями вложенности наследования
# атрибут 'b' будет у "самого базового" класса
# в использовать для доступа будет экзампляр "самого дочернего"

class A6(object):
b = None

for i in range(127):
A6 = type('A6', (A6,), {})

a6 = A6()

class A7(object):
def __getattr__(self, name):
return None

# глобальная переменная
global_var = 1

# пустая функция
def empty_func():
pass

# доступ к глобальной переменной из функции
def global_var_from_func():
global_var

# доступ к локальной переменной из функции
def local_var_from_func():
local_var = 1
local_var

# сложение целых
a = 1
# int + int
a + a

class A1(object):
def __getattribute__(self, name):
return None

class A2(object):

def get_b(self):
return None

def set_b(self, val):
pass

b = property(get_b, set_b)

class A3(object):
def __init__(self):
self.b = None

class A4(object):
def get_b(self):
return None

b = property(get_b)

class A5(object):
b = None


# создаем иерархию со 128ю уровнями вложенности наследования
# атрибут 'b' будет у "самого базового" класса
# в использовать для доступа будет экзампляр "самого дочернего"

class A6(object):
b = None

for i in range(127):
A6 = type('A6', (A6,), {})

a6 = A6()

class A7(object):
def __getattr__(self, name):
return None

Время замерянно на Core i7-2630QM, 2Ghz. Ubuntu 11.10, python 2.7.2, 64bit

    +----------------------------------------------------------------------------------------+
| Операция | время нс | diff % | относительное | такты CPU |
| показывающая где берется | | | время | |
| aX.b | | | time/(a.b time)| |
|----------------------------------------------------------------------------------------|
| Global var access | 7.4 | 0 | 0.2 | 15 |
| Empty func call | 66.2 | 2 | 2.2 | 132 |
| Global var from func | 9.3 | 0 | 0.3 | 19 |
| Local var from func | 7.4 | 0 | 0.3 | 15 |
| int + int | 17.5 | 1 | 0.6 | 35 |
| A1.__getattribute__(a, 'b') | 178.9 | 1 | 6.0 | 358 |
| A2.b.__get__(a) data property | 139.4 | 0 | 4.7 | 279 |
| a3.__dict__['b'] | 30.0 | 1 | 1.0 | 60 |
| A4.b.__get__(a) property | 140.0 | 1 | 4.7 | 280 |
| A5.__dict__['b'] | 21.6 | 0 | 0.7 | 43 |
| A6.__dict__['b'] | 21.6 | 1 | 0.7 | 43 |
| A7.__getattr__(a, 'b') | 517.0 | 4 | 17.2 | 1034 |
| a.b.b | 54.1 | 2 | 1.8 | 108 |
| a.b.b.b.b | 103.5 | 2 | 3.4 | 207 |
| a....b (128) | 3428.5 | 1 | 114.0 | 6857 |
+----------------------------------------------------------------------------------------+

diff — максимальный разброс времени в % на 10 измерениях, при выброш
енных 2 крайних значениях.

В принципе все результаты, кроме странных «тормозов» __getattr__, ожидаемы. Одинаковое время для A5.__dict__[«b»] и A6.__dict__[«b»] связанно с кешированием привязки имени классового атрибута к классу, так что поиск по всей иерархии не производится. Тестирующий скрипт лежит тут. Тестирование производительности отдельная достаточно большая тема, которой я собираюсь посвятить отдельный пост в ближайшем будущем. Получить указанные тайминги и погрешность на уровне < 5% простым использованием timeit.timeit не получится.

Ссылки:
          github.com/koder-ua/python-lectures/blob/master/attribute_access.py
          github.com/koder-ua/python-lectures/raw/master/attribute.jpg
          docs.python.org/reference/datamodel.html#customizing-attribute-access

Исходники этого и других постов со скриптами лежат тут — github.com/koder-ua. При использовании их, пожалуйста, ссылайтесь на koder-ua.blogspot.com.

Автор: konstantin danilov

libvirt & Co. Облако "на коленке". Часть 1

Buzzword

Облако(cloud) это инфраструктура для управления виртуальными машинами. Агенты облака устанавливаются на железных серверах, превращая их единый мегасервер, которые используется для виртуализации. Облако должно уметь:

  • запускать группы виртуальных машин на базе загруженных в него образов
  • изменять образы виртуальных машин
  • управлять сетевой инфраструктурой — объединять виртуальные машины в ( возможно виртуальные ) локальные сети, настраивать правила доступа к этим сетям извне и доступ наружу из сетей
  • поддерживать остановку, приостановку и миграцию виртуалок
  • балансировать нагрузку на железные сервера
  • управлять местом на дисках
  • …………..

Предисловие

На сегодняшний день есть четыре основных облачных системы — перспективный и активно развиваемый openstack, рабочий но мало интересный из-за лицензии eucalyptus, совсем-совсем проприетарный VMware vCloud и очень-очень microsoft azure. Но это все «серьезные» облака, а как это часто бывает большие системы не удобно использовать на малых задачах. Я расскажу как управлять небольшими группами виртуальных машин «малой кровью». Впрочем openstack использует эти же утилиты, а все остальные узнают на чем основываются linux клауды.

Для описанных методик вам необходим Linux 2.6.26+ и процессор с поддержкой виртуализации. Проверить это можно следующими командами:

Hightlited/Raw

$ cat /proc/cpuinfo | egrep 'vmx|svm'
$ cat /proc/cpuinfo | egrep 'rvi|ept'

$ cat /proc/cpuinfo | egrep 'vmx|svm'
$ cat /proc/cpuinfo | egrep 'rvi|ept'

Если первая команда ничего не вывела — вам не повезло, аппаратной поддержки виртуализации у вас нет. Если обе команды выдали не пустой ответ — вам повезло вдвойне — в вашем процессоре есть поддержка виртуализации таблицы страниц — это значительно ускоряет работу с памятью, фактически выводя ее на уровень сырого железа.

Вложенная аппаратная виртуализация не поддерживается, т.е. если linux установлен в виртуальной машине, то описанные примеры работать не будут. Впрочем и те, кто запускает линукс в виртуалке и те, у кого нет поддержки виртуализации могут адаптировать эти примеры для использования xen c паравиртуализацией или lxc — эти техники не требуют аппаратной поддержки. В принципе ипользуемая libvirt имеет зачаточную поддержку windows, желающие могут попробовать и так.

Из других аппаратных требований желательно по-больше оперативной памяти (3Gb+) и быстрый диск (SSD). На магнитном жестком диске все будет работать, но некоторые наиболее интересные варианты организации виртульных образов заметно тормозят на дисковых операциях из-за большого количества разрозненных обращений.

Все примеры для Ubuntu 11.10, для других дистрибутивов нужно подправить обращения к пакетному менеджеру и пути к конфигам.

libvirt

Хотя формально libvirt называется библиотекой, но это целая инфраструктура для управления виртуальными машинами. Она включает:

  • libvirt-bin демон с внешним API, управляющий виртуальными машинами
  • libvirt — библиотека для доступа к демону
  • masqdns — dns/dhcp сервер, используемый совместно с iptables, vlan и бриджами для управлением виртуальными сетями
  • virsh — клиент командной строки

libvirt предоставляет почти унифицированный интерфейс для работы с различными гипервизорами — поддерживаются kvm, lxc, xen, vmware, hyper-v, openvz, и другие — в общем почти все, что еще шевелится. При этом libvirt не пытается подобрать общий знаменатель ко всем системам виртуализации, а предоставляет полный набор возможностей каждого гипервизора — просто не все конфигурации будут работать на всех системах виртуализаций.

Раскраска графа

Рассмотрим математическую модель, используемую для управления
светофорами на сложном перекрестке дорог. Мы должны создать программу, которая в
качестве входных данных использует множество всех допустимых поворотов на перекрестке (продолжение прямой дороги, проходящей через перекресток, также будем
считать «поворотом») и разбивает это множество на несколько групп так, чтобы все повороты в группе могли выполняться одновременно, не создавая проблем друг для друга. Затем мы сопоставим с каждой группой поворотов соответствующий режим работы
светофоров на перекрестке. Читать

Python 3: Импорт и юникод

Третий питон с рождения замечательно поддерживает юникод. Собственно говоря, это одна из самых заметных его особенностей.

Русские идентификаторы

Чуть меньше бросается в глаза тот факт, что идентификаторы тоже стали юникодными. Уважаемые читатели, если вы используете третий питон и недостаточно хорошо владеете английским — пишите по русски. Это выглядит гораздо лучше, чем убогое средство под названием «транслитерация». Оцените сами:

def функция(агрумент):
коэффициент = 5
return агрумент * коэффициент

Это на самом деле здорово!

Еще один не вполне очевидный момент: имена модулей тоже могут быть в юникоде:

from . import вспомогательный_модуль

Тоже выглядит неплохо, верно? Есть только одна небольшая проблема: это не всегда работает. Вернее, на Windows возможны неприятности. И не нужно заявлять, что вопросы, касающиеся самой популярной на сегодняшний день операционной системы — никого не волнуют. Подавляющее большинство разработчиков самого Питона Windows не используют — и тем не менее Питон обязан на ней работать, и работать хорошо.

Чтобы рассказать в чем вышла загвоздка — я должен немного погрузиться в детали.

Юникод в C API

В Python 2 немалая часть Python C API принимала char * там, где требовалась строка. Поскольку str и был последовательностью байт — сложностей не возникало.

При переносе кода на Python 3 нужно было с этим что-то делать: strстал юникодным типом, последовательностью символов.

Но в С нет удобного типа для unicode! Вернее, существует стандартный тип wchar_t, который обременен множеством проблем. Главные из них: в разных реализациях этот тип имеет различный размер: 16 бит для UCS-2 и 32 бита для UCS-4. К тому же Windows (о, снова она) не поддерживает UCS-2 в полной мере (UCS-4 не поддерживает совсем).

Хуже всего то, что на некоторых платформах этот wchar_t попросту не определен.

Таким образом, использовать wchar_t в Python C API нельзя.

Сам Питон вводит тип Py_UNICODE для этих целей. Но и тут не все гладко. Этот тип не входит в Limited API (PEP 384).

Кроме того, разработчики не хотели радикально заменить все char * на что-то другое.

Есть еще и вопрос практического удобства: ведь очень здорово писать

ret = PyObject_GetAttrString(obj, "attribute");

Для wchar_t все гораздо сложнее, далеко не все компиляторы поддерживают строковые юникодные константы.

В свете вышеописанных причин Python C API продолжает использовать char *, считая, что эти строки имеют кодировку UTF-8 если явно не указано иное. Т.е. прототипы функций C API выглядят как:

PyObject *
PyImport_ImportModuleLevel(char *name, PyObject *globals,
PyObject *locals, PyObject *fromlist,
int level);

Это — импорт модуля с именем name, которое передается как UTF-8строка, аналог питоновской функции __import__.

И эта функция — лишь верхушка используемого механизма. В процессе импорта вызываются довольно много внутренних закрытых функций — и везде используются переменные вроде char *name в качестве имен модулей. В кодировке UTF-8, еще раз напомню.

А ведь имя модуля транслируется в путь к файлу! А кодировака файловой системы может отличаться от UTF-8. Счастливые пользователи Linux давно об этом забыли — в подавляющем большинстве систем по умолчанию как кодировка пользователя (переменная окружения LANG) так и файловой системы установлены в UTF-8 и проблем нет совсем. Но в общем случае это не всегда так.

Кодировки по умолчанию

Чуть-чуть о кодировках. Для определения используемых по умолчанию кодировок в питоне существуют три функции: sys.getdefaultencoding, sys.getfilesystemencoding и locale.getpreferredencoding.

  • sys.getdefaultencoding() — кодировка по умолчанию, используемая в питоновских исходниках. Для третьего питона всегда равна UTF-8. Это — та самая кодировка, которую можно перекрыть написав в начале файла

    # -*- encoding: utf-8 -*-
  • sys.getfilesystemencoding() — кодировка файловой системы. Например, для

    f = open('path/to/file', 'r')

    значение 'path/to/file' имеет тип str (юникод). Лежащая в основе функция из clib имеет прототип

    int open(const char *pathname, int flags, mode_t mode);

    Значит, 'path/to/file' должен быть преобразован в char *используя к
    одировку sys.getfilesystemencoding(). Конечно, в Python C API есть специальные функции для этого.

  • locale.getpreferredencoding() — предпочтительная для пользователя кодировка. Она устанавливается в региональных настройках и к файловой системе прямого отношения не имеет.

Теперь снова вспомним нашу горячо любимую Windows.

locale.getpreferredencoding() возвращает 'cp1251' — Windows настроена на русский язык. Кодировка для консоли (sys.stdout.encoding) другая, это 'cp866' — что добавляет сумбура в и без того запутанную проблему. Ну да ладно, не будем отвлекаться.

sys.getfilesystemencoding() возвращает 'mbcs'. И вот здесь начинаются основные чудеса. Обратите внимание, mbcs — это не cp1251. Равно как и не cp1252 или какая другая кодировка. mbcs — это нечто совершенно особенное!

Multibyte character set (кодировка MBCS)

При преобразовании mbcs -> unicode используется кодировка из locale.getpreferredencoding(), преобразование однозначное и проблем не вызывает.

Для обратного преобразования unicode -> mbcs тоже используется locale.getpreferredencoding() (cp1251 в нашем случае). Но cp1251 не может описать любой юникодный символ. А mbcs — хитрый и коварный. Если для символа не существует точного преобразования — используется ближайший похожий по начертанию.

Это непросто понять без примера. Давайте возьмем французское слово comédie и попробуем преобразовать его в mbcs, имея руский язык cp1251 в настройках по умолчанию.

Возьмем Python 3.1:

>>> b = b'comxc3xa9die'
>>> s = b.decode('utf8')
>>> s.encode('mbcs')
b'comedie'

Посмотрите, какая прелесть! Для символа é в русской раскладке cp1251 нет подходящего аналога. Но ведь английская буква e так похожа: нужно лишь убрать умляут (англ. umlaut, французы зовут этот знак accent aigu). Так и получили преобразование comédie -> comedie без единой ошибки.

А теперь представьте, что это — имя файла. Результат будет следующим: файл на диске есть, и так как в Windows файловая система юникодная, имя файла будет записано правильно, по французски. Но преобразование unicode -> mbcs даст несколько другое имя, которого на диске нет.

В результате получается изумительная по своей красоте ситуация:

f = open('comédie', 'r')

будет говорить, что файла нет — а на самом деле вот же он, красавец!

Справедливости ради нужно упомянуть, что в Python 3.2 поведение mbcsнемного поменялось, и 'comédie'.encode('mbcs') вызовет UnicodeEncodeError. Дело в том, что mbcs стал использовать режим strict по умолчанию. Чтобы повторить функциональность 3.1 следует указывать режим replace: 'comédie'.encode('mbcs', 'replace')

Юникодная файловая система

С mbcs мы разобрались и выяснили, что для работы с файловой системой эта кодировка в общем случае непригодна. Т.е. если я буду использовать русские имена файлов на русской Windows — всё будет хорошо. Но открыть этот файл у американца или голландца не выйдет. Что же делать?

В Windows помимо open есть еще и функция

FILE *_wfopen(const wchar_t *filename, const wchar_t *mode);

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

Значит, нужно делать следующее: для Windows использовать юникодные версии функций работы с файлами, а для всех остальных операционных систем применять .encode(sys.getfilesystemencoding()).

Реализация модуля io начиная с версии 3.1 так и поступает.

И снова импорт русских названий

Всё отлично за одним маленьким исключением — механизм импорта не использует io! Исторически сложилось так, что имя импортируемого модуля довольно быстро преобразовывается в sys.getfilesystemencoding() (с возможными ошибками и потерями, о которых я писал выше) и в таком виде пронизывает весь очень непростой и громоздкий код, чтобы попасть в функции стандартной библиотеки C.

Добавьте к этому довольно большой объем платформозависимого кода (на Маке все работает совсем не так, как на Linux) и проблему обратной совместимости (даже после объявления части API устаревшей она должна поддерживаться как минимум в двух следующих выпусках) — и вы сможете представить сложность и объемность задачи.

Так вот, после трехлетнего труда (с небольшими перерывами, естественно — это же добровольный некоммерческий Open Source) Victor Stinner завершил требуемое нелегкое преобразование. Дов
ольно незаметный, но очень важный шаг!

Файловые пути стали храниться в PyObject* (на самом деле это, конечно, strPyUnicodeObject), работающая с ними часть C APIимеет суффикс Object. Например:

PyObject *
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
PyObject *locals, PyObject *fromlist,
int level);

Сравните с PyImport_ImportModuleLevel. Все функции из старого APIстали тонкими обертками над новыми вариантами. Так, PyImport_ImportModuleLevel создает PyObject из name и вызывает PyImport_ImportModuleLevelObject.

Эти старые функции оставлены для сохранения обратной совместимости, сам Питон их уже не использует.

Если быть честным, именно Windows поддержка чуть-чуть не готова — но до выхода Python 3.3 еще очень много времени. Достаточно, чтобы закончить работу и навести полный порядок.

Заключение

Я написал этот довольно длинный текст преследуя несколько целей:

  • Пожалуй, главная из них — показать, насколько порой незначительные внешне изменения способны перевернуть внутреннюю реализацию, и как нелегко их проделать не сломав того, что уже отлично работает пятнадцать лет.

  • Вторая — продемонстрировать, как работают кодировки применительно к файловой системе.

  • Третья — напомнить, что можно использовать русские буквы в идентификаторах. Комментарии излишни.

  • И, наконец, очень хотелось отметить завершение отлично выполненной работы, которая делает Питон немного лучше.

Автор: Andrew Svetlov

Python — генерация случайного числа с заданной вероятностью

Представим, что нам потребовалось создать на Питоне генератор случайных чисел, который выводит 1 или 0, причем 1 он выводит с вероятностью 0,2.
Это можно сделать следующим образом:

>>> import random
>>> for i in range(20):
…     x = random.random()
…     if x <= 0.2:
…             print 1
…     else:
…             print 0

0
0
0
0
0
0
0
0
0
0
1
1
0
0
0
0
0
0
0
0

Что мы здесь сделали?
Во первых, первой строкой подключили модуль random, затем сгенерировали случайное число с плавающей точкой в пределах от 0 до 1. В зависимости от того, меньше это число 0.2, или больше, мы вывели 1 или 0. Таким образом, задачу можно считать выполненной.

Автор: AlexWinner
Дата публикации: 2011-01-31T06:58:00.000-08:00