28.6 Unary Operators: __neg__, __pos__, __abs__
The Purpose of Unary Operators in the Data Model
Unary operators are operations that act upon a single operand to produce a new value. In Python, the three primary unary operators are implemented through the __neg__, __pos__, and __abs__ special methods. These methods are integral to the Python data model, which defines the interfaces that allow your custom objects to interact seamlessly with the language’s core syntax. By implementing these methods, you empower your objects to respond to the - (negation), + (unary plus), and abs() built-in function calls, respectively. This integration is a cornerstone of writing Pythonic code; objects that behave like built-in types make APIs more intuitive and code more maintainable.
Implementing neg (-obj)
The __neg__ method is called to implement the unary arithmetic negation operator (-). Its purpose is to return a new object that represents the additive inverse of the current object. The implementation should be mathematically sound for the domain of your class. Crucially, it should not mutate the original object. Python’s standard library types, like integers and floats, adhere to this principle, and your custom types should too to avoid surprising behavior.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __neg__(self):
"""Return a new Vector that is the negation of this one."""
return Vector(-self.x, -self.y)
# Usage
v = Vector(3, -4)
negated_v = -v # This calls v.__neg__()
print(f"Original: {v}") # Output: Original: Vector(3, -4)
print(f"Negated: {negated_v}") # Output: Negated: Vector(-3, 4)
Implementing pos (+obj)
The __pos__ method is called to implement the unary arithmetic plus operator (+). This operator is often seen as redundant for numbers, as +5 is simply 5. Its primary purpose is to provide symmetry with the __neg__ method. However, for custom classes, it can be highly useful. A common and powerful pattern is to use __pos__ to return a copy of the object. This provides a very clean and explicit syntax for obtaining a copy, which is often a best practice over mutating methods.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __pos__(self):
"""Return a copy of this Vector. Very useful for explicit copying."""
return Vector(self.x, self.y)
def __neg__(self):
return Vector(-self.x, -self.y)
# Usage
original = Vector(2, 3)
copy_v = +original # Explicit and clear syntax for copying
print(f"Original: {original}") # Output: Original: Vector(2, 3)
print(f"Copy: {copy_v}") # Output: Copy: Vector(2, 3)
# Verify they are different objects
print(original is copy_v) # Output: False
Implementing abs (abs(obj))
The __abs__ method is called by the built-in abs() function. Its purpose is to return a value that represents the magnitude or absolute value of the object. For mathematical objects like vectors or complex numbers, this is typically the Euclidean norm. The return type of __abs__ is conventionally a non-negative integer or float, representing the magnitude, but this depends on the object’s domain. It should never mutate the original object.
import math
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __abs__(self):
"""Return the Euclidean magnitude of the vector."""
return math.sqrt(self.x**2 + self.y**2)
# Usage
v = Vector(3, 4)
magnitude = abs(v) # This calls v.__abs__()
print(f"Vector {v} has magnitude: {magnitude}") # Output: Vector Vector(3, 4) has magnitude: 5.0
Common Pitfalls and Best Practices
Immutability is Key: The most critical best practice is that all three of these methods should return a new object and should never modify (
mutate) the instance they are called on. Mutating an object during a unary operation is highly unexpected and will lead to bugs that are difficult to trace. For example, if-valso changedv, it would violate the principle of least astonishment.Type Consistency and Coercion: Consider what type your methods should return. For
__abs__, it’s almost always appropriate to return an integer or float. For__neg__and__pos__, you should typically return an instance of your own class. However, there are exceptions. For example, aComplexNumberclass might have an__abs__method that returns a float, which is the correct mathematical result.Handling Incompatible Types: These methods should raise a
TypeErrorif the operation is fundamentally undefined for the object’s state, though this is rare for unary operations. The error message should be descriptive.Inheritance from Built-in Types: If you are inheriting from a built-in type like
intorfloat, you often don’t need to implement these methods unless you want to change the default behavior. The parent class’s implementation will be used automatically.
class MyInt(int):
"""A simple integer subclass. It inherits __neg__, __pos__, and __abs__ from int."""
pass
num = MyInt(-5)
print(-num) # Output: 5 (calls inherited int.__neg__)
print(+num) # Output: -5 (calls inherited int.__pos__)
print(abs(num)) # Output: 5 (calls inherited int.__abs__)
- Operator Associativity: Be aware that these operators have higher precedence than most other operators. For instance,
-a ** 2is evaluated as-(a ** 2), not(-a) ** 2. Your implementation doesn’t control this; it’s part of Python’s grammar, but it’s important to understand when using your objects.