English 中文(简体)
Python: Can you make this __eq__ easy to understand?
原标题:

I have another question for you.

I have a python class with a list metainfo . This list contains variable names that my class might contain. I wrote a __eq__ method that returns True if the both self and other have the same variables from metainfo and those variables have the same value.

Here is my implementation:

 def __eq__(self, other):
    for attr in self.metainfo:
      try:
        ours = getattr(self, attr) 
        try:
          theirs = getattr(other, attr)
          if ours != theirs:
            return False
        except AttributeError:
          return False
      except AttributeError:
        try:
          theirs = getattr(other, attr)
          return False
        except AttributeError:
          pass
    return True

Does anyone have any suggestions as to how I can make this code easier on the eye? Be as ruthless as you please.

最佳回答

Use getattr s third argument to set distinct default values:

def __eq__(self, other):
    return all(getattr(self, a, Ellipsis) == getattr(other, a, Ellipsis)
               for a in self.metainfo)

As the default value, set something that will never be an actual value, such as Ellipsis. Thus the values will match only if both objects contain the same value for a certain attribute or if both do not have said attribute.

Edit: as Nadia points out, NotImplemented may be a more appropriate constant (unless you re storing the result of rich comparisons...).

Edit 2: Indeed, as Lac points out, just using hasattr results in a more readable solution:

def __eq__(self, other):
    return all(hasattr(self, a) == hasattr(other, a) and
               getattr(self, a) == getattr(other, a) for a in self.metainfo)

  : for extra obscurity you could write ... instead of Ellipsis, thus getattr(self, a, ...) etc. No, don t do it :)

问题回答

I would add a docstring which explains what it compares, as you did in your question.

def __eq__(self, other):
    """Returns True if both instances have the same variables from metainfo
    and they have the same values."""
    for attr in self.metainfo:
        if attr in self.__dict__:
            if attr not in other.__dict__:
                return False
            if getattr(self, attr) != getattr(other, attr):
                return False
            continue
        else:
            if attr in other.__dict__:
                return False
    return True

Since it s about to make it easy to understand, not short or very fast :

class Test(object):

    def __init__(self):
        self.metainfo = ["foo", "bar"]

    # adding a docstring helps a lot
    # adding a doctest even more : you have an example and a unit test
    # at the same time ! (so I know this snippet works :-))
    def __eq__(self, other):
        """
            This method check instances equality and returns True if both of
            the instances have the same attributs with the same values.
            However, the check is performed only on the attributs whose name
            are listed in self.metainfo.

            E.G :

            >>> t1 = Test()
            >>> t2 = Test()
            >>> print t1 == t2
            True
            >>> t1.foo = True
            >>> print t1 == t2
            False
            >>> t2.foo = True
            >>> t2.bar = 1
            >>> print t1 == t2
            False
            >>> t1.bar = 1
            >>> print t1 == t2
            True
            >>> t1.new_value = "test"
            >>> print t1 == t2
            True
            >>> t1.metainfo.append("new_value")
            >>> print t1 == t2
            False

        """

        # Then, let s keep the code simple. After all, you are just
        # comparing lists :

        self_metainfo_val = [getattr(self, info, Ellipsis)
                             for info in self.metainfo]
        other_metainfo_val = [getattr(other, info, Ellipsis)
                              for info in self.metainfo]
        return self_metainfo_val == other_metainfo_val

Going with "Flat is better than nested" I would remove the nested try statements. Instead, getattr should return a sentinel that only equals itself. Unlike Stephan202, however, I prefer to keep the for loop. I also would create a sentinel by myself, and not re-use some existing Python object. This guarantees that there are no false positives, even in the most exotic situations.

def __eq__(self, other):
    if set(metainfo) != set(other.metainfo):
        # if the meta info differs, then assume the items differ.
        # alternatively, define how differences should be handled
        # (e.g. by using the intersection or the union of both metainfos)
        # and use that to iterate over
        return False
    sentinel = object() # sentinel == sentinel <=> sentinel is sentinel
    for attr in self.metainfo:
        if getattr(self, attr, sentinel) != getattr(other, attr, sentinel):
            return False
    return True

