Attributes outside of __init__ in the presence of properties - python

Pylint tells me that I set _age outside of __init__ which is not good stylistically and I see why. However, if I use properties to ensure that my attributes are set within a certain interval, then it does make sense to have the attributes set in the property setter. How do I reconcile these two opposing thoughts?
class Person:
def __init__(self, age, height, weight):
self.age = age
#property
def age(self):
return self._age
#age.setter
def age(self, age):
if 18 <= age <= 81:
self._age = age
else:
raise ValueError('You are either too old or too young')

You are not really implementing the getters/setters properly. What you should be doing in your init is actually setting self._age = age:
def __init__(self, age, height, weight):
self._age = age
With that correction, now things will work as expected according to your design:
p = Person(1, 2, 3)
p.age = 10
Output:
ValueError: You are either too old or too young
Non exception:
p = Person(1, 20, 3)
p.age = 22
age = p.age
print(age)
Output: 22

It's clear that the answer given by idjaw misunderstands the design of the code, skipping the exception that should be raised when he tries to set the 'age' to 1.
The pylint exception appears to be a known issue that has just never been resolved. Your best bet would just be to use # pylint: disable=attribute-defined-outside-init in-line.

Related

Python: restrict value of the argument

How to restrict the value of sex argument here to either male or female?
class SoftwareEngineer:
def __init__(self, name, age, sex, level, salary):
sexes=['male','female']
if sex in sexes:
self.sex=sex.lower()
self.name=name
self.age=age
self.level=level
self.salary=salary
Second attempt:
class SoftwareEngineer:
def __init__(self, name, age, sex=['male','female'], level, salary):
sexes=['male','female']
if sex in sexes:
self.sex=sex.lower()
self.name=name
self.age=age
self.level=level
self.salary=salary
The simplest approach would be to validate your parameters before creating a SoftwareEngineer object - by the time __init__ is called the object has already been created. But, if you want to do your parameter validation in the initializer then you could do something like the code below to kick it back out to the surrounding code to handle.
Example:
class SoftwareEngineer:
def __init__(self, name, age, sex, level, salary):
self._validate_sex(sex)
self.sex = sex.lower()
self.name=name
self.age=age
self.level=level
self.salary=salary
def _validate_sex(self, sex):
if sex.lower() not in ["male", "female"]:
raise ValueError(f"Person's sex must specified as male or female not {sex}.")
parameters_validated = False
while not parameters_validated:
# Other parameter inputs not shown for simplicity
sex = input("Please enter sex of person [male, female]: ")
try:
swe = SoftwareEngineer("Fiona", 25, sex, 6, 12000)
except ValueError as exc:
print(exc)
print("Please try again.")
else:
print("All good, thanks.")
parameters_validated = True
Traditionally you’d enforce this by raising a ValueError, which is defined as:
Raised when an operation or function receives an argument that has the right type but an inappropriate value, and the situation is not described by a more precise exception such as IndexError.
class SoftwareEngineer:
def __init__(self, name, age, sex, level, salary):
sexes={'male','female'}
sex = sex.lower()
if sex in sexes:
self.sex = sex
else:
raise ValueError(“sex must be ‘male’ or ‘female’.”)
(Side note: I have friends who are software engineers who would be pretty annoyed at this arbitrary distinction. If you have to do this for homework, so be it. If you’re doing it for real, consider just storing whatever value gets passed in.)

Why can't i run a method from the same class in python?

