Хороший стиль в объектах Python
Большая часть моего программирования до Python была в С++ или Matlab. У меня нет степени в CS (почти закончил аспирантуру по физике), но сделал несколько курсов и хорошее количество фактического программирования. Теперь я беру курс алгоритмов на Coursera (отличный курс, кстати, с профессором из Стэнфорда). Я решил выполнить домашние задания на Python. Тем не менее, иногда я нахожу, что хочу, чтобы язык не так легко поддерживал язык. Я очень привык создавать классы и объекты для вещей на С++ только для группировки данных (т.е. Когда методов нет). Однако в Python, где вы можете добавлять поля "на лету", то, что я в основном в конечном итоге желаю, - это структуры Matlab. Я думаю, что это, возможно, знак, что я не использую хороший стиль и не делаю вещи "путинским" способом.
Под моей реализацией структуры данных на основе объединения (для алгоритма Крускаля). Хотя реализация относительно короткая и работает хорошо (проверка ошибок невелика), есть несколько нечетных точек. Например, мой код предполагает, что данные, первоначально переданные в union-find, представляют собой список объектов. Однако, если вместо этого передается список явных фрагментов данных (т.е. Список целых чисел), код не работает. Есть ли еще более ясный, более путинский способ реализовать это? Я попытался это сделать Google, но большинство примеров очень просты и связаны скорее с процедурным кодом (т.е. "Правильным" способом сделать цикл for в python).
class UnionFind:
def __init__(self,data):
self.data = data
for d in self.data:
d.size = 1
d.leader = d
d.next = None
d.last = d
def find(self,element):
return element.leader
def union(self,leader1,leader2):
if leader1.size >= leader2.size:
newleader = leader1
oldleader = leader2
else:
newleader = leader2
oldleader = leader1
newleader.size = leader1.size + leader2.size
d = oldleader
while d != None:
d.leader = newleader
d = d.next
newleader.last.next = oldleader
newleader.last = oldleader.last
del(oldleader.size)
del(oldleader.last)
Ответы
Ответ 1
Вообще говоря, такое поведение Python означает, что вы пытаетесь заставить свой код не заботиться о том, что ему дано, по крайней мере, не больше, чем это действительно нужно.
Возьмем ваш конкретный пример алгоритма объединения-поиска. Единственное, что алгоритм объединения-находки на самом деле делает со значениями, которые вы передаете ему, сравнивает их для равенства. Поэтому, чтобы сделать обычно полезный класс UnionFind
, ваш код не должен полагаться на значения, которые он получает, имея какое-либо поведение, отличное от проверки равенства. В частности, вы не должны полагаться на возможность присвоить произвольные атрибуты значениям.
То, как я предлагаю обойти это, состоит в том, чтобы UnionFind
использовать объекты-оболочки, которые содержат заданные значения и любые атрибуты, необходимые для работы алгоритма. Вы можете использовать namedtuple
, как было предложено другим ответом, или создать небольшой класс-оболочку. Когда элемент добавляется в UnionFind
, вы сначала переносите его в один из этих объектов и используете объект-обертку для хранения атрибутов leader
, size
и т.д. Единственный раз, когда вы получаете доступ к обернутой вещи, - это чтобы проверить, равно ли оно другому значению.
На практике, по крайней мере в этом случае, должно быть безопасно предположить, что ваши значения хешируются, поэтому вы можете использовать их в качестве ключей в словаре Python, чтобы найти объект-оболочку, соответствующий заданному значению. Конечно, не все объекты в Python обязательно хешируются, но те, которые не являются относительно редкими, и будет намного больше работать, чтобы создать структуру данных, способную обрабатывать их.
Ответ 2
Чем больше pythonic, тем лучше избегать утомительных объектов, если вам это не нужно.
class UnionFind(object):
def __init__(self, members=10, data=None):
"""union-find data structure for Kruskal algorithm
members are ignored if data is provided
"""
if not data:
self.data = [self.default_data() for i in range(members)]
for d in self.data:
d.size = 1
d.leader = d
d.next = None
d.last = d
else:
self.data = data
def default_data(self):
"""create a starting point for data"""
return Data(**{'last': None, 'leader':None, 'next': None, 'size': 1})
def find(self, element):
return element.leader
def union(self, leader1, leader2):
if leader2.leader is leader1:
return
if leader1.size >= leader2.size:
newleader = leader1
oldleader = leader2
else:
newleader = leader2
oldleader = leader1
newleader.size = leader1.size + leader2.size
d = oldleader
while d is not None:
d.leader = newleader
d = d.next
newleader.last.next = oldleader
newleader.last = oldleader.last
oldleader.size = 0
oldleader.last = None
class Data(object):
def __init__(self, **data_dict):
"""convert a data member dict into an object"""
self.__dict__.update(**data_dict)
Ответ 3
Чтобы проверить, соответствует ли аргумент ожидаемому типу, используйте встроенную функцию isinstance()
:
if not isinstance(leader1, UnionFind):
raise ValueError('leader1 must be a UnionFind instance')
Кроме того, хорошая привычка добавлять docstrings функции, классы и функции-члены. Такая docstring для функции или метода должна описывать, что она делает, какие аргументы должны быть переданы ей и, если применимо, то, что возвращается, и какие исключения могут быть подняты.
Ответ 4
Один из вариантов - использовать словари для хранения необходимой информации о элементе данных, а не атрибуты на элементе напрямую. Например, вместо обращения к d.size
вы можете обратиться к size[d]
(где size
- экземпляр dict
). Это требует, чтобы ваши элементы данных были хешируемыми, но им не нужно разрешать назначать атрибуты на них.
Вот простой перевод вашего текущего кода для использования этого стиля:
class UnionFind:
def __init__(self,data):
self.data = data
self.size = {d:1 for d in data}
self.leader = {d:d for d in data}
self.next = {d:None for d in data}
self.last = {d:d for d in data}
def find(self,element):
return self.leader[element]
def union(self,leader1,leader2):
if self.size[leader1] >= self.size[leader2]:
newleader = leader1
oldleader = leader2
else:
newleader = leader2
oldleader = leader1
self.size[newleader] = self.size[leader1] + self.size[leader2]
d = oldleader
while d != None:
self.leader[d] = newleader
d = self.next[d]
self.next[self.last[newleader]] = oldleader
self.last[newleader] = self.last[oldleader]
Минимальный тестовый пример:
>>> uf = UnionFind(list(range(100)))
>>> uf.find(10)
10
>>> uf.find(20)
20
>>> uf.union(10,20)
>>> uf.find(10)
10
>>> uf.find(20)
10
Помимо этого, вы также можете немного изменить свою реализацию, чтобы потребовать меньше инициализации. Здесь версия, которая не выполняет никакой инициализации (ей даже не нужно знать набор данных, над которыми она будет работать). Он использует сжатие путей и объединение по рангу, а не всегда поддерживает актуальное значение leader
для всех членов набора. Он должен быть асимптотически быстрее вашего текущего кода, особенно если вы делаете много союзов:
class UnionFind:
def __init__(self):
self.rank = {}
self.parent = {}
def find(self, element):
if element not in self.parent: # leader elements are not in `parent` dict
return element
leader = self.find(self.parent[element]) # search recursively
self.parent[element] = leader # compress path by saving leader as parent
return leader
def union(self, leader1, leader2):
rank1 = self.rank.get(leader1,1)
rank2 = self.rank.get(leader2,1)
if rank1 > rank2: # union by rank
self.parent[leader2] = leader1
elif rank2 > rank1:
self.parent[leader1] = leader2
else: # ranks are equal
self.parent[leader2] = leader1 # favor leader1 arbitrarily
self.rank[leader1] = rank1+1 # increment rank
Ответ 5
Я предполагаю, что здесь проблемы с отступом - это просто ошибки при вводе кода в SO. Не могли бы вы создать подкласс простого, встроенного типа данных? Например, вы можете создать подкласс класса данных списка, поставив его в круглые скобки:
class UnionFind(list):
'''extends list object'''