Các cách thanh lịch để hỗ trợ tính tương đương (bằng nhau) trong các lớp Python


421

Khi viết các lớp tùy chỉnh, điều quan trọng là phải cho phép sự tương đương bằng các toán tử ==!=toán tử. Trong Python, điều này được thực hiện bằng cách thực hiện các phương thức đặc biệt __eq____ne__tương ứng. Cách dễ nhất tôi tìm thấy để làm điều này là phương pháp sau:

class Foo:
    def __init__(self, item):
        self.item = item

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

    def __ne__(self, other):
        return not self.__eq__(other)

Bạn có biết phương tiện thanh lịch hơn để làm điều này? Bạn có biết bất kỳ nhược điểm cụ thể nào khi sử dụng phương pháp so sánh ở trên __dict__không?

Lưu ý : Một chút làm rõ - khi __eq____ne__không xác định, bạn sẽ tìm thấy hành vi này:

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
False

Đó là, a == bđánh giá Falsebởi vì nó thực sự chạy a is b, một bài kiểm tra danh tính (nghĩa là "Có phải acùng một đối tượng bkhông?").

Khi __eq____ne__được xác định, bạn sẽ tìm thấy hành vi này (đó là hành vi chúng tôi theo sau):

>>> a = Foo(1)
>>> b = Foo(1)
>>> a is b
False
>>> a == b
True

6
+1, vì tôi không biết rằng dict đã sử dụng đẳng thức thành viên cho ==, nên tôi đã giả sử nó chỉ tính chúng bằng nhau cho cùng một đối tượng. Tôi đoán điều này là hiển nhiên vì Python có istoán tử để phân biệt danh tính đối tượng với so sánh giá trị.
SingleNegationElimination

5
Tôi nghĩ rằng câu trả lời được chấp nhận sẽ được sửa chữa hoặc gán lại cho câu trả lời của Algorias, để việc kiểm tra loại nghiêm ngặt được thực hiện.
tối đa

1
Ngoài ra, hãy đảm bảo hàm băm được ghi đè stackoverflow.com/questions/1608842/
Kẻ

Câu trả lời:


328

Hãy xem xét vấn đề đơn giản này:

class Number:

    def __init__(self, number):
        self.number = number


n1 = Number(1)
n2 = Number(1)

n1 == n2 # False -- oops

Vì vậy, Python theo mặc định sử dụng các định danh đối tượng cho các hoạt động so sánh:

id(n1) # 140400634555856
id(n2) # 140400634555920

Ghi đè __eq__chức năng dường như để giải quyết vấn đề:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return False


n1 == n2 # True
n1 != n2 # True in Python 2 -- oops, False in Python 3

Trong Python 2 , luôn luôn nhớ ghi đè __ne__hàm, như tài liệu nêu:

Không có mối quan hệ ngụ ý giữa các nhà khai thác so sánh. Sự thật x==ykhông ngụ ý đó x!=ylà sai. Theo đó, khi xác định __eq__(), người ta cũng nên xác định __ne__()để các toán tử sẽ hành xử như mong đợi.

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    return not self.__eq__(other)


n1 == n2 # True
n1 != n2 # False

Trong Python 3 , điều này không còn cần thiết nữa, vì tài liệu nêu rõ:

Theo mặc định, __ne__()các đại biểu đến __eq__()và đảo ngược kết quả trừ khi có NotImplemented. Không có mối quan hệ ngụ ý nào khác giữa các toán tử so sánh, ví dụ, sự thật (x<y or x==y)không ngụ ý x<=y.

Nhưng điều đó không giải quyết được tất cả các vấn đề của chúng tôi. Hãy thêm một lớp con:

class SubNumber(Number):
    pass


n3 = SubNumber(1)

n1 == n3 # False for classic-style classes -- oops, True for new-style classes
n3 == n1 # True
n1 != n3 # True for classic-style classes -- oops, False for new-style classes
n3 != n1 # False

Lưu ý: Python 2 có hai loại lớp:

  • phong cách cổ điển (hoặc kiểu cũ ) lớp học, mà không kế thừa từobjectvà được khai báo làclass A:,class A():hoặcclass A(B):nơiBlà một lớp phong cách cổ điển;

  • các lớp kiểu mới , được kế thừa từobjectđó và được khai báo làclass A(object)hoặcclass A(B):ở đâuBlà một lớp kiểu mới. Python 3 chỉ có các lớp kiểu mới được khai báo làclass A:,class A(object):hoặcclass A(B):.