Also, the method should have a doc-string explaining it s eq behavior; same goes for the class which should have a docstring explaining the use of the metainfo attribute.

Finally, a unit-test for this equality-behavior should be present as well. Some interesting test cases would be:

  1. Objects that have the same content for all metainfo-attributes, but different content for some other attributes (=> they are equal)
  2. If required, checking for commutativity of equals, i.e. if a == b: b == a
  3. Objects that don t have any of the metainfo-attributes set

I would break the logic up into separate chunks that are easier to understand, each one checking a different condition (and each one assuming the previous thing was checked). Easiest just to show the code:

# First, check if we have the same list of variables.
my_vars = [var for var in self.metainf if hasattr(self, var)]
other_vars = [var for var in other.metainf if hasattr(other, var)]

if my_vars.sorted() != other_vars.sorted():
  return False # Don t even have the same variables.

# Now, check each variable:
for var in my_vars:
   if self.var != other.var:
      return False # We found a variable with a different value.

# We re here, which means we haven t found any problems!
return True

Edit: I misunderstood the question, here is an updated version. I still think this is a clear way to write this kind of logic, but it s uglier than I intended and not at all efficient, so in this case I d probably go with a different solution.

The try/excepts make your code harder to read. I d use getattr with a default value that is guaranteed not to otherwise be there. In the code below I just make a temp object. That way if object do not have a given value they ll both return "NOT_PRESENT" and thus count as being equal.


def __eq__(self, other):
    NOT_PRESENT = object()
    for attr in self.metainfo:
        ours = getattr(self, attr, NOT_PRESENT) 
        theirs = getattr(other, attr, NOT_PRESENT)
        if ours != theirs:
            return False
    return True

Here is a variant that is pretty easy to read IMO, without using sentinel objects. It will first compare if both has or hasnt the attribute, then compare the values.

It could be done in one line using all() and a generator expression as Stephen did, but I feel this is more readable.

def __eq__(self, other):
    for a in self.metainfo:
        if hasattr(self, a) != hasattr(other, a):
             return False
        if getattr(self, a, None) != getattr(other, a, None):
             return False
    return True

I like Stephan202 s answer, but I think that his code doesn t make equality conditions clear enough. Here s my take on it:

def __eq__(self, other):
    wehave = [attr for attr in self.metainfo if hasattr(self, attr)]
    theyhave = [attr for attr in self.metainfo if hasattr(other, attr)]
    if wehave != theyhave:
        return False
    return all(getattr(self, attr) == getattr(other, attr) for attr in wehave)




相关问题
Can Django models use MySQL functions?

Is there a way to force Django models to pass a field to a MySQL function every time the model data is read or loaded? To clarify what I mean in SQL, I want the Django model to produce something like ...

An enterprise scheduler for python (like quartz)

I am looking for an enterprise tasks scheduler for python, like quartz is for Java. Requirements: Persistent: if the process restarts or the machine restarts, then all the jobs must stay there and ...

How to remove unique, then duplicate dictionaries in a list?

Given the following list that contains some duplicate and some unique dictionaries, what is the best method to remove unique dictionaries first, then reduce the duplicate dictionaries to single ...

What is suggested seed value to use with random.seed()?

Simple enough question: I m using python random module to generate random integers. I want to know what is the suggested value to use with the random.seed() function? Currently I am letting this ...

How can I make the PyDev editor selectively ignore errors?

I m using PyDev under Eclipse to write some Jython code. I ve got numerous instances where I need to do something like this: import com.work.project.component.client.Interface.ISubInterface as ...

How do I profile `paster serve` s startup time?

Python s paster serve app.ini is taking longer than I would like to be ready for the first request. I know how to profile requests with middleware, but how do I profile the initialization time? I ...

Pragmatically adding give-aways/freebies to an online store

Our business currently has an online store and recently we ve been offering free specials to our customers. Right now, we simply display the special and give the buyer a notice stating we will add the ...

Converting Dictionary to List? [duplicate]

I m trying to convert a Python dictionary into a Python list, in order to perform some calculations. #My dictionary dict = {} dict[ Capital ]="London" dict[ Food ]="Fish&Chips" dict[ 2012 ]="...

热门标签