Topic Closed
So I'm learning OOP in python and wanted to test my knowledge. That's what i did
class Student:
def cantBeStudent():
print('You don\' classify as a stududent')
def __init__(self, age, education):
self.age = age
self.education = education
if (self.age < 16) or (self.education < 3):
cantBeStudent()
student1 = Student(age=18, education=2)
I get name_error when i try to call cantBeStudent(). It says that cantBeStudent is not defined. I can't find my answer on google so I came here.
Edit: Also when i comment out whole cantBeStudent i get SyntaxError on def init
You need to add self to the method invocation and declaration:
class Student:
def cantBeStudent(self): # need self
print('You don\' classify as a stududent')
def __init__(self, age, education):
self.age = age
self.education = education
if (self.age < 16) or (self.education < 3):
self.cantBeStudent() # need self
student1 = Student(age=18, education=2)
OR
You need to invoke cantBeStudent as a static method like so:
class Student:
def cantBeStudent(): # no self as first argument, therefore static method
print('You don\' classify as a stududent')
def __init__(self, age, education):
self.age = age
self.education = education
if (self.age < 16) or (self.education < 3):
Student.cantBeStudent() # because declaration has no self,
# cantBeStudent belongs to entire Student class
student1 = Student(age=18, education=2)
When you construct a class, methods that you define must take the instance as the first argument. The class instance is referred to as self (though you could call it anything you wanted):
class X:
def __init__(self, number):
self.number = number
def add_number(self, arg):
self.number += arg
You see this when you define __init__. All other functions* work this way as well. When you call them like
instance = X(1)
instance.add_number(3)
It's analogous to doing:
instance = X(1)
X.add_number(instance, 3)
It's just calling the method against the instance will automatically pass self for you. When you call that method inside the instance, you need to specify the instance you are calling against, it's just this is called self instead of instance:
class X:
~snip~
def add_number(self, arg):
self.number += arg
def increment_number(self):
# note the self.<method>
self.add_number(1)
Again, this would be identical to the call:
instance = X(1)
X.increment_number(instance)
Because the instance gets passed in so that it can be called with the appropriate method
* All other functions that are not decorated with #staticmethod or #classmethod
You should provide self to any function that you want it to be counted as an object's method. If you do not want to provide self that function could be a static function (which means it does not rely on the type of the object itself). Then, you need to clarify that function, by #staticmethod decorator.
You missed self parameter on cantBeStudent method and when call it from contructor, it should be self.canBeStudent. Like this:
class Student:
def cantBeStudent(self):
print('You don\' classify as a stududent')
def __init__(self, age, education):
self.age = age
self.education = education
if (self.age < 16) or (self.education < 3):
self.cantBeStudent()
The purpose of self in Python: What is the purpose of the word 'self', in Python?
Add self on the function cantBeStudent
class Student:
def cantBeStudent(self):
print("You don't classify as a stududent")
def __init__(self, age, education):
self.age = age
self.education = education
if (self.age < 16) or (self.education < 3):
self.cantBeStudent()
That happens because you need to specify that the function is contained in the same class of 'self'
If you would done in this way it would had worked:
def cantBeStudent():
print("You don't classify as a stududent")
class Student:
def __init__(self, age, education):
self.age = age
self.education = education
if (self.age < 16) or (self.education < 3):
cantBeStudent()
The main difference is that in the first case, the function is inside the class Student, in the other case the function is out of the class, so don't need the 'self'

Better method to check all objects' parameters with a single call