Đối với các lớp kiểu cổ điển, một phép toán so sánh luôn gọi phương thức của toán hạng đầu tiên, trong khi đối với các lớp kiểu mới, nó luôn gọi phương thức của toán hạng lớp con, bất kể thứ tự của toán hạng .

Vì vậy, ở đây, nếu Numberlà một lớp theo phong cách cổ điển:

  • n1 == n3các cuộc gọi n1.__eq__;
  • n3 == n1các cuộc gọi n3.__eq__;
  • n1 != n3các cuộc gọi n1.__ne__;
  • n3 != n1các cuộc gọi n3.__ne__.

Và nếu Numberlà một lớp kiểu mới:

  • cả hai n1 == n3n3 == n1gọi n3.__eq__;
  • cả hai n1 != n3n3 != n1gọi n3.__ne__.

Để khắc phục sự cố không giao hoán của các toán tử ==!=toán tử cho các lớp kiểu cổ điển Python 2, các phương thức __eq____ne__sẽ trả về NotImplementedgiá trị khi loại toán hạng không được hỗ trợ. Các tài liệu hướng dẫn xác định NotImplementedgiá trị như:

Các phương thức số và phương thức so sánh phong phú có thể trả về giá trị này nếu chúng không thực hiện thao tác cho các toán hạng được cung cấp. (Trình thông dịch sau đó sẽ thử hoạt động được phản ánh hoặc một số dự phòng khác, tùy thuộc vào toán tử.) Giá trị thật của nó là đúng.

Trong trường hợp này, toán tử ủy nhiệm thao tác so sánh cho phương thức phản ánh của toán hạng khác . Các tài liệu định nghĩa phản ánh phương pháp như:

Không có phiên bản đối số hoán đổi của các phương thức này (được sử dụng khi đối số bên trái không hỗ trợ thao tác nhưng đối số bên phải thì không); đúng hơn, __lt__()__gt__()là sự phản ánh của nhau, __le__()__ge__()là sự phản ánh của nhau, __eq__()__ne__()là sự phản ánh của chính họ.

Kết quả trông như thế này:

def __eq__(self, other):
    """Overrides the default implementation"""
    if isinstance(other, Number):
        return self.number == other.number
    return NotImplemented

def __ne__(self, other):
    """Overrides the default implementation (unnecessary in Python 3)"""
    x = self.__eq__(other)
    if x is NotImplemented:
        return NotImplemented
    return not x

Trả về NotImplementedgiá trị thay vì Falselà điều nên làm ngay cả đối với các lớp kiểu mới nếu tính giao hoán của toán tử ==!=toán tử được mong muốn khi các toán hạng có kiểu không liên quan (không có kế thừa).

Chúng ta đã ở đó chưa? Không hẳn. Chúng ta có bao nhiêu số duy nhất?

len(set([n1, n2, n3])) # 3 -- oops

Các bộ sử dụng hàm băm của các đối tượng và theo mặc định Python trả về hàm băm của mã định danh của đối tượng. Hãy thử ghi đè lên nó:

def __hash__(self):
    """Overrides the default implementation"""
    return hash(tuple(sorted(self.__dict__.items())))

len(set([n1, n2, n3])) # 1

Kết quả cuối cùng trông như thế này (tôi đã thêm một số xác nhận ở cuối để xác thực):

class Number:

    def __init__(self, number):
        self.number = number

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Number):
            return self.number == other.number
        return NotImplemented

    def __ne__(self, other):
        """Overrides the default implementation (unnecessary in Python 3)"""
        x = self.__eq__(other)
        if x is not NotImplemented:
            return not x
        return NotImplemented

    def __hash__(self):
        """Overrides the default implementation"""
        return hash(tuple(sorted(self.__dict__.items())))


class SubNumber(Number):
    pass


n1 = Number(1)
n2 = Number(1)
n3 = SubNumber(1)
n4 = SubNumber(4)

assert n1 == n2
assert n2 == n1
assert not n1 != n2
assert not n2 != n1

assert n1 == n3
assert n3 == n1
assert not n1 != n3
assert not n3 != n1

assert not n1 == n4
assert not n4 == n1
assert n1 != n4
assert n4 != n1

