开发者

Pointing to another object's attributes and adding your own

Suppose I have a class:

class Car(object):
    def __init__(self, name, tank_size=10, mpg=30):
        self.name = name
        self.tank_size = tank_size
        self.mpg = mpg

I put together a list of the cars I'm looking at:

cars = []
cars.append(Car("Toyota", 11, 29))
cars.append(Car("Ford", 15, 12))
cars.append(Car("Honda", 12, 25))

If I assign a name to my current favorite (a "pointer" into the list, if you will):

my_current_fav = cars[1]

I can easily access the attributes of my current favorite:

my_current_fav.name       # Returns "Ford"
my_current_fav.tank_size  # Returns 15
my_current_fav.mpg        # Returns 12

Here's where I start getting foggy. I would like to provide additional "computed" attributes only for my current favorite (let's assume these attributes are too "expensive" to store in the original list and are easier to just compute):

my_current_fav.range      # Would return tank_size * mpg = 180
                          # (if pointing to "Ford")

In my case, I just capitulated and added 'range' as an attribute of Car(). But what if storing 'range' in each Car() instance was expensive but calculating it was cheap?

I considered making 'my_current_fav' a sub-class of Car(), but I couldn't figure out a way to do that and still maintain my ability to simply "point" 'my_current_favorite' to an entry in the 'cars' list.

I also considered using decorators to compute and return 'range', but couldn't figure out a way to also provide access to the attributes 'name', 'mpg', etc.

Is there an elegant way to point to any item in the list 'cars', provide access to the attributes of the instance being pointed to as well as provide additional attributes not found in the class Car?

Additional information:

After reading many of your answers, I see there is background information I should have put into开发者_如何学Go the original question. Rather than comment on many answers individually, I'll put the additional info here.

This question is a simplification of a more complicated issue. The original problem involves modifications to an existing library. While making range a method call rather than an attribute is a good way to go, changing

some_var = my_current_favorite.range

to

some_var = my_current_favorite.range()

in many existing user scripts would be expensive. Heck, tracking down those user scripts would be expensive.

Likewise, my current approach of computing range for every car isn't "expensive" in Python terms, but is expensive in run-time because the real-world analog requires slow calls to the (embedded) OS. While Python itself isn't slow, those calls are, so I am seeking to minimize them.


This is easiest to do for your example, without changing Car, and changing as little else as possible, with __getattr__:

class Car(object):
    def __init__(self, name, tank_size=10, mpg=30):
        self.name = name
        self.tank_size = tank_size
        self.mpg = mpg

class Favorite(object):
    def __init__(self, car):
        self.car = car
    def __getattr__(self, attr):
        return getattr(self.car, attr)
    @property
    def range(self):
        return self.mpg * self.tank_size

cars = []
cars.append(Car("Toyota", 11, 29))
cars.append(Car("Ford", 15, 12))
cars.append(Car("Honda", 12, 25))

my_current_fav = Favorite(cars[1])

print my_current_fav.range

Any attribute not found on an instance of Favorite will be looked up on the Favorite instances car attribute, which you set when you make the Favorite.

Your example of range isn't a particularly good one for something to add to Favorite, because it should just be a property of car, but I used it for simplicity.

Edit: Note that a benefit of this method is if you change your favorite car, and you've not stored anything car-specific on Favorite, you can change the existing favorite to a different car with:

my_current_fav.car = cars[0] # or Car('whatever')


If you have access to the class (and it sounds like you do), just create a function inside the class instead.

def range(self):
    return self.tank_size * self.mpg


With regards to your example, you could make range a read-only property of class Car that would be computed on demand. No need for extra classes.


Why don't you just create a method:

class Car(object):
    def __init__(self, name, tank_size=10, mpg=30):
        self.name = name
        self.tank_size = tank_size
        self.mpg = mpg

    def range(self):
        return self.tank_size * self.mpg


Sounds like range() should be a method of the class. Methods are very cheap - the objects don't store them. The downside is it is computed each time you access the value of range.

class Car(object):
   def __init__(self, name, tank_size=10, mpg=30):
       [AS ABOVE]
   def range(self):
      return self.tank_size * self.mpg

If you prefer it to behave like a field, i.e. compute only once, you can store the value in the object during the range method:

  def range(self):
     if not hasattr(self,'_rangeval'):
         self._rangeval = self.tank_size * self.mpg
     return self._rangeval

This takes advantage of the fact that you can dynamically create fields in objects.

I don't understand why the default would be 180 when the default of the computed values is 300. If this strange behaviour is important, you will need to set another flag to see if the other parameters have been initialised to the default or not.


I'm not sure I understand what you're trying to do, so I'm going to cover a few different possible understandings of what you're thinking.

In my case, I just capitulated and added 'range' as an attribute of Car(). But what if storing 'range' in each Car() instance was expensive but calculating it was cheap?

First off, worrying about things being "expensive" is usually not that Pythonic. If it really mattered, you would be using a lower-level language most of the time. But in general, if you want something to be calculated rather than stored, the natural way is to use a method. Add it to the Car class. It does not cost per-object, unless of course you explicitly replace the method on a per-object basis.

Here's how it works: when you make a call to a_car.range(), the range attribute is looked up in a_car first. If it's not found there, then (skipping lots and lots of details here!) the class of a_car is identified as Car, and the attribute is looked up there. You define range as a Car method, so it gets found there, and is determined to be something that's actually callable, so it gets called. As a special syntax rule, a_car gets passed as the first parameter to the method (all of which partly explains why you need to have an explicit parameter - named self by convention - for methods in Python, unlike many other languages with an implicit this).

I considered making 'my_current_fav' a sub-class of Car(), but I couldn't figure out a way to do that and still maintain my ability to simply "point" 'my_current_favorite' to an entry in the 'cars' list.

You can definitely store a subclass of Car in the same list as a bunch of ordinary Cars. No problem there. Heck, you can store a Banana in the same list as a bunch of Cars if you like; Python is dynamically typed, and doesn't care. It will figure out what kind of object something is at the exact moment that it becomes relevant.

What you can't easily do is cause an existing Car to become a MyFavouriteCar. If you created a new MyFavouriteCar that my_favourite_car refers to (don't say "points at", please; object references are a higher-level abstraction, and unlike Java there is no "null pointer" in Python - None is an object), then you could replace an existing car in the list with it, but it's still a different object (even if it's somehow based on the original Car that it replaces). You can design in such a way that this doesn't matter; or you can resort to evil hackery (which I won't explain here); or you can (much better in your case) just offer the functionality to all Cars, because it's really free to do so.

From the Zen of Python: Special cases aren't special enough.


You talk about my_current_fav being a 'pointer' -- I just want to make sure you realize that, in fact,

my_current_fav = cars[1]

binds a name to cars[1] -- in other words, my_current_fav is cars[1] == True. There is no pointing going on. If you really want a pointer-stlye you can do something like this:

class Favorite(object):
    def __init__(self, car_list, index):
        self.car_list = car_list
        self.index = index
    def __getattr__(self, attr):
        return getattr(self.car_list[self.index], attr)
    def __index__(self):
        return self.index
    @property
    def range(self):
        return self.mpg * self.tank_size

my_current_fav = Favorite(cars, 1)

print my_current_fav.name
print my_current_fav.range
print cars[my_current_fav]
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