I want to get all objects which age is less than 18. How can easily do that?
Should I had to pass all objects to get_nonage to get it?
class People:
def __init__(self, uid, age):
self.uid = uid
self.age = age
def get_nonage(self):
# print nonage here which age<18
# expected output: {1: 11, 3: 15}
pass
p1 = People(1, 11)
p2 = People(2, 20)
p3 = People(3, 15)
...
People.get_nonage()
Your design seems flawed here. Firstly, People is really Person. This is an important distinction, because the class name should match the data and functions it's built from. In your case, groups of people don't have a single age and id but a person does. Once you create a person, you'll put them into a list called people. Then, reduction/filtration/mapping operations such as the one you request are simple.
class Person:
def __init__(self, uid, age):
self.uid = uid
self.age = age
def __repr__(self):
return f"Person (id: {self.uid}, age: {self.age})"
if __name__ == "__main__":
people = [
Person(1, 11),
Person(2, 20),
Person(3, 15)
]
underage_people = [p for p in people if p.age < 18]
print(underage_people)
Output:
[Person (id: 1, age: 11), Person (id: 3, age: 15)]
If you want, you can put this list comprehension in a static class function as in atline's answer, but it doesn't seem appropriate to me. Such a method should belong to some other class that manages people, such as, say, a VoterRegistration application class that needs to determine who isn't eligible to vote. Or it could be in a People class that collects Persons in some way; either way, I think it's important to clearly establish what data and relationships you're modelling with classes and objects here. In both cases, the People class would have a member list of Person objects and a variety of functions to manipulate and report on that data, such as get_underage() (as written above in the list comp) or get_median_age(), for example. Here's a concrete sketch of what I'm talking about:
class Person:
def __init__(self, uid, age):
self.uid = uid
self.age = age
def __lt__(self, other):
return self.age < other.age
def __repr__(self):
return f"Person (id: {self.uid}, age: {self.age})"
class People:
def __init__(self, people):
self.people = people
def get_underage(self):
return [p for p in self.people if p.age < 18]
def get_average_age(self):
return sum([p.age for p in self.people]) / len(self.people)
def get_oldest(self):
return max(self.people)
def get_youngest(self):
return min(self.people)
if __name__ == "__main__":
people = People([
Person(1, 11),
Person(2, 20),
Person(3, 15)
])
print(f"Underage people: {people.get_underage()}")
print(f"Average age: {people.get_average_age()}")
print(f"Oldest: {people.get_oldest()}")
print(f"Youngest: {people.get_youngest()}")
Output:
Underage people: [Person (id: 1, age: 11), Person (id: 3, age: 15)]
Average age: 15.333333333333334
Oldest: Person (id: 2, age: 20)
Youngest: Person (id: 1, age: 11)
In order to search all of the People objects, you have to have all of the People objects.
But that implies you shouldn't have them in a bunch of separate variables in the first place, but in some collection, like a list.
Then, get_nonage can take that list.
While we're at it, there's no reason for get_nonage to be a method. It's not something that a People instance does, it's something you do to a bunch of People instances.
So:
class People:
# ...
def get_nonage(people):
nonage = {person.uid: person.age for person in people if person.age<18}
print(nonage)
people = [
People(1, 11),
People(2, 20),
People(3, 35)
]
get_nonage(people)
In a comment, you ask:
Can I just know the class is People, then use some method of People to directly get all data of its objects
What method would that be? You haven't written one. A class normally doesn't know all of its instances.
Of course if you really want it to, you can make a class record its instances in a class attribute. (A normal instance, like age, has a separate value for each instance. A class attribute has a single value shared by all instances.)
For example:
class People:
all_people = []
def __init__(self, uid, age):
self.uid = uid
self.age = age
self.all_people.append(self)
If you do that, then you can make get_nonage into a class method—that is, a method meant to be called on the class itself, rather than on an instance. A class method gets the class, cls, as its first parameter, instead of an instance, self, and it can only access class attributes, like the all_people we just created above.
#classmethod
def get_nonage(cls):
nonage = {person.uid: person.age for person in cls.all_people if person.age<18}
print(nonage)
And now, you can call it, without having to pass it anything, because it already knows everything:
people = [
People(1, 11),
People(2, 20),
People(3, 35)
]
People.get_nonage()
However, this is usually not a great design. For example, what if you wanted to delete a person? Previously, you'd just remove them from the list (or, in your original version, reassign p3 to something else). But then People.all_people won't know you did that; that person will still be there, and still be counted, even though you no longer have any way to access them.

When I run this program in PyCharm I get the following errors

