4

Person.py

class Person:
    def __init__(self, pname):
        self.name = pname

    @classmethod
    def parse(cls, name):
        return cls(name)

Employee.py

class Employee(Person):
    def __init__(self, ename, esalary):
        super().__init__(ename)
        self.salary = esalary

    @classmethod
    def parse(cls, data):
        person = super().parse(data["name"])
        person.salary = data["salary"]
        return person

Customer.py

class Customer(Person):
    def __init__(self, ename, country):
        super().__init__(ename)
        self.country = country

    @classmethod
    def parse(cls, data):
        person = super().parse(data["name"])
        person.country = data["country"]
        return person

main.py

emp_data = {
    "name": "john",
    "salary": 1000
}
emp = Employee.parse(emp_data)
print(type(emp))
print(emp.name)
print(emp.salary)


cust_data = {
    "name": "peter",
    "country": "USA"
}
cust = Customer.parse(cust_data)
print(type(cust))
print(cust.name)
print(cust.country)

errors

TypeError                                 Traceback (most recent call last)
<ipython-input-32-a5abd51d9870> in <module>()
     36     "salary": 1000
     37 }
---> 38 emp = Employee.parse(emp_data)
     39 print(type(emp))
     40 print(emp.name)

<ipython-input-32-a5abd51d9870> in parse(cls, data)
     14     @classmethod
     15     def parse(cls, data):
---> 16         person = super().parse(data["name"])
     17         person.salary = data["salary"]
     18         return person

<ipython-input-32-a5abd51d9870> in parse(cls, name)
      5     @classmethod
      6     def parse(cls, name):
----> 7         return cls(name)
      8 
      9 class Employee(Person):

TypeError: __init__() missing 1 required positional argument: 'esalary'

This example is just for reproducing the problem in the actual code. The actual parse functions involve complex logic.

In the above example, Employee and Customer classes extend Person class. According to the error, init method of Person class which is invoked by parse form same class, is expecting esalary. But there is no esalary property on Person init method.

What's wrong with my program? I think I did not understand inheritance properly. Please, correct me. First of all, did I structure my program correctly?

martineau
  • 119,623
  • 25
  • 170
  • 301
lch
  • 4,569
  • 13
  • 42
  • 75
  • 3
    `cls` will be `Employee` not `Person` – juanpa.arrivillaga May 01 '18 at 20:24
  • why? could you explain that as an answer? – lch May 01 '18 at 20:26
  • @What first argument for what? – lch May 01 '18 at 20:28
  • Take a look :) : https://stackoverflow.com/questions/26788214/super-and-staticmethod-interaction – rafaelc May 01 '18 at 20:28
  • @LokeshCherukuri Im not 100% sure thats why I removed my comment but normally every first argument of a object refers to self. So when you pass cust_data, you actually only pass to self. I don't know the edge-cases however. – What May 01 '18 at 20:34
  • About your program structure, why do you need the parse method? You can pass the data into the `__init__`, it would be simpler, and essentially the same thing. – progmatico May 01 '18 at 20:35
  • @progmatico They mentionned this is a simplified example. In their real code I'm sure they are parsing something much more complicated which doesn't map directly to the init arguments. – jbch May 01 '18 at 20:45
  • exactly. if I have all those arguments readily available to pass to init, why would i create parse? – lch May 01 '18 at 20:48
  • Ok, I see they will want that complex methods to be classmethods, not instance methods or complex `__init__` code, but even then, making `__init__` call the class parse method will make the object instantiation more natural (instantiate with data object argument vs calling parse directly in client code). – progmatico May 01 '18 at 20:55
  • @progmaticoThey may be instantiating the class with just params from other places in the code so they want to keep init straightforward. I think it's a matter of taste whether you want `__init__` to dispatch based on params and/or type inspection or if you want to have multiple public constructor methods. An alternative would be to always use the constructor and have `parse` set the relevant fields instead of having `parse` create the instance. – jbch May 01 '18 at 21:26
  • I agree @jbch. And for dispatching based on type there is also funtctools singledispatch decorator, which compensates that absence. – progmatico May 01 '18 at 21:51
  • @jbch Basing on comments about using instance methods, I changed my code https://trinket.io/python/c161df00a4 . In this case, I don't need to init method at all. are there any pros and cons of each of these approaches (class methods vs instance methods). personally, I like to write simple init. so that code reader can easily know what properties that class have – lch May 02 '18 at 00:28

2 Answers2

4

Your problem is that parse calls cls(name), which only has one argument, but cls is Employee or Customer, not necessarily Person. You must either provide the additional arguments (salary/country) at construction time (when you call parse), or give them a default value when they aren't provided.

Either provide the additional needed arguments to the constructor via parse:

@classmethod
def parse(cls, name, *args, **kwargs):
    return cls(name, *args, **kwargs)

which would allow you do something like:

Employee.parse(name, salary)  # Really no different than Employee(name, salary)

Or add defaults to those parameters in the child constructors so that they can be constructed with only a name:

class Customer(Person):
    def __init__(self, ename, country=None):
        # ...

class Employee(Person):
    def __init__(self, ename, esalary=None):
        # ...

Note that this might result in you having None values floating around in these attributes, but that's inevitable if you want to construct a new, say, Employee object and don't want to also provide a salary at the same time.

scnerd
  • 5,836
  • 2
  • 21
  • 36
  • adding *args, **kwargs to Person's parse did not solve the problem – lch May 01 '18 at 20:42
  • Question: if cls is Employee, i should call cls(name, salary). what if salary data is not available at that moment. what if i want to initialize salary only after crating employee object? – lch May 01 '18 at 20:44
  • Then add default values, like I suggest in the second code block. A reasonable default would be `None`, e.g., `def __init__(self, ename, esalary=None):` – scnerd May 01 '18 at 20:49
  • `*args` and `**kwargs` only pass on extra arguments that got passed to `parse`. If they didn't get passed to `parse`, then there's nothing to pass on, so the constructor still receives too few parameters. If you're willing to tolerate missing data in these classes, at least briefly, add default values of `None` to these extra parameters in your child classes. – scnerd May 01 '18 at 20:51
  • adding default value `None` solved the problem. Finally, Could you explain in simple terms why cls is Employee here in your answer? is that how inheritance works? – lch May 01 '18 at 20:53
  • Yeah, that's what `classmethod` does is give you the type/class on which the method was called. So `Employee.parse` makes `cls=Employee` and `Customer.parse` makes `cls=Customer`. The same `parse` function gets called in either case (it's inherited), but the `cls` argument encapsulates *how* it got called, since that's important in this case. – scnerd May 01 '18 at 20:57
3

Consider the following:

class Foo():
    @classmethod
    def p(cls):
        print(cls.__name__)

 class Bar(Foo):
     @classmethod
     def p(cls):
         super().p()

bar = Bar()
bar.p()

This will print "Bar". The class object passed to Foo.p when called from within Bar.p is actually the Bar class, not the Foo class.

So in your code, when you call Person.parse through Employee.parse, your line in Person.parse, return cls(name) is actually calling Employee(name), but Employee's init take 2 positional arguments.

For your code to work with this structure, your __init__s have to have the same signature, or at least compatible signatures (for example by adding *args to Person's __init__ method and passing them to the constructor).

jbch
  • 591
  • 2
  • 6