skip to navigation
skip to content

feed icon RSS хранилка

class Vector(object)

Публикувано на 11.04.2008 23:19 от Стефан
Последна промяна на 18.04.2008 21:51
Тази публикация е от предишно издание на курса, моля не разчитайте на актуалността на информацията.

В сряда говорехме за обектно-ориентирано програмиране. Едно от нещата, които показахме е клас за вектор. Започнахме с идеята да ви покажем някаква основа и да я надграждаме по време на лекцията. Сега ще ви покажа целия код, с няколко добавки и разширения. Ще добавя и моите коментари към кода – какъв път съм изминал за да стигна до него. Познавайки чудесата на модерните хроматични технологии, Ники и Митьо ще добавят собствената си мъдрост към него.

Цел

Искаме да направим клас за тримерен вектор. Трябва да можем да събираме/изваждаме вектори, да ги умножаваме/делим с число и да правим векторно и скаларно произведение. Трябва да може и да нормализираме вектори (дължина единица).

Реализация

Първото решение което направих, е че векторите ще бъдат immutable – т.е. след като веднъж са създадени, няма да може да бъдат променяни. „Няма да може“ е условно, разбира се, понеже малко неща в Python са забранени. Но ако ползвате само публичните методи и оператори, няма начин да мутирате обекта – всички операции връщат нов.

Второто решение е вътрешното представяне на обекта. Избрах да пазя координатите му в n-орка. Алтернативата беше да ги пазя в self.x, self.y и self.z. Предпочетох първото, понеже на много места ми се налага да обходя координатите и три атрибута щеше да ми е неудобно.

Въоръжен с тези решения, стигнах до кода по-долу. Добавил съм коментари тук-там, които да ви улеснят, ако не сте опитни Python програмисти и ползвах по-просторно форматиране, отколкото щях да използвам за собствен код. Има и бележки, които да коментирам по-надолу.

class Vector(object):
    def __init__(self, x, y, z):
        self._coords = tuple(map(float, [x, y, z])) 1

    def __str__(self): # str(vector)
        return "Vector(%s, %s, %s)" % self._coords

    __repr__ = __str__   2

    def __getitem__(self, i): # vector[i]   3
        return self._coords[i]

    def __getattr__(self, attr): # vector.attr   4
        names = ['x', 'y', 'z']
        if attr in names:
            return self[names.index(attr)]
        else:
            raise AttributeError, attr

    def __eq__(self, other): # vector == other
        return self._coords == other._coords

    def __ne__(self, other): # vector != other
        return not self == other   5

    def __add__(self, other): # vector + other
        return Vector(*[a + b for a, b in zip(self, other)])

    def __neg__(self): # -vector
        return Vector(*[-c for c in self])

    def __sub__(self, other): # vector - other
        return self + (-other)   6

    def __mul__(self, other): # vector * other   7
        if isinstance(other, Vector):
            return Vector(
                self.y * other.z - self.z * other.y,
                self.z * other.x - self.x * other.z,
                self.x * other.y - self.y * other.x)
        else:
            return Vector(*[_ * other for _ in self])

    def __rmul__(self, other): # other * vector   8
        return self * other

    def __div__(self, other): # vector / other
        return Vector(*[_ / other for _ in self])

    def __xor__(self, other): # vector ^ other
        return sum([a * b for a, b in zip(self, other)])   9

    def length(self):
        return sum([_ ** 2 for _ in self]) ** 0.5

    def normalized(self):
        length = self.length()
        return Vector(*[_ / length for _ in self]) if length else self

Ето и как може да го използвате.

v = Vector
a, b = Vector(0, 6, 0), Vector(4, 0, 0)
a /= 4
b /= 3
c = (a * b).normalized() - Vector(1, 1, 0)
c *= -1
print c # Vector(1.0, 1.0, 1.0)

Точки из кода

1 В конструктура обръщам всичко до float. Така ще съм сигурен, че няма да получавам странни резултати при деление.

2 На този ред просто правя името __repr__ да сочи към същата функция, към която сочи името __str__. Кратко и ясно.

Но защо работи? Всички имена, дефинирани в class блок, са или статични променливи, или функции. Когато имате инстанция и достъпите атрибут, който е функция, Python ви връща нова функция, която е дефинирана, но с фиксиран първи аргумент. По тази причина, Vector.cross(foo, other) и foo.cross(other) са еквивалентни.

Тук бих действал съвсем малко по-различно — бих дефинирал __repr__, вместо __str__, тъй като ми се струва по-вероятно да променя някога какво връща __str__. Освен това имах някои задни мисли относно употребата на "Vector(...)" срещу self.__class__.__name__ + "(...)", но долните черти ми идват в повече.

3 Предефинирам [] за да може моя вектор да изглежда като списък. За потребители на класа няма твърде голямо значение, но за мен е много удобен, понеже мога да правя for c in self вместо for c in self._coords.

Първата ми идея за класа беше направо да наследим list, но след кратък замисъл се отказах. Ще получим много ненужна и даже вредна функционалност и предефинирани оператори, а ще спечелим само [] и циклене с for.

4 Това пък го правя за да мога да пиша vector.x, vector.y и vector.z, въпреки че вътрешното ми представяне е n-орка. Обърнете внимание, че ако името на атрибутите не е някое от трите, хвърлям грешка, без много да обяснявам. Така, ако потребителят пробва да извика друг атрибут (vector.foo), ще си получи очакваното съобщение за грешка. Моите три атрибута се намесват сравнително прозрачно.

