Using @property versus getters and setters

2018-12-31 04:09发布

Here is a pure Python-specific design question:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

and

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

Python lets us to do it either way. If you would design a Python program, which approach would you use and why?

12条回答
梦醉为红颜
2楼-- · 2018-12-31 04:39

I would prefer to use neither in most cases. The problem with properties is that they make the class less transparent. Especially, this is an issue if you were to raise an exception from a setter. For example, if you have an Account.email property:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

then the user of the class does not expect that assigning a value to the property could cause an exception:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

As a result, the exception may go unhandled, and either propagate too high in the call chain to be handled properly, or result in a very unhelpful traceback being presented to the program user (which is sadly too common in the world of python and java).

I would also avoid using getters and setters:

  • because defining them for all properties in advance is very time consuming,
  • makes the amount of code unnecessarily longer, which makes understanding and maintaining the code more difficult,
  • if you were define them for properties only as needed, the interface of the class would change, hurting all users of the class

Instead of properties and getters/setters I prefer doing the complex logic in well defined places such as in a validation method:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

or a similiar Account.save method.

Note that I am not trying to say that there are no cases when properties are useful, only that you may be better off if you can make your classes simple and transparent enough that you don't need them.

查看更多
何处买醉
3楼-- · 2018-12-31 04:43

The short answer is: properties wins hands down. Always.