assert len(set([n1, n2, n3, ])) == 1
assert len(set([n1, n2, n3, n4])) == 2

3
hash(tuple(sorted(self.__dict__.items())))sẽ không hoạt động nếu có bất kỳ đối tượng không thể băm nào trong số các giá trị của self.__dict__(nghĩa là, nếu bất kỳ thuộc tính nào của đối tượng được đặt thành, giả sử, a list).
tối đa

3
Đúng, nhưng sau đó nếu bạn có các đối tượng có thể thay đổi như vậy trong vars của bạn () thì hai đối tượng không thực sự bằng nhau ...
Tal Weiss


1
Ba nhận xét: 1. Trong Python 3, không cần phải thực hiện __ne__nữa: "Theo mặc định, __ne__()các đại biểu đến __eq__()và đảo ngược kết quả trừ khi có NotImplemented". 2. Nếu một người vẫn muốn thực hiện __ne__, thì một triển khai chung hơn (tôi sử dụng Python 3) là : x = self.__eq__(other); if x is NotImplemented: return x; else: return not x. 3. Việc thực hiện __eq____ne__thực hiện là không tối ưu: thực hiện if isinstance(other, type(self)):22 __eq__và 10 __ne__cuộc gọi, trong khi if isinstance(self, type(other)):sẽ thực hiện 16 __eq__và 6 __ne__cuộc gọi.
Maggyero

4
Anh ấy hỏi về sự thanh lịch, nhưng anh ấy mạnh mẽ.
GregNash

201

Bạn cần cẩn thận với sự kế thừa:

>>> class Foo:
    def __eq__(self, other):
        if isinstance(other, self.__class__):
            return self.__dict__ == other.__dict__
        else:
            return False

>>> class Bar(Foo):pass

>>> b = Bar()
>>> f = Foo()
>>> f == b
True
>>> b == f
False

Kiểm tra các loại nghiêm ngặt hơn, như thế này:

def __eq__(self, other):
    if type(other) is type(self):
        return self.__dict__ == other.__dict__
    return False

Bên cạnh đó, cách tiếp cận của bạn sẽ hoạt động tốt, đó là những phương pháp đặc biệt đang có.


đây là một quan điểm tốt. Tôi cho rằng đáng chú ý rằng phân loại được xây dựng theo kiểu vẫn cho phép bình đẳng theo cả hai hướng, và vì vậy kiểm tra xem đó có phải là cùng loại không mong muốn.
gotgenes

12
Tôi đề nghị trả về NotIm Hiện thực nếu các loại khác nhau, ủy thác việc so sánh với các rhs.
tối đa

4
@max so sánh không nhất thiết phải thực hiện bên tay trái (LHS) với bên tay phải (RHS), sau đó RHS sang LHS; xem stackoverflow.com/a/12984987/38140 . Tuy nhiên, trở lại NotImplementednhư bạn đề xuất sẽ luôn luôn gây ra superclass.__eq__(subclass), đó là hành vi mong muốn.
gotgenes

4
Nếu bạn có rất nhiều thành viên và không có nhiều bản sao đối tượng ngồi xung quanh, thì thông thường tốt hơn là thêm một bài kiểm tra nhận dạng ban đầu if other is self. Điều này tránh việc so sánh từ điển dài hơn và có thể là một khoản tiết kiệm rất lớn khi các đối tượng được sử dụng làm khóa từ điển.
Dane White

2
Và đừng quên thực hiện__hash__()
Dane White

161

Cách bạn mô tả là cách tôi luôn làm. Vì nó hoàn toàn chung chung, bạn luôn có thể chia chức năng đó thành một lớp mixin và kế thừa nó trong các lớp mà bạn muốn chức năng đó.

class CommonEqualityMixin(object):

    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)

    def __ne__(self, other):
        return not self.__eq__(other)

class Foo(CommonEqualityMixin):

    def __init__(self, item):
        self.item = item

6
+1: Mẫu chiến lược để cho phép thay thế dễ dàng trong các lớp con.
S.Lott

3
isinstance hút. Tại sao phải kiểm tra nó? Tại sao không chỉ tự .__ dict__ == khác .__ dict__?
nosklo

3
@nosklo: Tôi không hiểu .. nếu hai đối tượng từ các lớp hoàn toàn không liên quan xảy ra có cùng thuộc tính thì sao?
tối đa

