 So, just let me know. So yeah, I'm going to talk about why I think Python is not easy and why I hope, and I hope this talk will help you think that it is easy when you actually do open source, the same of this talk. So, what do I do? I'm Sadna Srinivasan. I work in AI and ML research at a company called SAMA. SAMA works in optimizing clinical trials using AI and ML. Currently, I'm trying to figure out if neural networks can be forced to follow hard logic. They're famously something that you cannot control, but we're trying to work on that. This talk is based of SIMPAI. It's based of some work that I did for the company using SIMPAI. The idea was to be able to run neural networks on a quantum computer in a nice and intuitive way. We had to use SIMPAI for that, but it was too slow and we had to optimize it. So, this is things that I encountered when I had to work with it. So, why do we say that Python is a really easy language? It's very easy to learn. You spend 25 hours, 14 hours, and you're good to go. You can start building web servers, you can start building apps, and it's very intuitive to use. Once you have some experience, you can also predict how other libraries with all of their awesome functions are going to behave. You know it's going to be as simple as import something, instantiate an object, give the object some data, take a scikit-learn as an example, and then fit it. You can expect that sort of simplicity, and that's why Python is so easy. I say that Python is not easy. Like we saw in the keynote today, Python was not meant to do what it is doing right now, and it's grown in complexity over time. And all of that complexity is still happening behind the scenes where the end user does not really see it. But as an open source first time open source contributor, you're going to have to deal with some of that complexity, and this talk is centered around concepts that you will encounter when you start doing this. So in this talk, I'm going to touch upon inheritance in Python. I'm going to talk about mixing classes, which is a good to know, and it is not a feature of Python. Then we're going to talk about decorators, slots, metaclasses, and then I'm going to talk about just a bit about what SimPy is and how all of this combines in the SimPy code base. So we all know what inheritance is. Let's say you have three classes. You have a class person, which could be inherited by a class royalty, and then you can have the Queen of England who inherits from the class royalty. So this is nice linear inheritance. We know this, but Python allows multiple inheritance, and most other languages don't allow this. And the reason they don't allow this is because if you look at the class E that I have in this diagram, which is an example inheritance flow that you could see in Python, you have class E, which is inheriting from DNC, and DNC in turn have inherited from B and also A. So any property that A has, both DNC are going to have, and when it comes to E, you're not really sure which property to keep. But this obviously works in Python. This is a toy example. So I have parent class one and parent class two. Both have the exact same attribute parent name, and I am instantiating a child class, which inherits from parent class one and two. Now, when I check this particular attribute, it prints out E. Now this is due to Python's dynamic ordering, which has a system for how the attributes of different classes are overridden when we do inheritance. So whenever you do multiple inheritance, if there is a clash, the leftmost class or the first class that you gave into that inheritance is the one whose properties are going to get preserved. So that's all about inheritance. And I'm going to talk about mix and classes. This is not a feature of Python. You will not find this documented anywhere as such. It's just something that you kind of encounter in different code bases. Sympy is one, Django is another. What they essentially mean is it's a nice way of encapsulating behaviors. So let's say you have, for example, taking Sympy as an example. Let's say you have different expressions, polynomials, you have integrals. All of these expressions can be evaluated. So the behavior that something can be evaluated can be put in a separate class. And whichever other class needs that behavior can just take this extra class and mix it in so that behavior gets mixed in. The reason this is done is that it makes the code bases very readable. Once you see a mix and class, you know it's a repeated behavior that occurs in the code base. And when you read really huge code bases, this can be very helpful if it exists in the code base. So now I am going to talk about decorators. So before that, everything in Python is an object. Every single thing in Python is an object. So functions are also objects. So functions are an object of type function, and they have no other attributes. But since functions are objects, you could also do something like this, which is very pointless. I don't think you should do it, but you can do this. Just add an extra attribute to a function. You can do it. But since functions are objects, you can pass functions as input to other functions. And this is the basis of what decorators are. So here you have the decorator function, which takes another function as its input. And this one times the function, so I check the time at the beginning, and I check the time at the end, print the difference. So this way of using functions as decorators is syntactic sugar for saying decorator function of this long process that I've defined. So it will run this long process and print out the amount of time it took. So why would we use decorators? It's repeatable. You have this function that will always time something. So you can just whichever function you want to time, you can just decorate it with this function, and it'll work. So you could also use classes as decorators. Except when you're using a class as a decorator, you'd have to modify the call function. And let's say you have a class error check. So you have to initialize it saying that yes, you're going to get a function as an input. And you're going to tell whatever else you want to happen, you're going to have to do it in call. And here I'm saying, OK, I want to check the parameters. And I want to make sure all of them are of the type, are not string, basically. So I have another function, which is add numbers. And I decorate that with error checks. So the first one will run. If I say add one, two, three, it'll run as expected. But if I try one string, two, and three, that's going to throw up an error. And again, decorators help whenever you want to make things very repeatable. So you can also decorate classes. I don't have an example for that here. But how many of you have used PyTest? All of us. OK, a lot of us. So you could actually reimplement PyTest using decorators. You could say, OK, I have this particular class. I want to test it. And you can encapsulate all of the tests in different classes, decorate each of those classes so that whenever you run test, you can just run each of those functions. So decorators allow you to do this sort of thing. You could also do things like check which functions are used more often. Whenever a function is called, you modify a global variable or you modify something, which gives you a count of how many times this function is used. So you can use decorators in many different ways. You can use them in very creative ways. And these are some things that you will always see. And each, yeah. So I'm going quite fast. So in case someone wants to stop me, please feel free. So now I'm going to talk about slots. This is where classes don't behave like classes anymore. So when you have a normal class in Python, this is a normal class, which is a person. You have a name. You have an age. And I'm saying that by default, everybody is a coder. And I have a normal in it. I have an object. And I try to add an attribute to that object. All of this works. This is behavior that we're all very familiar with for classes. Here I'm just showing all of the attributes of each of these objects. So Bezos is an object. And you have the name, age, and the extra attribute that we just added. I'm also showing you the attributes of the class person, since person is also an object, it will also have an attribute dictionary. So you check that you have your name, age, and coder, which is just here. If coder has a default value, that's already here. So this is how it looks. But it's going to look slightly different with slots. So the way I define it is I'd say I have a class person, which has three different slots. Same in it, there's absolutely no change. Same way we initialize objects, there's no change. I'm printing the attribute dictionary of person before. And then I'm trying to add an attribute to that object. As you see, this will fail. And if you check the attribute of person, you'll see that age becomes a particular member. It's not just something that it's not a placeholder anymore. So what is actually happening? So by default, Python will use a dictionary. And whenever you use a dictionary, it's mutable. You can add attributes. And you can add attributes at runtime. You can change them. But since it is so mutable, it also slows down the whole process. But when you use slots, you're telling Python, hey, look, don't do all this. I know I'm only going to use these three slots, or these four slots. You can go ahead and allocate memory for this. And why do we do this? Because one, the use of RAM becomes much better optimized. Things run much faster. And if you have a really huge code base and you're entirely sure about exactly how the class is supposed to be used, you can prevent accidental misuse of classes. You won't have an obscure part of the code where somebody just does something with some attribute and you don't see it anywhere else. You won't have that. Now, why would we do this? So let's take SimPy. SimPy has symbols. And it has multiple other classes which gets initialized before symbols get initialized. And there are classes in SimPy which, for normal use, will get you to easily 100 different objects that exist at the same time. So slots help optimize all that. You don't want your system crashing. So slots help with that. So now I am actually going to talk about metaclasses, and this is where I slow down. In Python, every class is of type type. Class definitions are objects, and they are objects of type type. And this enables all of the default behavior that we see. We see by default we have init, we have call, we have all of these behaviors that we are used to. This is what enables all of those behaviors. Metaclasses allow programmers to take control of all of these behaviors. So this is just an example. I have an example class, and I check the type of that example class, and it's of type type. This is a bit of syntax. So how do we define a metaclass? Any class that inherits from type is a metaclass. So you inherit from type, and then you change whatever behavior you want to change. You don't want to have to change and define every single behavior every time, so that's why it's done like this. We also have to tell Python that, look, we're not going to inherit from object, we're going to have our own metaclass defined. So this is a class meta. This is, so you have class meta, which inherits from type. And all I'm doing here is that every time an object is initialized, I'm saying I want this attribute to be added. And so I have a class where I'm defining that metaclass. This is how you specify it. There's no other definition there. Initialize an object. And then when I print brown dot coder, voila, you see true. But when you check the type of that object, it's no longer of the usual type object. It's of type main meta. It's dunder main dunder because this metaclass was defined in main. And yeah. So this is another much better example of how metaclasses could be used. This is the same example that I mentioned before in Decorators. We can, sorry. We can use metaclasses to keep track of objects which get initialized in certain classes. And the idea is we want to keep track and we want to know the number of objects and the number of ways in which a different class is being used. So here I have a global variable models and I have a metaclass which will update this global variable every time a new object is initialized. And then I have a class model which inherits this metaclass. And when I initialize it and afterwards I check the global variable, I have that automatically updated. You could also do this with Decorators like I mentioned before, but it's just that I personally find this cleaner and everything that you inherit from model will inherit the same behavior. So I find this a little bit cleaner. So yeah, this could be achieved by a Decorator but that's not as clean. But metaclasses are way more powerful. You can change the, sorry, I think I skipped this slide. You can change the basic structure of a class itself. So this, we just saw that you have, the classes use dictionaries. Suppose we have to process that dictionary and for some reason we don't want it to be a dictionary but we want it to be an ordered dictionary. Metaclasses will allow you to change things like that. Here, again, I'm defining a metaclass and since I'm changing the way the class itself, the objects are going to be, I change prepare and instead of returning a normal dictionary, it's going to return an ordered dictionary in this case. I'm changing you just to print out the list of attributes that are there. There's nothing really going on there and I have a class A. This one has a metaclass, it has a few attributes and if you look at the attributes list, you see that it's all ordered and that was the point of this example and you can change this to anything. You want, you want ordered dictionary, you need it to be some sort of dictionary or tuple or something like that but other than that, you have complete control. You can also change the base class. Going back to the initial example that I used with inheritance, you have the class Queen of England which has inherited from royalty and royalty has inherited from person. Now, we obviously do want Queen of England to also identify as person which means we want its base class to also be person. So if you want to manipulate the base class, if you want to manipulate the name of a class, all of that metaclasses allow you to do here in this case, you have to change new. So here you can change what the metaclass is to here for record keeping. So here, instead of returning just the object, I'm saying I want you to define it in this way. I want to change the name to foo and I want to change the base class to int and so I have a base class which has the metaclass, this metaclass and then you have class A which inherits from base class and you have class B which inherits from A. So by default, if you try to check is B a subclass of base class, it won't evaluate true. But here I'm checking A is named foo, B is named foo, the outputs are below and you also try to check is B a subclass of A now that will evaluate to false because basis is empty except for int. And is B a subclass of int? Yes, B is a subclass of int because we've defined it to be that way. A word of caution, metaclasses are really powerful, they're really flexible, but they add a lot of complexity to the code and especially it becomes very hard to read code. You might use a metaclass now and then you inherit five other classes from it and you keep using and then you inherit from those classes. The behavior of the metaclass is propagated but for you to actually figure out that this particular object is behaving weirdly because somebody has changed the metaclass downstream somewhere can get really hard for someone. So use it with a pinch of salt but if you can't find any other solution this is the way to go. So I'm just gonna touch upon what SimPy is. SimPy is symbolic Python. We all have done symbolic mathematics which is the mathematics that we use in school. You have symbols x, y and z and you try to solve for them in some way and you manipulate those symbols without actually knowing what value they carry. So SimPy lets you initialize symbols. So they're the fundamental thing in SimPy. So here I have three variables and I'm solving this particular equation which is sin x minus x and I can even define a domain. I can say, hey, solve this equation within this domain. And you can also do solving of linear equations where you can define those equations say solve for x, y and z. You can get those answers. So this is what SimPy does. SimPy does all sort of symbolic mathematics. It's similar to Wolfram. It's similar to other things that you've seen. So I said I worked with SimPy. This is a bit of SimPy's inheritance tree. SimPy has around 1,200 classes, 1,700 classes, something like that. This is a part of it. This is what I had to work with mostly. So I had to work with around 1,000 symbols at the same time and manipulate all of them. And we wanted to make that asynchronous and we wanted to add some properties to help speed up things further. So that's when we hit upon slots. We realized we can't just add properties and that's when we realized metaclasses also exist and managed properties is not gonna behave like a normal class. And that property starts propagating. You also have evalif mixing, which is the only thing that kind of helped where you have, this is the point where a function gets evaluated and that's inherited by addition, subtraction, multiplication and all of them have this. So you change evalif mixing. Everything works and anything that inherits that is gonna work just like that. So this is mostly what I have to say. We've talked about inheritance. We've talked about a lot of things. We've talked about inheritance, mixing classes, decorators, metaclasses. We've talked about how they could be used and are used in SimPy. And yeah, I think we're open to questions. Do we have questions? Sorry. Do we have questions? Yeah. Someone's saying something, but I can't hear it. Hi. Yeah. Am I audible? Yeah, you are. Yeah, so about the mixing classes, can you elaborate on that? Is it like calling the super function or? No, it's just a convention. I'm gonna take this off. So it's just a convention. It's just something that people do, which makes code bases more maintainable and more readable. It is not a feature of Python. It's not involved with super. It's just a class that you define. Let's say you have, okay, let's say you're doing something in which models all of the people and you're gonna have an object, a class for every person in the world. And there is a property that people can read. Instead, and they can read different things. People can read English. People can read Hindi. There's a lot of different things that reading can do. But you can just keep this, but everybody reads in the same way. You see something, you do some processing and you read. So you isolate that behavior reading as read mix in, maybe. And then whenever you have a class person who can read, you just mix that behavior in. And presumably you'll have languages that they can read, except as attributes which will then be processed. So it's a way to isolate behavior so that the code is really readable. Here, back here in the back. Okay, hi. Hi, hi, sir. Thank you. It was a great talk. So, you wanna have questions? Yeah, over here. First row? Yeah, yeah. I'm getting some interference. Yeah, okay. Yeah, so at the beginning you, when you were talking about SMPI, you had mentioned something about hard logic in neural networks. Could you elaborate on that? Possibly show some references to any research papers you're working on? For the research papers, you just Google neural networks and logic. You're gonna get a lot of them. Symbolic logic in neural networks. Basically what it is, is that neural networks are really hard to control and their behavior becomes undefined beyond certain boundaries. This research is to basically not have that happen and have some control over the way neural network behaves despite the inputs that we get. We can talk about this later. I don't think it's directly related. We can talk about it. Yeah, any questions? Yeah. Yeah, so this is regarding metaprogramming. So I also use a lot of metaclasses in my work. So there is one problem that I have not been able to solve is if let's say you are in the main function and there are functions in that main module that are not part of any class. So can you control those as part of your metaprogramming in any way? Probably, I really don't know. You potentially could, but I would say please don't use that many metaclasses. They're not good for readability. They are good for maintainability. Unless you absolutely have to, please don't. Yeah, okay.