There is sometimes a need for getters and setters, but even then, I would "hide" them to the outside world. There are plenty of ways to do this in Python (getattr, setattr, __getattribute__, etc..., but a very concise and clean one is:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

Here's a brief article that introduces the topic of getters and setters in Python.

查看更多
皆成旧梦
4楼-- · 2018-12-31 04:45

[TL;DR? You can skip to the end for a code example.]

I actually prefer to use a different idiom, which is a little involved for using as a one off, but is nice if you have a more complex use case.

A bit of background first.

Properties are useful in that they allow us to handle both setting and getting values in a programmatic way but still allow attributes to be accessed as attributes. We can turn 'gets' into 'computations' (essentially) and we can turn 'sets' into 'events'. So let's say we have the following class, which I've coded with Java-like getters and setters.

class Example(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

    def getX(self):
        return self.x or self.defaultX()

    def getY(self):
        return self.y or self.defaultY()

    def setX(self, x):
        self.x = x

    def setY(self, y):
        self.y = y

    def defaultX(self):
        return someDefaultComputationForX()

    def defaultY(self):
        return someDefaultComputationForY()

You may be wondering why I didn't call defaultX and defaultY in the object's __init__ method. The reason is that for our case I want to assume that the someDefaultComputation methods return values that vary over time, say a timestamp, and whenever x (or y) is not set (where, for the purpose of this example, "not set" means "set to None") I want the value of x's (or y's) default computation.

So this is lame for a number of reasons describe above. I'll rewrite it using properties:

class Example(object):
    def __init__(self, x=None, y=None):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self.x or self.defaultX()

    @x.setter
    def x(self, value):
        self._x = value

    @property
    def y(self):
        return self.y or self.defaultY()

    @y.setter
    def y(self, value):
        self._y = value

    # default{XY} as before.

What have we gained? We've gained the ability to refer to these attributes as attributes even though, behind the scenes, we end up running methods.

Of course the real power of properties is that we generally want these methods to do something in addition to just getting and setting values (otherwise there is no point in using properties). I did this in my getter example. We are basically running a function body to pick up a default whenever the value isn't set. This is a very common pattern.

But what are we losing, and what can't we do?

The main annoyance, in my view, is that if you define a getter (as we do here) you also have to define a setter.[1] That's extra noise that clutters the code.

Another annoyance is that we still have to initialize the x and y values in __init__. (Well, of course we could add them using setattr() but that is more extra code.)

Third, unlike in the Java-like example, getters cannot accept other parameters. Now I can hear you saying already, well, if it's taking parameters it's not a getter! In an official sense, that is true. But in a practical sense there is no reason we shouldn't be able to parameterize an named attribute -- like x -- and set its value for some specific parameters.

It'd be nice if we could do something like:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

for example. The closest we can get is to override the assignment to imply some special semantics:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

and of course ensure that our setter knows how to extract the first three values as a key to a dictionary and set its value to a number or something.

But even if we did that we still couldn't support it with properties because there is no way to get the value because we can't pass parameters at all to the getter. So we've had to return everything, introducing an asymmetry.

The Java-style getter/setter does let us handle this, but we're back to needing getter/setters.

In my mind what we really want is something that capture the following requirements:

  • Users define just one method for a given attribute and can indicate there whether the attribute is read-only or read-write. Properties fail this test if the attribute writable.

  • There is no need for the user to define an extra variable underlying the function, so we don't need the __init__ or setattr in the code. The variable just exists by the fact we've created this new-style attribute.

  • Any default code for the attribute executes in the method body itself.

  • We can set the attribute as an attribute and reference it as an attribute.

  • We can parameterize the attribute.

In terms of code, we want a way to write:

def x(self, *args):
    return defaultX()

and be able to then do:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

and so forth.

We also want a way to do this for the special case of a parameterizable attribute, but still allow the default assign case to work. You'll see how I tackled this below.

Now to the point (yay! the point!). The solution I came up for for this is as follows.

We create a new object to replace the notion of a property. The object is intended to store the value of a variable set to it, but also maintains a handle on code that knows how to calculate a default. Its job is to store the set value or to run the method if that value is not set.

Let's call it an UberProperty.

class UberProperty(object):

    def __init__(self, method):
        self.method = method
        self.value = None
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def clearValue(self):
        self.value = None
        self.isSet = False

I assume method here is a class method, value is the value of the UberProperty, and I have added isSet because None may be a real value and this allows us a clean way to declare there really is "no value". Another way is a sentinel of some sort.

This basically gives us an object that can do what we want, but how do we actually put it on our class? Well, properties use decorators; why can't we? Let's see how it might look (from here on I'm going to stick to using just a single 'attribute', x).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

This doesn't actually work yet, of course. We have to implement uberProperty and make sure it handles both gets and sets.

Let's start with gets.

My first attempt was to simply create a new UberProperty object and return it:

def uberProperty(f):
    return UberProperty(f)

I quickly discovered, of course, that this doens't work: Python never binds the callable to the object and I need the object in order to call the function. Even creating the decorator in the class doesn't work, as although now we have the class, we still don't have an object to work with.

So we're going to need to be able to do more here. We do know that a method need only be represented the one time, so let's go ahead and keep our decorator, but modify UberProperty to only store the method reference:

class UberProperty(object):

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

It is also not callable, so at the moment nothing is working.

How do we complete the picture? Well, what do we end up with when we create the example class using our new decorator:

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

in both cases we get back the UberProperty which of course is not a callable, so this isn't of much use.

What we need is some way to dynamically bind the UberProperty instance created by the decorator after the class has been created to an object of the class before that object has been returned to that user for use. Um, yeah, that's an __init__ call, dude.

Let's write up what we want our find result to be first. We're binding an UberProperty to an instance, so an obvious thing to return would be a BoundUberProperty. This is where we'll actually maintain state for the x attribute.

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

Now we the representation; how do get these on to an object? There are a few approaches, but the easiest one to explain just uses the __init__ method to do that mapping. By the time __init__ is called our decorators have run, so just need to look through the object's __dict__ and update any attributes where the value of the attribute is of type UberProperty.

Now, uber-properties are cool and we'll probably want to use them a lot, so it makes sense to just create a base class that does this for all subclasses. I think you know what the base class is going to be called.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

We add this, change our example to inherit from UberObject, and ...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

After modifying x to be:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

We can run a simple test:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

And we get the output we wanted:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(Gee, I'm working late.)

Note that I have used getValue, setValue, and clearValue here. This is because I haven't yet linked in the means to have these automatically returned.

But I think this is a good place to stop for now, because I'm getting tired. You can also see that the core functionality we wanted is in place; the rest is window dressing. Important usability window dressing, but that can wait until I have a change to update the post.

I'll finish up the example in the next posting by addressing these things:

  • We need to make sure UberObject's __init__ is always called by subclasses.

    • So we either force it be called somewhere or we prevent it from being implemented.
    • We'll see how to do this with a metaclass.
  • We need to make sure we handle the common case where someone 'aliases' a function to something else, such as:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
    
  • We need e.x to return e.x.getValue() by default.

    • What we'll actually see is this is one area where the model fails.
    • It turns out we'll always need to use a function call to get the value.
    • But we can make it look like a regular function call and avoid having to use e.x.getValue(). (Doing this one is obvious, if you haven't already fixed it out.)
  • We need to support setting e.x directly, as in e.x = <newvalue>. We can do this in the parent class too, but we'll need to update our __init__ code to handle it.

  • Finally, we'll add parameterized attributes. It should be pretty obvious how we'll do this, too.

Here's the code as it exists up to now:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)


class UberProperty(object):
    def __init__(self, method):
        self.method = method

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

[1] I may be behind on whether this is still the case.

查看更多
浪荡孟婆
5楼-- · 2018-12-31 04:46

I am surprised that nobody has mentioned that properties are bound methods of a descriptor class, Adam Donohue and NeilenMarais get at exactly this idea in their posts -- that getters and setters are functions and can be used to:

  • validate
  • alter data
  • duck type (coerce type to another type)

This presents a smart way to hide implementation details and code cruft like regular expression, type casts, try .. except blocks, assertions or computed values.

In general doing CRUD on an object may often be fairly mundane but consider the example of data that will be persisted to a relational database. ORM's can hide implementation details of particular SQL vernaculars in the methods bound to fget, fset, fdel defined in a property class that will manage the awful if .. elif .. else ladders that are so ugly in OO code -- exposing the simple and elegant self.variable = something and obviate the details for the developer using the ORM.

If one thinks of properties only as some dreary vestige of a Bondage and Discipline language (i.e. Java) they are missing the point of descriptors.

查看更多
还给你的自由
6楼-- · 2018-12-31 04:48

In Python you don't use getters or setters or properties just for the fun of it. You first just use attributes and then later, only if needed, eventually migrate to a property without having to change the code using your classes.

There is indeed a lot of code with extension .py that uses getters and setters and inheritance and pointless classes everywhere where e.g. a simple tuple would do, but it's code from people writing in C++ or Java using Python.

That's not Python code.

查看更多
与君花间醉酒
7楼-- · 2018-12-31 04:51

In complex projects I prefer using read-only properties (or getters) with explicit setter function:

class MyClass(object):
...        
@property
def my_attr(self):
    ...

def set_my_attr(self, value):
    ...

In long living projects debugging and refactoring takes more time than writing the code itself. There are several downsides for using @property.setter that makes debugging even harder:

1) python allows creating new attributes for an existing object. This makes a following misprint very hard to track:

my_object.my_atttr = 4.

If your object is a complicated algorithm then you will spend quite some time trying to find out why it doesn't converge (notice an extra 't' in the line above)

2) setter sometimes might evolve to a complicated and slow method (e.g. hitting a database). It would be quite hard for another developer to figure out why the following function is very slow. He might spend a lot of time on profiling do_something() method, while my_object.my_attr = 4. is actually the cause of slowdown:

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()
查看更多
登录 后发表回答