1
Tôi nghĩ rằng nokslo đề nghị bỏ qua isinstance. Trong trường hợp đó, bạn không còn biết nếu otherlà một lớp con của self.__class__.
tối đa

10
Một vấn đề khác với __dict__so sánh là nếu bạn có một thuộc tính mà bạn không muốn xem xét trong định nghĩa về đẳng thức của bạn (ví dụ: id đối tượng duy nhất hoặc siêu dữ liệu như dấu thời gian được tạo).
Adam Parkin

14

Không phải là một câu trả lời trực tiếp nhưng dường như đủ liên quan để giải quyết vì nó tiết kiệm một chút tẻ nhạt đôi khi. Cắt thẳng từ tài liệu ...


funcools.total_ordering (cls)

Đưa ra một lớp xác định một hoặc nhiều phương thức đặt hàng so sánh phong phú, trình trang trí lớp này cung cấp phần còn lại. Điều này đơn giản hóa các nỗ lực liên quan đến việc chỉ định tất cả các hoạt động so sánh phong phú có thể có:

Lớp phải xác định một trong __lt__() , __le__(), __gt__(), hoặc __ge__(). Ngoài ra, lớp nên cung cấp một __eq__()phương thức.

Phiên bản mới 2.7

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

1
Tuy nhiên, Total_ordering có những cạm bẫy tinh vi: regebro.wordpress.com/2010/12/13/ . Hãy nhận biết!
Mr_and_Mrs_D

8

Bạn không phải ghi đè cả hai __eq____ne__bạn chỉ có thể ghi đè __cmp__nhưng điều này sẽ ảnh hưởng đến kết quả của == ,! ==, <,>, v.v.

iskiểm tra nhận dạng đối tượng. Điều này có nghĩa là isb sẽ nằm Truetrong trường hợp khi cả a và b đều giữ tham chiếu đến cùng một đối tượng. Trong python, bạn luôn giữ một tham chiếu đến một đối tượng trong một biến không phải là đối tượng thực tế, vì vậy về cơ bản đối với a là b là đúng, các đối tượng trong chúng phải được đặt trong cùng một vị trí bộ nhớ. Làm thế nào và quan trọng nhất tại sao bạn sẽ đi về việc ghi đè hành vi này?

Chỉnh sửa: Tôi không biết __cmp__đã bị xóa khỏi python 3 vì vậy hãy tránh nó.


Bởi vì đôi khi bạn có một định nghĩa khác nhau về sự bình đẳng cho các đối tượng của bạn.
Ed S.

toán tử is cung cấp cho bạn thông dịch viên trả lời cho danh tính đối tượng, nhưng bạn vẫn có thể tự do bày tỏ quan điểm của mình về sự bình đẳng bằng cách ghi đè cmp
Vasil

7
Trong Python 3, "Hàm cmp () không còn nữa và phương thức đặc biệt __cmp __ () không còn được hỗ trợ." is.gd/aeGv
gotgenes

4

Từ câu trả lời này: https://stackoverflow.com/a/30676267/541136 Tôi đã chứng minh rằng, trong khi đó là chính xác để định nghĩa __ne__theo thuật ngữ __eq__- thay vì

def __ne__(self, other):
    return not self.__eq__(other)

bạn nên sử dụng:

def __ne__(self, other):
    return not self == other

2

Tôi nghĩ rằng hai thuật ngữ bạn đang tìm kiếm là bình đẳng (==) và danh tính (là). Ví dụ:

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> a == b
True       <-- a and b have values which are equal
>>> a is b
False      <-- a and b are not the same list object

1
Có lẽ, ngoại trừ việc người ta có thể tạo một lớp chỉ so sánh hai mục đầu tiên trong hai danh sách và nếu các mục đó bằng nhau, nó sẽ đánh giá là Đúng. Đây là sự tương đương, tôi nghĩ, không bình đẳng. Hoàn toàn hợp lệ trong eq , vẫn còn.
gotgenes

Tôi đồng ý, tuy nhiên, "là" là một bài kiểm tra danh tính.
gotgenes

1

Kiểm tra 'is' sẽ kiểm tra danh tính bằng cách sử dụng hàm 'id ()' dựng sẵn, về cơ bản trả về địa chỉ bộ nhớ của đối tượng và do đó không bị quá tải.