When I run this code. I get the following errors
Traceback (most recent call last): File "C:/Users/Nabeel Hussain Syed/PycharmProjects/Hello World/check.py", line 80, in
print(spot.toString())
File "C:/Users/Nabeel Hussain Syed/PycharmProjects/Hello World/check.py", line 66, in toString
return "{} is {} cm tall and {} kilograms and say {}. His owner is {}".format(self.__name,
AttributeError: 'Dog' object has no attribute '_Dog__name'
Open the link of the image to check out the errors.
class Animal:
__name = None
__height = 0
__weight = 0
__sound = 0
def __init__(self, name, height, weight, sound):
self.__name = name
self.__height = height
self.__weight = weight
self.__sound = sound
def set_name(self, name):
self.__name = name
def set_height(self, height):
self.__height = height
def set_weight(self, weight):
self.__weight = weight
def set_sound(self, sound):
self.__sound = sound
def get_name(self):
return self.__name
def get_height(self):
return str(self.__height)
def get_weight(self):
return str(self.__weight)
def get_sound(self):
return self.__sound
def get_type(self):
print("Animal")
def toString(self):
return "{} is {} cm tall and {} kilograms and say {}".format(self.__name,
self.__height,
self.__weight,
self.__sound)
cat = Animal('Whiskers', 33, 10, 'Meow')
print(cat.toString())
class Dog(Animal):
__owner = ""
def __init__(self,name,height,weight,sound,owner):
self.__owner = owner
super(Dog,self).__init__(name,height,weight,sound)
def set_owner(self, owner):
self.__owner = owner
def get_owner(self):
return self.__owner
def get_type(self):
print("Dog")
def toString(self):
return "{} is {} cm tall and {} kilograms and say {}. His owner is {}".format(self.__name,
self.__height,
self.__weight,
self.__sound,
self.__owner)
def multiple_sounds(self, how_many=None):
if how_many is None:
print(self.get_sound())
else:
print(self.get_sound() * how_many)
spot = Dog("Spot", 53, 27, "Ruff", "Derek")
print(spot.toString())
Attributes with names starting with double underscores are considered "private", and not accessible from child classes. You could still access them by names like _Animal__name (Animal is a parent class name in which attribute was defined), but it's a bad practice.
More information in official documentation: https://docs.python.org/3.6/tutorial/classes.html#private-variables
the double-underscore has significance in Python. Please see this excerpt from a previous stack overflow answer:
Double leading underscore
This one actually has syntactical significance. Referring to
self.__var1 from within the scope of your class invokes name mangling.
From outside your class, the variable will appear to be at
self._YourClassName__var1 instead of self.__var1. Not everyone uses
this - we don't at all where I work - and for simple classes it feels
like a slightly absurd and irritating alternative to using a single
leading underscore.
However, there is a justification for it existing; if you're using
lots of inheritance, if you only use single leading underscores then
you don't have a way of indicating to somebody reading your code the
difference between 'private' and 'protected' variables - ones that
aren't even meant to be accessed by subclasses, and ones that
subclasses may access but that the outside world may not. Using a
single trailing underscore to mean 'protected' and a double underscore
to mean 'private' may therefore be a useful convention in this
situation (and the name mangling will allow a subclasses to use a
variable with the same name in their subclass without causing a
collision).

How to change an object's attribute with a method?

I have this code that stores a person's name, age and funds. I am trying to write a method that can take "age" or "funds", along with a number, and increase the given attribute by that number.
class Actor:
def __init__(self, name, age, funds):
self.name
self.age = age
self.funds = funds
def increase_age(self, increase_amount=1):
self.age = self.age + increase_amount
def increase_attrib(self, attrib, increase_amount=1):
self.attrib = self.attrib + increase_amount
a = Actor("Andrea", 32, 10000)
a.increase_age() works fine: calling it increases the age of Andrea to 33, just as expected. However, a.increase_attrib("age") gives an error, saying AttributeError: 'Actor' object has no attribute 'attrib'. a.increase_attrib("funds") gives similar results.
If I just say a.increase_attrib(age) (without the quotes) I get a NameError, which is what I expected.
By my understanding, giving the parameter "age" to increase_attrib() should mean that the attrib mentioned becomes age, so that increase_attrib() references self.age instead of self.attrib. Apparently, I was wrong.
Now, I could just use increase_age() instead, but then I'd have to make different methods for age and funds, and even more methods once I add other features to Actor, like location, gender, and nationality.
What do I need to do so that I can pass the name of an attribute to a method and have it change that attribute?
You are looking for setattr:
setattr(obj, 'foo', 42)
Is the same as
obj.foo = 42
So for your example:
def increase_attrib(self, attrib, increase_amount=1):
setattr(self, attrib, getattr(self, attrib, 0) + increase_amount)

Categories

Resources