Чистыйкод дляпродолжающи х
Скачать 7.85 Mb.
|
import wizcoinTypeError: unsupported operand type(s) for +: 'WizCoin' and 'WizCoin' Вместо того чтобы писать метод addWizCoin() для класса WizCoin , вы можете ис- пользовать dunder-метод __add__() и заставить объекты WizCoin работать с опера- тором + . Добавьте следующий фрагмент в конец файла wizcoin.py : 370 Глава 17.ООП в Python: свойства и dunder-методы --snip-- def __add__(self, other): ❶ """Суммирует денежные величины двух объектов WizCoin.""" if not isinstance(other, WizCoin): ❷ return NotImplemented return WizCoin(other.galleons + self.galleons, other.sickles + ❸ self.sickles, other.knuts + self.knuts) Когда объект WizCoin стоит в левой части оператора + , Python вызывает метод __add__() ❶ и передает значение в правой части оператора + в параметре other (Параметру можно назначить любое имя, но традиционно используется имя other .) Помните, что методу __add__() можно передать объект любого типа, поэтому в ме- тод необходимо включить проверку типа ❷ . Например, бессмысленно прибавлять целое число или число с плавающей точкой к объекту WizCoin , потому что мы не знаем, к какому атрибуту его следует прибавить — galleons , sickles или knuts Метод __add__() создает новый объект WizCoin с атрибутами, равными сумме galleons , sickles и knuts объектов self и other ❸ . Так как все три атрибута содержат целые числа, их можно суммировать оператором + . Теперь оператор + для класса WizCoin перегружен, и его можно использовать с объектами WizCoin Перегрузка оператора + позволяет создавать более понятный код. Например, вве- дите следующий фрагмент в интерактивной оболочке: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # Создает объект WizCoin. >>> tipJar = wizcoin.WizCoin(0, 0, 37) # Создает другой объект WizCoin. >>> purse + tipJar # Создает новый объект WizCoin с суммой. WizCoin(2, 5, 47) Если в other передается объект неправильного типа, dunder-метод должен не вы- давать исключение, а возвращать встроенное значение NotImplemented . Например, в следующем фрагменте в other передается целое число: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse + 42 # Объекты WizCoin и целые числа не могут суммироваться. Traceback (most recent call last): File " TypeError: unsupported operand type(s) for +: 'WizCoin' and 'int' Возвращение NotImplemented приказывает Python попробовать вызвать другие ме- тоды для выполнения этой операции (за подробностями обращайтесь к подразделу «Отраженные числовые dunder-методы» этой главы). Во внутренней реализации Python вызывает метод __add__() со значением 42 для параметра other , но этот метод тоже возвращает NotImplemented , что заставляет Python выдать исключение TypeError Dunder-методы Python 371 И хотя операции прибавления или вычитания целых чисел из объектов WizCoin не имеют смысла, будет разумно разрешить умножение объектов WizCoin на положи- тельные целые числа; для этого определяется dunder-метод __mul__() . Добавьте следующий фрагмент в конец файла wizcoin.py : --snip-- def __mul__(self, other): """Умножает количество монет на неотрицательное целое число.""" if not isinstance(other, int): return NotImplemented if other < 0: # Умножение на отрицательное целое число приведет # к отрицательному количеству монет, что недопустимо. raise WizCoinException('cannot multiply with negative integers') return WizCoin(self.galleons * other, self.sickles * other, self.knuts * other) Этот метод __mul__() позволяет умножать объекты WizCoin на положительные це- лые числа. Если other является целым числом, то это тип данных, которые ожидает получить метод __mul__() , и возвращать NotImplemented не нужно. Но если целое число — отрицательное, то умножение объекта WizCoin на него приведет к отрица- тельному количеству монет в объекте WizCoin . Так как это противоречит нашему подходу к проектированию класса, мы выдаем исключение WizCoinException с со- держательным сообщением об ошибке. ПРИМЕЧАНИЕ Не изменяйте объект self в математическом dunder-методе. Метод всегда должен созда- вать и возвращать новый объект. Оператор + и другие числовые операторы всегда должны давать в результате новый объект, вместо того чтобы изменять значение объекта на месте. Чтобы увидеть dunder-метод __mul__() в действии, введите следующий фрагмент в интерактивной оболочке: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # Создать объект WizCoin. >>> purse * 10 # Умножить объект WizCoin на целое число. WizCoin(20, 50, 100) >>> purse * -2 # Умножение на отрицательное целое число приводит к ошибке. Traceback (most recent call last): File " File "C:\Users\Al\Desktop\wizcoin.py", line 86, in __mul__ raise WizCoinException('cannot multiply with negative integers') wizcoin.WizCoinException: cannot multiply with negative integers В табл. 17.1 приведен полный список числовых dunder-методов. Как правило, вам не требуется полный список этих методов для ваших классов. Вы сами решаете, какие методы для вас актуальны. 372 Глава 17.ООП в Python: свойства и dunder-методы Таблица 17.1. Числовые dunder-методы Dunder-метод Операция Оператор или встроенная функция __add__() Сложение + __sub__() Вычитание - __mul__() Умножение * __matmul__() Матричное умножение (в Python 3.5 и выше) @ __truediv__() Деление / __floordiv__() Целочисленное деление // __mod__() Остаток % __divmod__() Деление с остатком divmod() __pow__() Возведение в степень **, pow() __lshift__() Сдвиг влево >> __rshift__() Сдвиг вправо << __and__() Поразрядная операция AND & __or__() Поразрядная операция OR | __xor__() Поразрядная исключающая опе- рация OR ^ __neg__() Отрицание Унарный - , как в -42 __pos__() Тождество Унарный + , как в +42 __abs__() Абсолютное значение (модуль) abs() __invert__() Поразрядное инвертирование __complex__() Комплексная форма числа complex() __int__() Целая форма числа int() __float__() Форма числа с плавающей точкой float() __bool__() Логическая форма bool() __round__() Округление round() __trunc__() Целая часть math.trunc() __floor__() Округление в меньшую сторону math.floor() __ceil__() Округление в большую сторону math.ceil() Dunder-методы Python 373 Некоторые из этих методов актуальны для нашего класса WizCoin . Попробуйте напи- сать собственные реализации методов __sub__() , __pow__() , __int__() , __float__() и __bool__() . Пример реализации доступен на https://autbor.com/wizcoinfull. Полное описание числовых dunder-методов в документации Python доступно на https:// docs.python.org/3/reference/datamodel.html#emulating-numeric-types. Числовые dunder-методы позволяют использовать объекты ваших классов со встроенными математическими операторами Python. Если вы пишете методы с именами вида multiplyBy() , convertToInt() и т. д., описывающими задачу, которая выполняется существующим оператором или встроенной функцией, используйте числовые dunder-методы (а также отраженные dunder-методы и dunder-методы присваивания на месте, описанные в следующих двух разделах). Отраженные числовые dunder-методы Python вызывает числовые dunder-методы, когда объект находится в левой части математического оператора. Но если объект располагается в правой части мате- матического оператора, то вызываются отраженные числовые dunder-методы (их также называют обратными числовыми dunder-методами). Отраженные числовые dunder-методы полезны, потому что программисты, ис- пользующие ваш класс, не всегда будут записывать объект в левой части оператора, что может привести к непредвиденному поведению. Для примера рассмотрим, что произойдет, если purse содержит объект WizCoin , а Python вычисляет выражение 2 * purse , когда purse находится в правой части оператора. 1. Так как 2 является целым числом, вызывается метод __mul__() класса int , которому в параметре other передается purse 2. Метод __mul__() класса int не знает, как обрабатывать объекты WizCoin , по- этому он возвращает NotImplemented 3. Пока Python не выдает ошибку TypeError . Так как purse содержит объект WizCoin , вызывается метод __rmul__() класса WizCoin , которому в параметре other передается 2 4. Если __rmul__() возвращает NotImplemented , Python выдает ошибку TypeError . В противном случае __rmul__() возвращает объект, полученный в результате вычисления выражения 2 * purse А вот выражение purse * 2 , в котором purse находится в левой части оператора, работает иначе. 1. Так как purse содержит объект WizCoin , вызывается метод __mul__() класса WizCoin , которому в параметре other передается 2 374 Глава 17.ООП в Python: свойства и dunder-методы 2. Метод __mul__() создает новый объект WizCoin и возвращает его. 3. Этот возвращенный объект — то, что возвращает выражение purse * 2 Числовые dunder-методы и отраженные числовые dunder-методы имеют одина- ковый код, если они обладают свойством коммутативности. Коммутативные операции (такие как сложение) дают одинаковый результат при записи в прямом и обратном направлении: 3 + 2 — то же самое, что 2 + 3 Но существуют и другие операции, которые коммутативными не являются: 3 — 2 и 2 — 3 дают разные результаты. Любая коммутативная операция может просто вы- зывать исходный числовой dunder-метод каждый раз, когда вызывается отраженный числовой dunder-метод. Например, добавьте следующий фрагмент в конец файла wizcoin.py , чтобы определить отраженный числовой dunder-метод для операции умножения: --snip-- def __rmul__(self, other): """Умножает количество монет на неотрицательное целое число.""" return self.__mul__(other) Умножение целого числа на объект WizCoin коммутативно: 2 * purse — то же самое, что purse * 2 . Вместо того чтобы копировать и вставлять код из __mul__() , мы про- сто вызываем self.__mul__() и передаем параметр other После обновления файла wizcoin.py проверьте, как работает отраженный dunder- метод умножения. Для этого введите следующий фрагмент в интерактивную обо- лочку: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse * 10 # Вызывает __mul__() с параметром `other`, равным 10. WizCoin(20, 50, 100) >>> 10 * purse # Вызывает __rmul__() с параметром `other`, равным 10. WizCoin(20, 50, 100) Помните, что в выражении 10 * purse Python сначала вызывает метод __mul__() класса int , чтобы узнать, могут ли целые числа умножаться на объекты WizCoin Конечно, встроенный класс int ничего не знает о создаваемых нами классах, по- этому он возвращает NotImplemented . Это значение сигнализирует Python о том, чтобы следующим для обработки операции был вызван метод __rmul__() класса WizCoin (если он существует). Если вызовы __mul__() класса int и __rmul__() класса WizCoin вернут NotImplemented , Python выдает исключение TypeError Только объекты WizCoin способны суммироваться друг с другом. Это гарантирует, что метод __add__() первого объекта WizCoin обработает операцию, поэтому реали- зовать __radd__() не нужно. Например, в выражении purse + tipJar метод __add__() Dunder-методы Python 375 для объекта purse вызывается с передачей tipJar в параметре other . Так как этот вызов не возвращает NotImplemented , Python не пытается вызвать метод __radd__() объекта tipJar с передачей purse в параметре other В табл. 17.2 приведен полный список доступных отраженных dunder-методов. Таблица 17.2. Отраженные числовые dunder-методы Dunder-метод Операция Оператор или встроенная функция __radd__() Сложение + __rsub__() Вычитание - __rmul__() Умножение * __rmatmul__() Матричное умножение (в Python 3.5 и выше) @ __rtruediv__() Деление / __rfloordiv__() Целочисленное деление // __rmod__() Остаток % __rdivmod__() Деление с остатком divmod() __rpow__() Возведение в степень **, pow() __rlshift__() Сдвиг влево >> __rrshift__() Сдвиг вправо << __rand__() Поразрядная операция AND & __ror__() Поразрядная операция OR | __rxor__() Поразрядная исключающая операция OR ^ Полное описание отраженных dunder-методов доступно в документации Python на https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types. Dunder-методы присваивания на месте (in-place) Числовые и отраженные dunder-методы всегда создают новые объекты (вместо из- менения объектов на месте). Dunder-методы присваивания на месте, вызываемые расширенными операторами присваивания (такими как += и *= ), изменяют объект на месте без создания нового объекта. (У этого правила есть исключение, о котором я расскажу в конце раздела.) Имена таких dunder-методов начинаются с i (напри- мер, __iadd__() и __imul__() для операторов += и *= соответственно). 376 Глава 17.ООП в Python: свойства и dunder-методы Например, при выполнении кода Python purse *= 2 мы не предполагаем, что метод __imul__() класса WizCoin создаст и вернет новый объект WizCoin с вдвое большим количеством монет, а затем присвоит его переменной purse . Вместо этого метод __imul__() изменяет существующий объект WizCoin в purse , чтобы он содержал вдвое большее количество монет. Это тонкое, но важное отличие, которое необхо- димо учитывать, если ваши классы должны перегружать расширенные операторы присваивания. Наши объекты WizCoin уже перегружают операторы + и * , поэтому определим dunder-методы __iadd__() и __imul__() , чтобы они также перегружали операторы += и *= . В выражениях purse += tipJar и purse *= 2 вызываются методы __iadd__() и __imul__() , при этом в параметре other передаются tipJar и 2 соответственно. Добавьте следующий фрагмент в конец файла wizcoin.py : --snip-- def __iadd__(self, other): """Прибавляет монеты из другого объекта WizCoin к этому объекту.""" if not isinstance(other, WizCoin): return NotImplemented # Объект `self` изменяется на месте: self.galleons += other.galleons self.sickles += other.sickles self.knuts += other.knuts return self # Dunder-методы присваивания на месте # почти всегда возвращают self. def __imul__(self, other): """Умножает galleons, sickles и knuts этого объекта на неотрицательное целое число.""" if not isinstance(other, int): return NotImplemented if other < 0: raise WizCoinException('cannot multiply with negative integers') # Класс WizCoin создает изменяемые объекты. НЕ СОЗДАВАЙТЕ новый # объект, как в следующем закомментированном коде: # return WizCoin(self.galleons * other, self.sickles * other, # self.knuts * other) # Объект `self` изменяется на месте: self.galleons *= other self.sickles *= other self.knuts *= other return self # Dunder-методы присваивания на месте # почти всегда возвращают self. Объекты WizCoin могут использовать оператор += с другими объектами WizCoin и оператор *= с положительными целыми числами. Заметим, что после проверки Dunder-методы Python 377 параметра other методы присваивания на месте изменяют объект self , вместо того чтобы создавать новый объект WizCoin . Чтобы увидеть, как расширенные опера- торы присваивания изменяют объекты WizCoin на месте, введите следующий код в интерактивной оболочке: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> tipJar = wizcoin.WizCoin(0, 0, 37) >>> purse + tipJar ❶ WizCoin(2, 5, 46) ❷ >>> purse WizCoin(2, 5, 10) >>> purse += tipJar ❸ >>> purse WizCoin(2, 5, 47) >>> purse *= 10 ❹ >>> purse WizCoin(20, 50, 470) Оператор + ❶ вызывает dunder-методы __add__() или __radd__() для создания и воз- вращения новых объектов ❷ . Исходные объекты, с которыми работал оператор + , остаются без изменений. Dunder-методы присваивания на месте ❸ и ❹ должны изменять объект на месте, при условии что объект является изменяемым (то есть это объект, значение которого может изменяться). Исключение делается для неиз- меняемых объектов, так как такие объекты по определению не могут изменяться. В таком случае dunder-метод присваивания на месте должен создать и вернуть новый объект, как и числовые и отраженные dunder-методы. Мы не сделали атрибуты galleons , sickles и knuts доступными только для чтения; это означает, что они могут изменяться. Таким образом, объекты WizCoin являются изменяемыми. Большинство классов, которые вы напишете, будут создавать изме- няемые объекты, поэтому вам стоит проектировать dunder-методы присваивания на месте, так чтобы они изменяли объект на месте. Если вы не реализуете dunder-метод присваивания на месте, Python вызовет число- вой dunder-метод. Например, если в классе WizCoin отсутствует метод __imul__() , выражение purse *= 10 вызовет __mul__() и присвоит возвращаемое значение пере- менной purse . Так как объекты WizCoin являются изменяемыми, это неожиданное поведение способно порождать коварные ошибки. |