Tuy nhiên, trong trường hợp kiểm tra tính bằng nhau của một lớp, bạn có thể muốn nghiêm ngặt hơn một chút về các kiểm tra của mình và chỉ so sánh các thuộc tính dữ liệu trong lớp của bạn:

import types

class ComparesNicely(object):

    def __eq__(self, other):
        for key, value in self.__dict__.iteritems():
            if (isinstance(value, types.FunctionType) or 
                    key.startswith("__")):
                continue

            if key not in other.__dict__:
                return False

            if other.__dict__[key] != value:
                return False

         return True

Mã này sẽ chỉ so sánh các thành viên dữ liệu không có chức năng trong lớp của bạn cũng như bỏ qua mọi thứ riêng tư mà thường là những gì bạn muốn. Trong trường hợp các Đối tượng Python cũ đơn giản, tôi có một lớp cơ sở thực hiện __init__, __str__, __Vpr__ và __eq__ để các đối tượng POPO của tôi không mang gánh nặng của tất cả logic đó (và trong hầu hết các trường hợp giống hệt nhau).


Bit nitpicky, nhưng 'là' kiểm tra bằng cách sử dụng id () nếu bạn chưa xác định hàm thành viên is_ () của riêng bạn (2.3+). [
docs.python.org/l Library / operator.html

Tôi giả sử bằng cách "ghi đè" bạn thực sự có nghĩa là khỉ vá mô-đun toán tử. Trong trường hợp này tuyên bố của bạn không hoàn toàn chính xác. Mô-đun toán tử được cung cấp để thuận tiện và ghi đè các phương thức đó không ảnh hưởng đến hành vi của toán tử "is". Một so sánh sử dụng "is" luôn sử dụng id () của một đối tượng để so sánh, hành vi này không thể bị ghi đè. Ngoài ra một hàm is_ thành viên không có tác dụng so sánh.
mcrute

mcrute - Tôi đã nói quá sớm (và không chính xác), bạn hoàn toàn đúng.
dành

Đây là một giải pháp rất hay, đặc biệt là khi __eq__sẽ được khai báo CommonEqualityMixin(xem câu trả lời khác). Tôi thấy điều này đặc biệt hữu ích khi so sánh các thể hiện của các lớp xuất phát từ Base trong SQLAlchemy. Để không so sánh _sa_instance_statetôi đổi key.startswith("__")):thành key.startswith("_")):. Tôi cũng có một số phản hồi trong đó và câu trả lời từ Algorias tạo ra đệ quy vô tận. Vì vậy, tôi đã đặt tên cho tất cả các phản hồi bắt đầu bằng '_'để chúng cũng bị bỏ qua trong khi so sánh. LƯU Ý: trong Python 3.x thay đổi iteritems()thành items().
Wookie88

@mcrute Thông thường, __dict__một cá thể không có bất cứ thứ gì bắt đầu bằng __trừ khi nó được xác định bởi người dùng. Những thứ như __class__, __init__v.v. không phải trong trường hợp __dict__, mà là trong lớp của nó ' __dict__. OTOH, các thuộc tính riêng tư có thể dễ dàng bắt đầu __và có lẽ nên được sử dụng cho __eq__. Bạn có thể làm rõ chính xác những gì bạn đang cố gắng tránh khi bỏ qua __các thuộc tính -prefixed?
tối đa

1

Thay vì sử dụng phân lớp / mixins, tôi thích sử dụng một trình trang trí lớp chung

def comparable(cls):
    """ Class decorator providing generic comparison functionality """

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.__dict__ == other.__dict__

    def __ne__(self, other):
        return not self.__eq__(other)

    cls.__eq__ = __eq__
    cls.__ne__ = __ne__
    return cls

Sử dụng:

@comparable
class Number(object):
    def __init__(self, x):
        self.x = x

a = Number(1)
b = Number(1)
assert a == b

0

Điều này kết hợp các nhận xét về câu trả lời của Algorias và so sánh các đối tượng theo một thuộc tính duy nhất bởi vì tôi không quan tâm đến toàn bộ lệnh. hasattr(other, "id")phải đúng, nhưng tôi biết đó là vì tôi đặt nó trong hàm tạo.

def __eq__(self, other):
    if other is self:
        return True

    if type(other) is not type(self):
        # delegate to superclass
        return NotImplemented

    return other.id == self.id
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.