Също така, обърнете внимание, че можех да напиша кода така:

        if attr == 'x':
            return self[0]
        elif attr == 'y':
            return self[1]
        elif attr == 'z':
            return self[2]
        else:
            return object.__getattr__(self, attr)

Или дори така:

        names = {'x':0, 'y':1, 'z':2}
        if attr in names:
            return self[names[attr]]
        else:
            return object.__getattr__(self, attr)

Вместо това, успях да изразя нещата по-кратко и с по-малко повторение. Вярвам, че колкото по-малко излишна информация се съдържа в един код (имена на променливи, mapping-и които могат да се съобразят от реда в списъка), толкова по-качествен (красив, лесен са разбиране и с по-малко бъгове) е той.

Много съм съгласен със Стефан по тази точка. Е, не бих написал списък, ами n-торка: names = 'x', 'y', 'z'. Или, ако имах по-дълги думи: 'hyku ramatahatta evil'.split().

5 Тук можех да извикам и self._coords != other._coords, но предпочетох да не го правя. Защо? Ако го бях направил така, операторът != щеше да зависи от вътрешното представяне на вектора. Ако реша да променям последното, щеше да се наложи да променя и двата оператора. Вместо това реших да кажа „операцията е != е обратното на ==“, без да ме интересува как е имплементирана ==.

Забележете още нещо интересно – така съсредоточавам познанията върху вътрешната имплементация на по-малко места, а другите ги изразявам чрез тези места. Същото важи и за итерацията през вектор (##3.). Ако реша да променя вътрешното представяне от n-орка на три атрибута, нужно е само да променя само __getitem__.

6 Тук виждате нещо подобно на горното. Изразявам __sub__ (изваждане) чрез __neg__ (унарен минус) и __add__ (събиране). Отново, този код ще продължи да функционира независимо от вътрешното представяне на вектора.

7 Тук има няколко коментара да се направят.

Първото е, избрах да ползвам * както за умножение на вектор с число, така и за векторно произведение. Резултата и на двете операции е вектор и неща като v1 * 2 * v2 * 3 са далеч по-предвидими. За сметка на това, резултатът от едно скаларно произведение щеше да е число и тогава горният израз щеше да има по-странно тълкование.

Второ, проверявам дали съм получил вектор с isinstance(self, Vector), а не с type(self) == Vector. По този начин ще работя и с наследник на вектор. Също така, не проверявам типа на other, ако е различен от вектор –- просто приемам, че ще дефинира * по хубав начин, какъвто е случаят със стандартните типове -– int, long, float.

8 Когато извиквате 4 * Vector(1, 2, 3), произведението се изпълнява върху числото – int.__mul__(self, other). Все пак, тази операция изглежда има смисъл да работи за вектори. За да го постигнете, трябва да дефинирате оператора __rmul__, който се изпълнява в този случай. Детайли как работи, може да намерите в презентациите или в документацията на Python. Отново, обърнете внимание, че изразявам операцията чрез вече дефинирана операция, без логика базирана на вътрешното представяне.

От кода не ми става ясно, че other никога няма да бъде вектор. Първоначално се заблудих, че ще дава грешни резултати, тъй като умножението на вектори е антирефлексивно. Аз бих преименувал other на scalar и бих сложил кратък коментар защо не може да се викне с вектор.

Във връзка с ##5. и ##6., има идея да се реализира __div__ чрез __mul__ като просто връщаме self * (1.0 / other), но тук трябва да се отбележи, че има опасност от загуба на точност заради особеностите на типа float.

9 За скаларно произведение реших да ползвам ^. Не е твърде смислено, но върши работа.

Други коментари

С риск да се повторя, може да забележите, че не обичам да повтарям код. Не обичам и да повтарям пасажи, които си приличат. Например, предпочетох да напиша sum([a * b for a, b in zip(self, other)]) пред self.x * other.x + self.y * other.y + self.z * other.z. Първото се чете по-близко до „сумата на произведенията на коефициентите“, отколкото второто.

Разбира се, това е концепция с която не трябва да се прекалява. На някои места тя е по-трудно четима и изразява идеята по-завъртяно. Иронично, това е един пример където трябваше да предпочета втория код пред първия, понеже е по-близък до това, което намирате в учебниците по математика. Друг такъв пример е сумата – все пак предпочетох да илюстрирам още веднъж как нещата могат да се изразяват със способи като map, zip и list comprehension.

Друго интересно наблюдение — как ползвам подчертавката. Навсякъде, където не мога да сложа смислено име на променлива (име, което би направило програмата по-разбираема) предпочитам да ползвам _. По-кратко и по-ясно е. Разбира се, важно е да го ползвате в минимална област -– което за мен лично се свежда до lambda изрази и list comprehension-и. Не бих го ползвал за име на итерационна променлива във for. И забележете, че в отрицанието ползвам името c. Просто -_ щеше да изглежда странно.

Не бих ползвал _, ако стойността се използва по някакъв начин в резултата, както е тук. Бих ползвал coord: [coord * other for coord in self]

Споделям мнението на Ники за „_“ — [coord / other for coord in self] ми хармонира много повече. И още нещо — с оглед на това, че Vector по идея е immutable, бих реализирал self.length като поле (property), а не метод на класа.

Ако имате коментари и въпроси, добре са дошли в тази тема на форумите.