Atributos da Classe e Métodos Estáticos em Python
Atributos de Classe
Além dos atributos específicos de cada objeto, é possível definir atributos de classe. Esses atributos são variáveis declaradas no nível do próprio classe e compartilhadas por todos os objetos dela. Por exemplo:
class Person:
    type = "Person"
    description = "Describes a person"
print(Person.type)          # Person
print(Person.description)   # Describes a person
Person.type = "Class Person"
print(Person.type)          # Class PersonNeste exemplo, a classe Person define dois atributos: type, que armazena o nome da classe, e description, que fornece uma descrição. Esses atributos podem ser acessados pelo nome da classe, como em Person.type, e, assim como os atributos de objeto, podemos obter e modificar seus valores.
Esses atributos são compartilhados por todos os objetos da classe:
class Person:
    type = "Person"
    def __init__(self, name):
        self.name = name
tom = Person("Tom")
bob = Person("Bob")
print(tom.type)     # Person
print(bob.type)     # Person
# Alterando o atributo de classe
Person.type = "Class Person"
print(tom.type)     # Class Person
print(bob.type)     # Class PersonOs atributos de classe são úteis quando queremos definir valores padrão para todos os objetos da classe. Por exemplo:
class Person:
    default_name = "Undefined Name"
    def __init__(self, name):
        self.name = name if name else Person.default_name
tom = Person("Tom")
bob = Person("")
print(tom.name)  # Tom
print(bob.name)  # Undefined NameNeste caso, o atributo default_name guarda um nome padrão. Se o construtor receber uma string vazia como nome, o valor padrão de default_name é atribuído a name. Para acessar atributos de classe dentro de métodos, usamos o nome da classe, como em Person.default_name.
Atributo de Classe e Atributo de Objeto com o Mesmo Nome
Pode ocorrer que um atributo de classe e um atributo de objeto tenham o mesmo nome. Quando o atributo do objeto não tem um valor atribuído, o valor do atributo de classe será utilizado:
class Person:
    name = "Undefined Name"
    def print_name(self):
        print(self.name)
tom = Person()
bob = Person()
tom.print_name()    # Undefined Name
bob.print_name()    # Undefined Name
bob.name = "Bob"
bob.print_name()    # Bob
tom.print_name()    # Undefined NameNeste exemplo, print_name usa o atributo name do objeto, mas como o atributo de objeto não foi definido para tom, o valor de name da classe é usado. Quando atribuimos um valor ao atributo name de bob, ele passa a utilizar seu próprio valor em vez do valor da classe.
Se o valor do atributo de classe for alterado, o atributo name dos objetos que ainda usam o valor da classe refletirá essa mudança:
Person.name = "Some Person"
bob.name = "Bob"
bob.print_name()    # Bob
tom.print_name()    # Some PersonAqui, bob possui seu próprio valor de name, enquanto tom usa o valor atualizado do atributo de classe.
Métodos Estáticos
Além dos métodos comuns, uma classe também pode definir métodos estáticos. Estes são precedidos pelo decorador @staticmethod e são associados à classe como um todo, não a objetos específicos:
class Person:
    __type = "Person"
    @staticmethod
    def print_type():
        print(Person.__type)
Person.print_type()  # Person - chamado através do nome da classe
tom = Person()
tom.print_type()     # Person - chamado através de um objetoNeste exemplo, a classe Person possui um atributo __type, que armazena um valor comum para toda a classe. Como este atributo é precedido por dois sublinhados, ele é privado, evitando alterações não autorizadas.
O método estático print_type exibe o valor de __type. Como sua função não depende de objetos específicos, ele pode ser definido como estático, permitindo uma consistência ao exibir o mesmo valor para qualquer instância.